Home / Stories / Vault as code
Story · Secrets & access In development

Your Vault lives in git. Someone should read it like a reviewer would.

Most teams click Vault together in the UI or with one-off CLI calls. A few go further and declare policies, auth roles and secret engines in a repository that CI applies, which is already beyond what is typically automated. The assistant reads that repository every day and flags the grants a careful reviewer would stop. Keeping it in step with the Kubernetes workloads that use it is where this is going next.

Starts fromA Vault config repo: HCL/JSON policies, auth roles, engine and auth-method declarations, applied by your CI.
Looks atEvery declared policy path and capability, every role's policy list, read from git through the GitLab API.
You getSecurity issues that name the exact path, the capability and the fix. Draft MRs where the change is provably narrow.
01 · Vault, declared

One repo. CI applies it. Nobody clicks.

Policies live under policies/, auth roles under roles/, and auth methods and secret engines are declared next to them. A pipeline plans on every merge request and applies on the default branch, with a short-lived token from your own CI.

The assistant never applies anything. It reads the repo like any other reviewer and proposes changes through the same merge requests your team uses.

02 · Read it exactly

A small parser for a small grammar.

Vault policies are flat: a path and a list of capabilities. The assistant parses them with a focused, dependency-free reader that copes with either brace style, and loads role JSON alongside, so every later check works on structure, not on text matching.

On a real 14-policy, 4-role estate this reader and the rules below produced zero findings. That clean baseline is the point: it only speaks when something is actually wrong.

03 · Capability hygiene

Write or sudo on an escalation surface.

A grant is flagged when it gives sudo, or create / update / delete, on a path that controls Vault itself:

  • near-root wildcards: *, sys/*, auth/*
  • the control-plane engines: sys/, auth/, identity/
  • audit devices
  • CA and key management on pki/, transit/, ssh/

A companion check catches broad read/list reach on those same wildcard surfaces.

04 · Don't cry wolf

Least-privilege consumer paths are exempt, by name.

A service that issues certificates needs update on pki/issue/<role>. That is a correctly scoped grant, not a finding. So the check fires on everything under the consumer engines except a curated list of consumer leaves: issue, sign, encrypt, decrypt, creds, and the public read endpoints.

Anything not on that list keeps firing, including management verbs nobody thought to enumerate. Key export and verbatim signing stay out of the list on purpose.

05 · Cross-reference integrity

A role that grants a policy that does not exist.

An auth role lists the policies its tokens receive. If one of them is missing from policies/, the role mints tokens bound to nothing: today that is a silent no-op, and the day someone creates a policy with that name it becomes an unreviewed grant.

06 · Findings go out as issues

Named path, named capability, "N of M paths".

Each finding says which file, which path, which capability, and how much of the policy it concerns. It passes the same fail-closed safety layer as the audit story: the protected root policy is never flagged or edited, and nothing touching an audit device is ever proposed.

Lint findings are issue-only today. The one path that already produces draft MRs against this repo is the audit-driven policy widen from the previous story.

07 · Consistent with its consumers Roadmap

Planned: check Vault against the workloads that use it.

Planned: join the Vault repo to the cluster side. Every ExternalSecret or agent-injected path should be readable by the policy its ServiceAccount's Kubernetes auth role grants, and every role should point at a ServiceAccount and namespace that still exist.

Today the assistant sees Vault on the cluster side only as an External Secrets backend, so this join is not built yet.

08 · Declared vs live Roadmap

Planned: compare git with what Vault actually runs.

With a read-only Vault token (list and read on policies, auth methods and mounts), the assistant could flag policies, roles or engines that exist in Vault but not in git, the classic "someone fixed it in the UI" drift. This needs an operator-provisioned token and is not built.

acme/vault-config · main
policies/        14 files · HCL
  shop-orders-api.hcl  payments-worker.hcl  pki-issuer.hcl  …
roles/           auth roles · JSON
  kubernetes-shop.json  approle-ci.json  …
auth/            kubernetes · approle · oidc
engines/         kv-v2 · pki · transit
.gitlab-ci.yml   plan on MR · apply on main
Merge requestplan
mainapply job
Vaultpolicies · roles · mounts
The assistant reads the repo. Your CI owns the apply.
policies/payments-worker.hcl → parsed
path "secret/data/shop/payments/*" { capabilities = ["read"] }
path "transit/encrypt/payments"
{
  capabilities = ["update"]
}
  • secret/data/shop/payments/* read
  • transit/encrypt/payments update · consumer leaf
14 policies · 4 roles parsed · 0 findings on a clean estate
policies/pki-issuer.hcl
path "pki/issue/shop-internal" {
  capabilities = ["update"]
}
path "pki/root/*" {
  capabilities = ["create", "update", "sudo"]
}
path "sys/mounts" {
  capabilities = ["read"]
}
  • pki/root/* sudo_grant · CA management
  • pki/issue/shop-internal consumer leaf · exempt
  • sys/mounts read, exact path · no finding
consumer-leaf exempt list · write + sudo matrix
  • pki/issue · pki/sign · pki/cert · pki/ca · pki/crl exempt
  • transit/encrypt · decrypt · sign · verify · hash · random exempt
  • ssh/sign · ssh/creds · ssh/public_key exempt
  • pki/keys · pki/issuers · pki/intermediate · pki/roles fires
  • transit/keys · transit/export · transit/backup · restore fires
  • pki/sign-verbatim · pki/sign-intermediate fires
escalation by default · exempt only the known-safe leaves
roles/kubernetes-shop.json
{
  "bound_service_account_names": ["orders-api"],
  "bound_service_account_namespaces": ["shop"],
  "token_policies": ["shop-orders-api", "shop-orders-api-v2"]
}
  • shop-orders-api policies/shop-orders-api.hcl
  • shop-orders-api-v2 role_missing_policy · not in policies/
Detectorsoverbroad_capability · sudo_grant · wildcard_path · role_missing_policy
Safety layerprotected root policy dropped · audit devices untouchable · unsure = suppress
Issuecategory:security · source:vault · "1 of 3 paths"
cluster · namespace shop
ExternalSecret orders-api-psp
ES
ServiceAccount orders-api
SA
vault-config repo
role kubernetes-shop
R
policy shop-orders-api
P
  • ExternalSecret path readable by the SA's role policy? planned
  • role still points at a live SA + namespace? planned
Roadmap. Illustrative of the planned join.
declared (git) vs live (Vault) · planned
  • policy shop-orders-api in git · in Vault · identical
  • policy tmp-debug in Vault only · created outside git?
  • auth/approle role legacy-batch in Vault only
  • mount kv-old/ in Vault only

The issue you get.

Illustrative. Static-lint findings are issue-only today, because a Vault capability change needs a human decision about intent. This is the shape the assistant writes.

Open issue

Vault policy pki-issuer grants sudo on pki/root/*

in acme/vault-config · policies/pki-issuer.hcl · 1 of 3 paths
category:securitysource:vaultrisk:high
OverviewEvidenceSuggested changeWhy not an MR
Summary

Policy pki-issuer grants create, update and sudo on pki/root/*. That subtree manages the root CA: generating, importing and replacing it. Any token with this policy can mint a new root for the whole PKI engine.

Scope

1 of 3 paths in this policy is flagged. pki/issue/shop-internal (issuing leaf certificates) is a least-privilege consumer path and is not flagged.

PathCapabilitiesVerdict
pki/issue/shop-internalupdateconsumer leaf, exempt
pki/root/*create, update, sudosudo_grant on CA management
sys/mountsreadread on an exact path, no finding

Roles binding this policy: approle-ci.

If the CI role only issues certificates, drop the root block:

 path "pki/issue/shop-internal" {
   capabilities = ["update"]
 }
-path "pki/root/*" {
-  capabilities = ["create", "update", "sudo"]
-}

If root rotation is a real need, move it to a separate policy bound only to the operator identity that performs rotations.

Removing a capability can break a job that depends on it, and only you know whether the CI role rotates the root CA. The assistant files the evidence, and the suggested change in this mock is the kind of fix to consider; the decision stays with you.

Turning lint findings into draft MRs where the narrowing is provably unused (no successful request on that path in the audit window) is on the roadmap.

What keeps it honest.

Precision matters more than coverage here: a Vault finding that is wrong teaches people to ignore the next one.

Deterministic, no modelPure functions over parsed policy and role files. The same repo always gives the same findings.
Tested in both directionsEvery consumer path is pinned to produce zero findings, and every privileged path is pinned to fire, with write and sudo.
Escalation by defaultUnknown management verbs keep firing. Only named consumer leaves are exempt, so a forgotten path fails loud, not silent.
Protected policies stay protectedThe operator's root policy is recognised and never flagged or edited; audit devices are never the subject of a proposal.
Read-only by constructionThe repo is read through the forge API. Vault is never contacted. Your CI remains the only thing that applies.
Opt-in per estateOff by default, enabled per Vault repo, and scheduled in the quiet early-morning band so a first scan never lands in working hours.

Where this stands.

Live
  • Daily lint of the Vault config repo (policies and auth roles)
  • Capability hygiene on escalation surfaces, with the consumer-leaf exempt list
  • Role → policy cross-reference integrity
  • Issue-only security findings through a fail-closed safety layer
In development
  • Draft MRs to the Vault repo from audit-log widen findings (see Vault audit logs)
Roadmap
  • Consistency with Kubernetes consumers: ExternalSecret paths, ServiceAccounts, auth roles
  • Declared-vs-live drift for policies, auth methods and engines
  • Lint checks for auth-method and secret-engine declarations
  • Draft MRs for lint findings proven unused by the audit log
← Previous storyVault audit logs to policy fixes