KV v2 policy written against the wrong paths
An application team stores configuration on a KV v2 mount at kv/ (they use vault kv get kv/myapp/db). Their policy contains exactly one stanza:
path "kv/data/myapp/*" { capabilities = ["create","read","update","delete"] }
vault kv get works, but vault kv list kv/myapp returns 403 and vault kv destroy returns 403 - which security is quietly pleased about, since the app must never be able to permanently remove a version. Two requirements have now landed: the platform on-call team must be able to destroy versions under the same tree, and kv/data/myapp/prod/stripe holds a payment key that only the payments service may read, even though the rest of the team legitimately needs everything else under myapp/*. The team's proposal is to add path "kv/data/myapp/prod/stripe" { capabilities = ["deny"] } to the shared policy.
1. Explain each 403 and write the corrected policy for the application, keeping destroy off the table.
Read the worked answer
The CLI verbs map to different API paths on a KV v2 mount; a policy for kv/data/... only covers reads and writes of version data.
| Command | API path | Needs |
|---|---|---|
vault kv get kv/myapp/db |
GET kv/data/myapp/db |
read on kv/data/myapp/* โ |
vault kv list kv/myapp |
LIST kv/metadata/myapp |
list on the metadata path - missing |
vault kv delete |
DELETE kv/data/myapp/db |
delete on data (soft delete) โ |
vault kv destroy |
PUT kv/destroy/myapp/db |
update on kv/destroy/* - correctly absent |
vault kv metadata delete |
DELETE kv/metadata/myapp/db |
delete on metadata - correctly absent |
Subtlety: kv/metadata/myapp/* does not match kv/metadata/myapp, so listing the folder itself needs the exact path. Same trap for a secret stored at the folder root.
path "kv/data/myapp/*" { capabilities = ["create","read","update","delete"] }
path "kv/metadata/myapp" { capabilities = ["list"] }
path "kv/metadata/myapp/*" { capabilities = ["list","read"] }
Omit patch unless the app genuinely needs partial updates. For on-call, add update on kv/destroy/myapp/* and delete on kv/metadata/myapp/* in a separate policy - never merged into the app's.
2. Does the proposed `deny` stanza solve the `prod/stripe` isolation requirement? Explain what actually works.
Read the worked answer
Partially, and it solves it for the wrong audience. A token's effective capabilities are the union of its attached policies, and an explicit deny beats any grant within a policy that is attached to that token. So a deny stanza in the app's policy does stop that app - but every other token holding the wildcard grant (the platform team, the deploy pipeline, the next service copy-pasted from this one) still reads prod/stripe unless each of them also carries a deny.
You cannot express "everything under myapp/* except one path" with Vault globs. There is no negative wildcard, so security-by-exception must be pushed onto every consumer. That is unmaintainable and will rot the first time someone creates a new policy.
What works:
- Restructure so the sensitive value is not inside the wildcard. Move it to
kv/data/payments/stripe(or its own mount) and grant the wildcardkv/data/myapp/*plus a narrow grant forpayments. Exception-free policies survive staff changes. - Or drop the wildcard and grant explicit subtrees - viable when the tree is small and stable.
- Enterprise-only: a Sentinel EGP can enforce a central "deny unless caller is
payments" rule across all tokens, which is the only way to keep the exception in one place.
Prefer restructuring; it needs no paid feature and no per-consumer maintenance.
3. How would you prove the final policy is right before shipping it, including for a token you do not hold?
Read the worked answer
Never validate by reading the HCL - validate capabilities.
- From the workload's own token:
vault token capabilities kv/data/myapp/prod/stripe, repeated for each path in the table. This returns exactly what Vault will enforce, including the deny-taking-precedence rule. - For a token you don't hold:
vault write sys/capabilities-accessor accessor=<accessor> paths=kv/data/myapp/prod/stripe. Accessors cannot authenticate, so this is safe to run against production tokens and safe to log. - Negative tests matter more than positive ones: assert that
destroyandmetadata deletereturn 403 for the app, and thatlistreturns 403 for a token that should not enumerate. A policy that grants too much passes every happy-path test. - Include the folder root: test
kv/metadata/myappandkv/data/myappexplicitly, since the glob covers children only. - Run the same matrix in a non-production mount of the same
kvversion before the mount is upgraded - policy behaviour aroundpatch, soft delete and metadata changed across Vault versions, and a policy that was correct on KV v1 silently becomes wrong.