Skip to content

Latest commit

 

History

History
187 lines (151 loc) · 10.1 KB

File metadata and controls

187 lines (151 loc) · 10.1 KB

vault

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.

Credential

Either of:

  1. A token (auth_mode: token, the default), for instance a periodic service token.
  2. An AppRole (auth_mode: approle): role_id and the secret_id as credential; toolpass logs in at auth/<approle_mount>/login and 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 spend secret_id uses 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.

Connection

  - 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

Identity

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.

Which policies

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.

Resources

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.

Actions

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).

Evaluation

Vault's documented rules, applied to the union of the policies' stanzas:

  1. 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.
  2. 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.
  3. deny in the winning stanza denies. Otherwise the needed capability must be present.
  4. {{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 is unsupported.
  5. allowed_parameters, denied_parameters, required_parameters, min_wrapping_ttl and max_wrapping_ttl on the winning stanza answer unsupported: toolpass does not see the request's parameters (reads carry them too, such as KV's version).

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.

Decisions

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

What it cannot see

  • Token policies from auth roles, unless declared in token_policies.
  • Sentinel (EGP and RGP) policies, control groups, MFA enforcement.
  • Request parameters: allowed_parameters and 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, sudo paths' x-vault-sudo requirement is not checked unless asked with raw:sudo.
  • Path aliases resolved by engines (e.g. secret/foo on KV v2 rewritten by the CLI): toolpass checks the API path, so use kv: for KV engines.

Unverified

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 default policy is attached to every token of the alias mount.
  • The capabilities the deprecated policy = "read|write|sudo" attribute maps to.
  • identity/lookup/entity answers 204 for no match (an empty data is also taken as none).
  • sys/policies/acl/<name> wraps policy in data (the top-level form is also read).

Test

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.