ACL policies that scale

Path capabilities, identity, and least privilege.

In short. Authentication answers who you are. ACL policies answer what you may touch. A Vault cluster with open policies is a fancy database of secrets with a login screen.

Intermediate30 min · lesson 5 of 13

Authentication answers who you are. ACL policies answer what you may touch. A Vault cluster with open policies is a fancy database of secrets with a login screen. Production policies are small, path-scoped capability lists assigned through roles and identity groups — not a single admin policy stapled onto every human token.

Vault policies are authored in HCL (or JSON) and written to sys/policy. Each path lists capabilities such as read, create, update, delete, list, and sudo. Deny is the default: if no policy grants a capability on a path, the request fails. That default is the whole security model.

Write least privilege on real paths

KV v2 data lives under secret/data/... while metadata lives under secret/metadata/.... Granting only secret/data/app/* without list on metadata is a common surprise. Database credential minting is database/creds/role-name. PKI issue paths are pki_int/issue/role-name. Read the engine docs for the exact path shape before you invent wildcards.

terminal
vault policy write app-webapp -<<'EOF'
path "secret/data/webapp/*" {
capabilities = ["read"]
}
path "database/creds/webapp-rw" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
EOF
vault policy read app-webapp
output
path "secret/data/webapp/*" {
capabilities = ["read"]
}
...

Attach policies via auth roles (token_policies) and via identity groups so IdP group membership flows into Vault without editing every user. Prefer group-attached policies for humans and tight role policies for machines. Avoid path "*" except on break-glass policies that should almost never be assigned.

Identity entities and tidy ownership

When a person logs in through OIDC and a workload through Kubernetes, Vault can map both to identity entities and groups. Templated policies (covered deeper in the advanced course) can scope paths per entity. Even without templates, naming policies after applications (app-payments, app-webapp) beats naming them after people (alice-admin).

Request authorization
1Token presented
from OIDC, K8s, AppRole…
2Policies loaded
union of token + identity policies
3Path matched
longest/most specific rules apply
4Allow or deny
default deny if nothing grants
Explicit deny capability in a policy overrides grants — useful for guardrails inside broad roles.

Operational hygiene

Store policies in Git and apply them with CI using a limited GitOps token. Review diffs like code. Alert on policy writes in the audit log. Periodically list tokens and accessors; revoke orphans. A policy that grants sudo on sys/* is root in slow motion — treat it that way.

Wildcards hide escalations
path "secret/data/*" { capabilities = ["read","create","update","delete"] } looks convenient and often becomes the production policy forever. Scope to application prefixes.

Try this

Create a narrow policy, assign it to a token, prove a permitted read works and a neighboring path fails.

terminal
vault token create -policy=app-webapp -ttl=15m
export VAULT_TOKEN=<token>
vault kv get secret/webapp/config
vault kv get secret/other/config
output
====== Data ======
...allowed...
Error making API request...
* permission denied
# SUCCESS — default deny on the neighbor path

Takeaway

Policies are the real authorization layer: default deny, path-accurate capabilities, group-driven assignment. Auth without policy is an unlocked door with a visitor log.

Next: store the static secrets you still need in KV v2 with versioning and check-and-set.

Quick check
01If no policy grants a capability on a path, Vault…
Incorrect — There is no implicit read.
Correct — default deny is the model.
Incorrect — MFA is separate from ACL grants.
Incorrect — Root is a token type, not a fallback.
02KV v2 secret data paths typically look like…
Incorrect — That shape is v1-style; v2 data is under secret/data/.
Correct — data vs metadata are different prefixes.
Incorrect — Mount name varies; v2 still uses /data/.
Incorrect — sys is for system backends.

Related