One connection is one HashiCorp Vault (one namespace). toolpass authenticates with a token or an AppRole, finds the identity entity whose alias on the configured auth mount is the user's email, collects the ACL policies attached to the entity, its groups (direct and inherited) and the auth role (declared in the connection), reads each policy and evaluates the requested path and capability with Vault's own rules. Nothing is written.
Either of:
- A token (
auth_mode: token, the default), for instance a periodic service token. - An AppRole (
auth_mode: approle):role_idand thesecret_idascredential; toolpass logs in atauth/<approle_mount>/loginand logs in again when the token expires or is revoked (at most once per token, so a permission denied on a path toolpass needs does not spendsecret_iduses on every check).
Attach a policy with exactly these capabilities:
path "identity/lookup/entity" { capabilities = ["update"] }
path "identity/entity/id/*" { capabilities = ["read"] }
path "identity/group/id/*" { capabilities = ["read"] }
path "sys/policies/acl/*" { capabilities = ["read"] }
path "sys/auth" { capabilities = ["read"] }
path "sys/mounts" { capabilities = ["read"] }
path "auth/token/lookup-self" { capabilities = ["read"] }sys/auth resolves the alias mount's accessor, sys/mounts the KV version of a mount. Nothing
under the secrets engines themselves is read.
toolpass probe looks the token up and resolves the alias mount's accessor.
- id: vault-prod
integration: vault
url: https://vault.example.com:8200
alias_mount: oidc/
credential: env:VAULT_TOKEN
# auth_mode: approle
# role_id: 7f1c...
# approle_mount: approle
# token_policies: developers, oncall # policies the oidc role attaches at login
# namespace: admin/ # Vault Enterprise, sent as X-Vault-Namespace| Key | Meaning |
|---|---|
url |
the Vault address |
alias_mount |
the auth mount whose aliases carry the users' emails (oidc/, ldap/, okta/) |
credential |
the token or the AppRole secret_id, env: or file: |
auth_mode, role_id, approle_mount |
AppRole login |
token_policies |
policies every login through the alias mount receives (see below) |
namespace |
the Enterprise namespace |
POST identity/lookup/entity with alias_name = the email and alias_mount_accessor = the
alias mount's accessor; no entity is user_not_found. Then GET identity/entity/id/<id> for the
entity's policies, disabled flag, metadata, aliases and group ids, and GET identity/group/id/<id>
for each direct and inherited group's name and policies. A disabled entity is denied every action;
an entity read that does not say whether it is disabled is unknown (unsupported).
The identity's groups are the group ids; its attributes carry the entity name, metadata, aliases and
group names so that policy templates can be resolved. Groups sent by the caller are ignored.
Vault attaches policies to a token from three places. toolpass sees two of them and takes the third from configuration:
| Source | Read by toolpass |
|---|---|
the entity's policies |
yes |
| the policies of the entity's groups, direct and inherited (external groups included) | yes |
the auth method role's token_policies (the OIDC role, the LDAP group mapping) |
no: declare them in token_policies |
default |
assumed attached (UNVERIFIED for auth methods that exclude it) |
A policy named root on an entity or group answers unsupported: Vault refuses root alongside
other policies and never issues it through auth methods, so its presence is a misconfiguration
toolpass does not turn into allow (root is rejected in token_policies). A policy a token names
but Vault does not have contributes nothing, as in Vault.
| Resource | Meaning |
|---|---|
kv:<mount>/<key> |
a secret in a KV engine; the mount is the longest sys/mounts prefix (mounts may span several segments), its KV version decides the API path (<mount>/data/<key>, metadata, destroy on v2; the logical path on v1) |
path:<api path> |
any API path, checked as written (sys/seal, pki/issue/web, secret/data/x) |
Paths are plain segments (letters, digits, _ . - @ : ~ =); wildcards and dot-only segments are
rejected. LIST questions are evaluated both with the trailing slash Vault adds and without it, as
Vault does; an explicit deny on either form wins.
| Action | Capability | KV v2 path |
|---|---|---|
secret.read |
read |
<mount>/data/<key> |
secret.write |
create and update (see below) |
<mount>/data/<key> |
secret.delete |
delete |
<mount>/data/<key> |
secret.list |
list (prefix) |
<mount>/metadata/<key>/ |
secret.metadata |
read |
<mount>/metadata/<key> (v2 only) |
secret.destroy |
update |
<mount>/destroy/<key> (v2 only) |
raw:<capability> |
read, create, update, patch, delete, list, sudo, subscribe, recover on a path: |
A write is create for a new secret and update for an existing one. When the policies grant
both the answer is allowed, neither denied, one of the two unsupported (the write succeeds
only if the secret does or does not exist yet).
Vault's documented rules, applied to the union of the policies' stanzas:
- Stanzas whose path matches the request path are candidates: an exact path, a segment that is
exactly
+for any one segment, a trailing*for any suffix.*elsewhere and+inside a segment are literal; one leading/is dropped, as Vault does. - The highest-priority pattern wins: the one whose first wildcard comes latest, then one without a
trailing glob, then fewer
+, then longer, then lexicographically greater. The same pattern in several policies takes the union of its capabilities. denyin the winning stanza denies. Otherwise the needed capability must be present.{{identity.entity.id}},.name,.metadata.<k>,.aliases.<accessor>.id|name|metadata.<k>,{{identity.groups.ids.<id>.name}}and{{identity.groups.names.<name>.id}}are resolved for the user. From a template that cannot be resolved (another selector, an empty value, a value with a slash or wildcard) the stanza matches everything under its literal prefix, and if such a stanza matches the request at all the answer isunsupported.allowed_parameters,denied_parameters,required_parameters,min_wrapping_ttlandmax_wrapping_ttlon the winning stanza answerunsupported: toolpass does not see the request's parameters (reads carry them too, such as KV'sversion).
Policies in HCL (including the deprecated policy = "read|write|sudo|deny" attribute,
path = { ... } maps and nested control_group blocks, which are skipped) and in JSON (object and
list forms) are parsed. Heredocs and other syntax answer unsupported.
| Code | When |
|---|---|
allowed |
the winning stanza grants the capability |
denied |
no stanza matches; the winning stanza denies or lacks the capability; the entity is disabled |
unsupported |
a parameter or wrapping constraint; an unresolvable template; a policy toolpass cannot parse; the root policy; a KV v2 question on a v1 mount; a non-KV mount asked with kv:; a mixed create/update write |
resource_not_visible |
kv: names no mount sys/mounts lists, or a mount without a key |
user_not_found |
no entity has the alias |
credential_rejected |
403 permission denied on a read toolpass needs; the AppRole login fails |
invalid_request |
a malformed path or resource; alias_mount is not an enabled auth method |
upstream_* |
5xx, 429, 412, timeouts, a sealed Vault |
- Token policies from auth roles, unless declared in
token_policies. - Sentinel (EGP and RGP) policies, control groups, MFA enforcement.
- Request parameters:
allowed_parametersand friends. - Namespaces above the connection's: policies granted in a parent namespace on child paths.
Under a
namespace, a policy of the entity or its groups that answers 404 may be such a policy, so the answer is unknown (unsupported) rather than a check without it; in the root namespace a policy Vault does not have is skipped, as Vault skips it. - Token-specific state: TTLs, uses, bound CIDRs,
sudopaths'x-vault-sudorequirement is not checked unless asked withraw:sudo. - Path aliases resolved by engines (e.g.
secret/fooon KV v2 rewritten by the CLI): toolpass checks the API path, so usekv:for KV engines.
Written from Vault's documentation source (policies, identity, system API) and the OpenAPI document
in vault-client-go; not run against a live Vault. Marked UNVERIFIED in the code where it
matters:
- The
defaultpolicy is attached to every token of the alias mount. - The capabilities the deprecated
policy = "read|write|sudo"attribute maps to. identity/lookup/entityanswers 204 for no match (an emptydatais also taken as none).sys/policies/acl/<name>wrapspolicyindata(the top-level form is also read).
python -m pytest tests/integrations/vault (in toolpass-py) runs a fake Vault validated against the OpenAPI document
when TOOLPASS_SPECS_DIR holds vault.spec (test/specs/fetch.sh). The fake has an OIDC mount,
entities with direct and inherited groups, a disabled entity, a root entity, HCL and JSON
policies with globs, + segments, templates, a legacy policy attribute, parameter and wrapping
constraints, KV v1 and v2 mounts and a PKI mount, and an AppRole login. Property tests
(test_fuzz_parse_target, test_fuzz_policy) check the path validation and that the policy parser
and evaluator never fail with anything but a policy error. tests/real/test_vault_real.py starts
vault server -dev in Docker (hashicorp/vault:1.17, when the image is present locally), sets up
policies, entities, aliases and groups through its API, and checks every answer against Vault's own
sys/capabilities.