Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

By updated

Improve this page

HashiCorp Vault logo

A Vault policy is a file that says which paths a token can reach and with which capabilities (read, write, delete, list). A token is the credential Vault hands out after a successful login. Everything that no policy allows is refused: this is the default deny. This lesson teaches you to write a policy in HCL, to load it, to attach it to an identity, then to predict what Vault does with it when several rules target the same path. It finally defines dev-policy and webapp-policy, which the authentication lesson has already attached to alice and to the my-webapp role.

What you will learn

  • Write an HCL policy: path blocks, capabilities, * and + wildcards
  • Target the real KV v2 paths (data, metadata) rather than the ones typed on the keyboard
  • Predict which rule Vault retains when several match, and know when deny really acts
  • Load, attach and test a policy without ever guessing the result
  • Split policies by role and by environment, with the strict minimum
  • SOCLE anchoring: this lesson implements SOCLE-SEC-STO-3 (in French), access scoped to the strict minimum and separated by environment, and SOCLE-SEC-STO-6 (in French), access denied by default

Prerequisites

This lesson belongs to the Vault module (in French). You need a Vault server installed and started, an administrator access (root token or an administration policy) to write policies, and two takeaways from the previous lessons: knowing how to obtain a token through userpass or AppRole, and knowing the KV v2 engine and its paths. Without them, you would write rules that nothing triggers.

What does a policy look like?

A policy is written in HCL (HashiCorp Configuration Language) or in JSON: a series of path blocks, each carrying a list of capabilities. The path is not a file system path, it is the URL of Vault's HTTP API without its /v1/ prefix. Allowing a path therefore amounts to allowing a precise API call. Here is dev-policy, the policy of developer alice: full rights on dev, read-only on the shared secrets, and nothing else.

# dev-policy.hcl: developer
# Full access to the dev environment
path "secret/data/dev/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Dev metadata: read, without deleting the history
path "secret/metadata/dev/*" {
capabilities = ["list", "read"]
}
# Read-only on the shared secrets
path "secret/data/shared/*" {
capabilities = ["read", "list"]
}
# Neither staging nor prod: nothing is written, so everything is denied

The breakdown fits in four lines. path "secret/data/dev/*" designates everything that starts with this prefix, the * being a wildcard (also called a glob). capabilities lists the actions allowed on this path. The blocks are independent: none assumes another. And the default deny applies to everything else: prod appears nowhere, so prod is forbidden, without any deny rule being needed.

Which capabilities exist?

A capability is an action allowed on a path. Six map onto an HTTP verb. sudo unlocks the so-called protected paths (sys/auth, sys/seal, some PKI operations), which require a root token or the sudo capability in the policy. deny refuses a path, but only within the retained rule: the section on evaluation gives its exact scope, which is not the one you imagine. Do not confuse the two ways of not allowing something: leaving a capability out simply does not grant it (the default deny does the rest), while deny is an explicit refusal that overrides every other capability of the same rule.

CapabilityWhat it allowsHTTP verbs
createCreate a new entry (add an API key)POST/PUT
readRead an entry (retrieve a database password)GET
updateModify an existing entry (change the password)POST/PUT
deleteDelete an entry (remove a revoked key)DELETE
listList the names, without reading the contentsLIST
patchPartial modification, if the endpoint supports itPATCH
sudoAccess protected paths (root-protected)-
denyRefuse the path within the retained rule-

Which paths should you target with KV v2?

With the KV v2 engine, the path you type is not the one the policy must allow. vault kv get secret/myapp actually queries secret/data/myapp: the versioning inserts a technical segment (data, metadata, destroy) between the mount (the path where the engine is mounted, here secret/) and the secret name. Forgetting this segment produces unexplained permission denied errors on KV v2, and it is the first thing to check.

PathUsage
secret/data/*Read/write the secrets
secret/metadata/*Metadata, versions, key listing
secret/delete/*Soft deletion of versions
secret/undelete/*Restoring versions
secret/destroy/*Permanent deletion

What do * and + mean in a path?

Two characters let you write a generic pattern, and they do not have the same scope. The glob * swallows the whole rest of the path and is written only at the end of a pattern: secret/*/config is not valid, * is not a regular expression. The + consumes a single segment only and can be placed in the middle: it is the tool of horizontal partitioning, which opens one level of the tree without opening what lies below.

PatternMatchesDoes not match
secret/data/dev/*secret/data/dev/a, secret/data/dev/a/b/csecret/data/dev
secret/data/apps/+secret/data/apps/a, secret/data/apps/bsecret/data/apps/a/b
secret/data/+/configsecret/data/prod/configsecret/data/a/b/config

Two details of this table regularly trip people up. secret/data/dev/* does not cover secret/data/dev itself: the exact path of the folder is declared separately if you want to reach it. And secret/data/+/config works although the wildcard is not at the end of the path, because + is not a suffix glob. Confusing the two produces either a permission that is too broad, or a rule that never matches.

How does Vault choose the rule that applies?

When several rules can match the same path, Vault does not add them all up. Two cases are distinguished, according to the "Priority matching" section of the policies documentation. If several policies declare exactly the same pattern, Vault takes the union of their capabilities. If the patterns differ, Vault retains only the rule of highest priority: the rights of the other patterns are not combined with the retained rule.

How is the priority of a pattern computed?

Vault ranks the patterns that match the requested path and retains only the first one. Five criteria apply in a cascade, from the most discriminating to the least. The guiding idea: the more precise a pattern is, the more it wins, and an early wildcard strongly lowers its rank.

  1. The first wildcard (+ or *) appears earlier: lower priority
  2. The pattern ends with *: lower priority
  3. More + segments: lower priority
  4. A shorter pattern: lower priority
  5. As a last resort: lexicographic order

The following diagram follows the path of a request. First notice that the step "retain the most specific pattern" comes before the search for a deny, then that this deny is looked for only among the capabilities of that pattern. The "most specific" of the diagram summarises the cascade of five criteria above.

Evaluation flowchart: a request with no matching pattern is denied by default. Otherwise Vault retains the most specific pattern, gathers its capabilities across all the token's policies, denies if deny is one of them, and allows only if the requested capability is present.noyesyesnonoyesToken requeston a pathDoes a pattern inone of the token'spolicies match?Denieddeny by defaultKeep the mostspecific patternMerge the capabilities of thatpattern across all policiesdeny amongthem?Deniedpermission deniedIs the requestedcapability present?Allowed
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

Does a more specific deny win over a broad rule?

Yes, and it is the classic setup: a broad rule opens reading everywhere, a deny exception targets production. It works because the exception's pattern is more specific than the general pattern. The deny does not win because it is written second, but because its wildcard comes further in the path, so its pattern ranks better.

# General rule: read on every secret/*
path "secret/*" {
capabilities = ["read"]
}
# Specific rule: deny on prod
path "secret/data/prod/*" {
capabilities = ["deny"]
}

For the path secret/data/prod/app, both patterns match, but secret/data/prod/* has its first wildcard further along: it is retained, its deny applies, access is denied. For secret/data/dev/app, only secret/* matches: read is enough.

Why does a broad deny not block a more precise access?

Because deny acts inside the retained rule, never above the others. A deny on secret/* does not override a read granted on secret/data/app/*: this second pattern is more specific, it is the one Vault retains, and the broad pattern's deny is not even consulted. This result was measured on 2026-10-19 on a disposable Vault 2.2.0 development server (vault server -dev): with these two patterns on the same token, vault token capabilities returns read and the read succeeds.

The documentation says two things that fit together. On one hand, deny always takes precedence regardless of any other defined capabilities; on the other, Vault takes only the highest-priority match. The deny therefore wins within the retained rule, not over the other rules that touch the path. The following example uses an exact path, with no wildcard, which the ranking places higher still.

# Policy A: a broad deny, meant to block everything under secret/
path "secret/*" {
capabilities = ["deny"]
}
# Policy B: a precise access
path "secret/data/app/x" {
capabilities = ["read"]
}

With these two policies on the same token, reading secret/data/app/x is allowed, and it is counter-intuitive. The widespread idea of a "deny that always wins" is only true inside the retained rule; beyond it, it is a trap. For a deny to really block a path, it must appear on the retained pattern for that path, that is to say one as specific as the authorisations it targets.

What happens when the same pattern appears in two policies?

The capabilities add up, and a deny present in one of them then becomes part of the union: it wins, even over sudo, because the documentation states that deny takes precedence over any other capability defined for the retained rule. This is the easiest case to predict: the patterns being identical, no ranking comes into play, and one policy's deny always neutralises another policy's read. A deny on a more specific pattern, seen above, has the same effect by another route.

# Policy A
path "secret/data/shared/*" {
capabilities = ["read"]
}
# Policy B
path "secret/data/shared/*" {
capabilities = ["list"]
}

A token that carries A and B gets read and list on this pattern. If a third policy declared ["deny"] for the same pattern, the token would have no access left on this path.

Why does a mixed-case role name make a deny stop protecting?

Since Vault 2.2.0, a deny written with capital letters in the name of an authentication method role no longer refuses anything: Vault lowercases the role name of the request before comparing it, so the mixed-case rule matches no path anymore. It does not even take part in the arbitration, and the broader allow rules apply: the refusal becomes an authorization. The official changelog speaks of rules that "will fail open (become allow-all) after upgrade until rewritten with lowercase names".

The case was measured on 2026-10-19 on Vault 2.2.0 and 2.0.3, with an AppRole role named MyRole and two policies granting read and list on auth/approle/role/*. They differ by a single deny, on the role-id. Each row of the table is a real HTTP request on that path.

Versiondeny written asRequest on MyRoleRequest on myrole
2.2.0MyRole200, granted200
2.2.0myrole403403
2.0.3MyRole403200
2.0.3myrole200403

Read the first two rows: on 2.2.0, only the lowercase rule refuses, and it refuses both spellings. The only remedy is to write role names and every rule that targets them in lowercase, then recheck each existing deny after an upgrade to 2.2.0. The measured scope is limited to role names of authentication methods, and only AppRole was tested. A deny on a KV v2 path in mixed case (secret/data/Prod/Secret) stays effective on 2.2.0. The changelog announces the same behavior for AWS, Azure, GCP and Kubernetes, which this lesson has not verified.

What do the built-in policies contain?

Vault creates two policies at initialisation, before any rule of yours. They frame the two extremes of the model: unlimited access on one side, a minimal base on the other. Knowing them avoids two frequent mistakes: administering durably with the root token, and believing that an authenticated token has no permission while default already gives it some.

The root policy gives total access and cannot be modified. Reserve the root token for initialisation and emergencies: create administrators with dedicated policies, then revoke the root token. The default policy is attached to most tokens and provides a minimal base: look up its own token, use the cubbyhole (a private storage space that belongs to each token), and renew. Its exact content can be adapted on your instance, it is not an immutable constant.

How do you create and load a policy?

A policy exists in Vault only once it has been loaded under its name: writing an .hcl file on disk has no effect until vault policy write has been run. The choice between file and standard input is not cosmetic: the file is versioned in Git and replayed in CI, which makes every grant of rights explicit and traceable.

File method

This is the method to favour as soon as a policy exceeds three rules. The HCL file becomes the reference artefact reviewed in code review, and vault policy write simply pushes it. The command completely overwrites the policy with the same name: there is no merge, the supplied file is authoritative.

  1. Create the HCL file of dev-policy, identical to the one in the previous section.

    Terminal window
    cat > dev-policy.hcl << 'EOF'
    # dev-policy.hcl: developer
    # Full access to the dev environment
    path "secret/data/dev/*" {
    capabilities = ["create", "read", "update", "delete", "list"]
    }
    # Dev metadata: read, without deleting the history
    path "secret/metadata/dev/*" {
    capabilities = ["list", "read"]
    }
    # Read-only on the shared secrets
    path "secret/data/shared/*" {
    capabilities = ["read", "list"]
    }
    EOF
  2. Load the policy into Vault

    Terminal window
    vault policy write dev-policy dev-policy.hcl

    Output:

    Success! Uploaded policy: dev-policy
  3. Check

    Terminal window
    vault policy read dev-policy

Same gesture for webapp-policy, the policy of the my-webapp application: read-only on its own secrets, the ones the KV v2 lesson stores under apps/webapp/. The file has a single block, because an application should do nothing more than read its configuration.

Terminal window
cat > webapp-policy.hcl << 'EOF'
# webapp-policy.hcl: my-webapp application
path "secret/data/apps/webapp/*" {
capabilities = ["read"]
}
EOF
vault policy write webapp-policy webapp-policy.hcl

Inline method

The dash - in place of the file name tells Vault to read the policy on the standard input. Handy for a quick test or a bootstrap script, but nothing is kept on disk: the only trace remains in Vault, and you lose the revision history. Reserve this form for short, disposable policies, like staging-readonly below, already cited by the authentication lesson.

Terminal window
vault policy write staging-readonly - << 'EOF'
path "secret/data/staging/*" {
capabilities = ["read", "list"]
}
EOF

A policy name is free, with one limit introduced by Vault 2.2.0: a name containing a . or .. segment is refused at assignment. vault token create -policy=. returns 400 invalid policy name "." in policies: policy name cannot contain '.' path segments, and -policy=.. returns invalid policy name ".." in policies: policy name cannot contain '..' path segments: the segment quoted at the end of the message follows the tested name. A name with a single dot, such as my.policy, is still accepted at write and at assignment (Vault only warns Policy "my.policy" does not exist if the policy has not been written, which is not a refusal). Before 2.2.0 (measured on 2.0.3), all these assignments went through.

How do you list, read and delete a policy?

Policies live in the sys/policies/acl backend and are handled as named objects. Beware of deletion: vault policy delete removes the policy from the store, but the tokens already issued that reference it keep existing. They simply lose the matching permissions, which shows up as sudden permission denied errors on running workloads.

Terminal window
# List all policies
vault policy list
# Read a policy
vault policy read dev-policy
# Delete a policy
vault policy delete dev-policy

How do you attach a policy to an identity?

A policy that is loaded but attached to nobody protects nothing. The attachment is made at the level of the authentication method: at login time, it builds a token carrying the configured list of policies. It is this list of names that is fixed at issuance: modifying a user's policies does not change the tokens they already hold, they must log in again.

To a userpass user

The policies field of the userpass entry is replaced in full on every write: always give the complete list, otherwise you unintentionally remove the policies missing from the command. The tokens already handed to alice keep the old list until they expire.

Terminal window
vault write auth/userpass/users/alice policies="dev-policy,staging-readonly"

To an AppRole role

For a pipeline or an application, the policy is attached to the AppRole role, not to a person. The duration parameters (TTL, time to live) matter as much as the policy: a secret_id_ttl of 10 minutes limits the window during which an intercepted secret_id can still be exchanged for a token, and a token_ttl of 20 minutes bounds how long that token can be used.

Terminal window
vault write auth/approle/role/my-webapp \
policies="webapp-policy" \
secret_id_ttl=10m \
token_ttl=20m

To a token

Creating a token directly bypasses any authentication method: it is an administrator operation, useful to test a policy or to troubleshoot. A child token can never receive more permissions than its parent, which prevents an escalation from an already restricted token.

Terminal window
vault token create -policy="dev-policy" -policy="staging-readonly"

How do you test a policy?

A policy is hard to proofread: priority matching and KV v2's implicit paths make the result difficult to predict by eye. The three methods below check Vault's real behaviour rather than your intention. Always test both sides: what must succeed, and above all what must fail.

Method 1: create a token and test

This is the most faithful test: you put yourself exactly in the shoes of the policy holder. Export the token in a dedicated shell so as not to overwrite your administrator session, then deliberately provoke a refusal. A policy that has never returned a 403 in your trials has not been tested.

Terminal window
# Create a token with the policy
vault token create -policy="dev-policy" -field=token

Then test with this token:

Terminal window
export VAULT_TOKEN="<token>"
# Must succeed (dev/*)
vault kv put secret/dev/myapp password="test"
# Must fail (no permission)
vault kv put secret/prod/myapp password="test"

Expected output for the failure:

Error writing data to secret/data/prod/myapp: Error making API request.
Code: 403. Errors:
* 1 error occurred:
* permission denied

Method 2: vault token capabilities

This command queries Vault's own evaluation engine and returns the effective capabilities after priority matching. It answers the real question, "what can this token do on this path", without writing anything or consuming a secret. It is the diagnostic tool to use first in front of a permission denied. It has a known limit: on mixed-case role names of an authentication method (Vault 2.2.0), it can display deny for an access that is really granted. See the section on the deny of a mixed-case role name, and then confirm with a real request.

Terminal window
# With the current token
vault token capabilities secret/data/dev/myapp
# With a specific token
vault token capabilities <token> secret/data/dev/myapp

Output:

create, delete, list, read, update

Method 3: -output-policy

Rather than guessing the exact path Vault expects, let the CLI write it. With -output-policy, the command is not executed: it prints the minimal HCL block that would have to be granted for it to succeed, KV v2's data segment included. It is the most reliable way to build a policy at the strict minimum, rule after rule.

Terminal window
vault kv get -output-policy secret/apps/webapp

Output:

path "secret/data/apps/webapp" {
capabilities = ["read"]
}

How do you write a policy that adapts to the identity?

Vault can use the requester's identity in the path thanks to templated policies. A single rule then serves all users: at request time, Vault replaces the variable with a value specific to the token holder. Each person thus gets their own space without writing one policy per person.

# Each user accesses their own space
path "secret/data/users/{{identity.entity.name}}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

User alice will be able to access secret/data/users/alice/* but not secret/data/users/bob/*.

Which variables are available?

These variables are resolved at request time, from the entity linked to the token. An identity entity is the record Vault keeps for one person or one machine, and it groups the different aliases of that same person across several authentication methods: alice in userpass and alice in OIDC can point to the same entity, hence to the same space of secrets. The group variables give the same mechanism at team scale.

VariableDescription
{{identity.entity.id}}Unique ID of the entity
{{identity.entity.name}}Name of the entity
{{identity.entity.aliases.<mount>.id}}Alias ID for an authentication method
{{identity.groups.ids.<group_id>.name}}Name of a group by ID
{{identity.groups.names.<group_name>.id}}ID of a group by name

What trap awaits a substituted value?

The pattern above is correct. What is refused is a + or * wildcard present in the value rendered by the template. The documentation is explicit: using wildcards in the rendered output of an identity template produces a permission denied response. If an entity name, an alias or a substituted metadata contains * or +, the rule is therefore refused. The official example is a group metadata valued amer/+/us-west.

How do you restrict the parameters of a request?

A policy can also restrict which parameters a client sends to an endpoint, in addition to the capability. Three keywords complement each other: allowed_parameters (allow list), denied_parameters (deny list) and required_parameters (mandatory presence). It is a finer control than the path, useful when the same endpoint serves usages of different sensitivity.

allowed_parameters

This block turns the path into an allow list: any parameter absent from the list makes the request fail, even if the create capability is granted. The following example lets you write username and password in a KV v2 secret and refuses any other field. The empty array [] means "any value" for that field; putting values in it also restricts the accepted content.

path "secret/data/limited/*" {
capabilities = ["create", "update"]
# Only these fields can be written
allowed_parameters = {
"data" = {
"username" = []
"password" = []
}
}
}

denied_parameters

This is the opposite logic, a deny list: everything goes through except the named fields. This mode is more permissive and therefore more fragile, since a sensitive field you did not anticipate remains allowed. It is useful when a single field must be locked in an otherwise free structure, such as an administration token that applications must never rewrite.

path "secret/data/apps/*" {
capabilities = ["create", "update"]
# Everything is allowed except these fields
denied_parameters = {
"data" = {
"admin_token" = []
}
}
}

required_parameters

required_parameters imposes the presence of a field in the request, without saying anything about its value. On a PKI endpoint, requiring common_name prevents issuing a certificate without an explicit identity, a case where the role would silently apply its default value. This safeguard forces the caller to declare its intent rather than inherit a configuration.

path "pki/issue/my-role" {
capabilities = ["create", "update"]
# The common_name is mandatory
required_parameters = ["common_name"]
}

What do the policies of a real project look like?

The policies below complete dev-policy and webapp-policy with the profiles of a common installation. They illustrate a principle: least privilege does not consist in reducing the number of paths, but in separating the verbs. A pipeline that encrypts has no reason to be able to decrypt, and a developer who writes in dev has nothing to do in prod.

How does the developer policy isolate prod?

dev-policy, written above, rests entirely on the default deny: prod and staging do not appear in it, so they are refused without any explicit deny rule. staging is opened read-only by another policy, staging-readonly, which alice carries in addition. The asymmetry is intended: full rights on dev, read on staging, nothing on prod, and no deletion of the version history.

How is a build pipeline's policy limited?

A build pipeline generally only needs to encrypt. The update capability on transit/encrypt/ci-key allows the call to the encryption endpoint, without ever giving access to the key itself, which does not leave Vault. This relies on the Transit engine. A runner compromised during the build phase can therefore produce ciphertext, but cannot read back the artefacts already encrypted.

# ci-build-policy.hcl - Build pipeline
# Encryption only (no decryption)
path "transit/encrypt/ci-key" {
capabilities = ["update"]
}
# Read the non-sensitive configs
path "secret/data/build/config" {
capabilities = ["read"]
}

How is a deployment pipeline's policy limited?

The deployment pipeline carries the exact mirror of the previous one: transit/decrypt but not transit/encrypt. It reads the deploy secrets and requests its certificates through pki/issue/deploy-role. This separation of verbs has a concrete effect on the threat model: an attacker who takes over the deployment job cannot reinject encrypted data that would then be accepted as legitimate upstream.

# ci-deploy-policy.hcl - Deployment pipeline
# Read the deployment secrets
path "secret/data/deploy/*" {
capabilities = ["read"]
}
# Decryption (no encryption)
path "transit/decrypt/ci-key" {
capabilities = ["update"]
}
# Access to PKI certificates
path "pki/issue/deploy-role" {
capabilities = ["create", "update"]
}

How do you write an administration policy without the root token?

This policy replaces the root token for day-to-day operation. It gives control over policies, authentication methods and secrets engines. The sudo capability on sys/auth/* is indispensable: these endpoints are protected and would remain refused without it. The last rule, which opens all the secrets, is a choice to make consciously: remove it if operating Vault must not involve reading the secrets.

# admin-policy.hcl - Admin without being root
# Policy management
path "sys/policies/acl/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Auth method management (enable/disable)
path "sys/auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# Auth method configuration
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Secrets engine management
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Access to all secrets
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

This policy grants neither sys/seal nor sys/rekey, and the default deny therefore keeps them closed. A received idea says these paths "require the unseal keys": this is not the case. The documentation lists sys/seal among the protected paths, which require a root token or the sudo capability in the policy; the unseal keys are used for sys/unseal. Moreover, since Vault 2.0.0, sys/rekey and sys/generate-root are authenticated by default, the old behaviour being re-enabled through the enable_unauthenticated_access configuration key.

How do you organise policies by team and by environment?

The number of policies climbs fast, and a giant policy per person becomes impossible to review. The solution consists in splitting along orthogonal axes, then attaching several policies to the same token. The HCL files are versioned in Git and deployed by CI. But the composition has a limit, which follows from the evaluation rule: it does not make an "intersection" of the rights, it makes a union on identical patterns and lets a single pattern win over different patterns.

By team

Splitting by team attaches the policy to an organisational responsibility: the backend team manages its application secrets, the security team the Transit and PKI engines. The advantage is the review: each file has an identified owner. The limit appears quickly: nothing in this split distinguishes production from development.

policies/
├── team-backend.hcl
├── team-frontend.hcl
├── team-devops.hcl
└── team-security.hcl

By environment

Splitting by environment carries the axis that matters most in security: it is what materialises the prod boundary. A single env-prod policy to audit, a single one to restrict, and the question "who has access to production" is answered by listing who carries it. This split alone does not, however, partition the teams from each other inside the same environment.

policies/
├── env-dev.hcl
├── env-staging.hcl
└── env-prod.hcl

How do you combine the two axes without surprises?

Crossing the two axes gives the intended result: alice carries her team policy and the policies of the environments she is cleared for. On the same pattern, the capabilities add up. On different patterns that cover the same path, only the retained pattern applies: if team-backend gives read on secret/data/apps/backend/* and env-dev gives update on secret/data/apps/*, a token carrying both does not accumulate these rights on apps/backend/x, since the first pattern is the more specific (measured on 2026-10-19 on a disposable Vault 2.2.0 development server: read on apps/backend/x, update only elsewhere under apps/).

Terminal window
vault write auth/userpass/users/alice \
policies="team-backend,env-dev,env-staging"

To avoid these silent overrides, write the policies of a single token on paths that do not overlap, or repeat the exact same pattern when you want to accumulate. Never adding env-prod to a developer profile is enough to hold the boundary between environments.

Which good practices should you adopt?

Four rules prevent the drifts of policies that live for a long time. They take the default deny as a starting point: you add only what is necessary, and each addition is justified by a named usage.

  • One policy per business role, not per person: policy-dev-team, policy-ci-runner, policy-prod-readonly. Otherwise their number explodes and nobody knows who has access to what.
  • Least privilege: grant only the necessary capabilities and paths, and separate the verbs (build encrypts, deploy decrypts).
  • Caution with wildcards in production: prefer secret/data/prod/db-* to secret/data/prod/*, because a * that is too broad exposes a critical secret that nobody planned to cover.
  • Test before deploying: vault token capabilities on the expected and forbidden paths. vault policy fmt does not validate, it reformats.

Troubleshooting

Vault almost always returns the same message, permission denied, whatever the real cause. This terseness is deliberate: detailing the reason for the refusal would inform an attacker about the tree of secrets. The table below does the translation, and the last row explains the policy fixes that have "no effect".

SymptomProbable causeSolution
permission deniedMissing policy or uncovered patternvault token capabilities <path>
Syntax error on loadingMalformed HCL (braces, quotes, brackets)Re-read the file; vault policy fmt reformats without validating
Unexpected capabilitiesPriority matching: another pattern is retainedCompare the wildcards of each pattern
A deny on an auth role no longer blocks anything after 2.2.0Mixed-case role name in the ruleRewrite in lowercase, verify with curl, not with vault token capabilities
A broad deny blocks nothingA more specific pattern grants the pathPut the deny on the retained pattern
Wildcard does not work* not at the end of the pattern, or + confused with *Review the syntax
KV v2 access deniedPath without /data/Add /data/ to the path
Template refusedA * or + in the substituted valueCheck names, aliases and metadata
User policies modified with no effectToken issued before the modificationLog in again to get a new token

How do you see the policies of a token?

vault token lookup displays the list of policies engraved in the token at its issuance, as well as its remaining TTL (time to live). Compare it with the user's current configuration: a difference confirms that the token predates your modification and that you must log in again rather than touch the policy.

Terminal window
# Current token
vault token lookup
# Specific token
vault token lookup <token>

Key points

The nine points below condense the essentials of the lesson. The first four concern the evaluation engine and explain most unexpected permissions; the following ones come under syntax and design.

  1. Default deny: without an explicit policy, everything is refused
  2. Same pattern: the capabilities of all policies add up
  3. Different patterns: a single rule wins, rights do not combine
  4. deny acts within the retained rule: a broad deny does not override a more precise access
  5. KV v2: secret/data/*, secret/metadata/*, never the typed path
  6. Wildcards: * at the end of a pattern only, + for one segment; no * or + in the value of a template
  7. Role-name case: since Vault 2.2.0, a deny on an auth role name written with capitals refuses nothing; write it in lowercase and verify with a real request, not with vault token capabilities
  8. Least privilege: one policy per role, build and deploy separated, dev and prod separated
  9. Expected evidence for SOCLE-SEC-STO-3 and SOCLE-SEC-STO-6: versioned policies that scope each access to the strict minimum, separate the environments and leave everything else denied by default

Knowledge check

Check that the essentials of this lesson are in place. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

5 questions
5 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn