secrets-engine/docs/openbao-stage-roles.md
tegwick 6382139890 feat: add auth-capability lanes and pilot closeout
Add the warden-sign auth-capability lane, AppRole handoff, verification guards, docs, and tests.

Point the whynot-design pilot at the canonical decision and add the real publish closeout preflight/runbook.
2026-06-29 16:58:16 +02:00

3.9 KiB

OpenBao Stage Roles & Bootstrap

secrets-engine talks to OpenBao through three stage roles, never as root or a platform admin. Each role is confined to one stage's KV prefix and a small, explicit capability set.

Role Policy file KV prefix May do May NOT do
secrets-engine-build policies/secrets-engine-build.hcl secret/.../build/ manage build metadata + generated test values; own se-build-* policies/roles touch test/prod, sys/*, auth/token/*, identity/*, act as root
secrets-engine-test policies/secrets-engine-test.hcl secret/.../test/ manage test metadata + values; run positive/negative checks; own se-test-* touch build/prod, sys/*, auth/token/*, identity/*, act as root
secrets-engine-prod policies/secrets-engine-prod.hcl owner-scoped prod lanes apply approved prod ACL policies + approle roles; write approved values; own se-prod-*; exact SECRETS-WP-0004 warden-sign policy/AppRole exception reach build/test, edit the stage roles themselves, admin sys/auth, sys/mounts, identity/*, auth/token/*, act as root

The product requirement is the stage distinction and the denials, not the exact policy names — those may evolve as info-tech-canon hardens.

Negative guarantees (enforced two ways)

  1. In OpenBao — each policy carries explicit deny stanzas for other stages and for sys/*, auth/token/*, identity/*.
  2. In secrets-engineroles.assert_path_in_stage() and roles.assert_policy_safe() reject any plan that would touch another stage's prefix, use a wildcard, name itself like an admin policy, or carry a capability outside create/read/update/delete/list. Auth-capability lanes use roles.assert_auth_capability_safe(), which refuses broad ssh/*, wildcard, sys/, auth/token/, identity/, root-like, or non-update grants and, for the SSH mount, renders only exact ssh/sign/<role> paths. A bad plan fails closed before any OpenBao call.

Run the negative checks:

pytest tests/test_guards.py -q

Bootstrap token files (temporary)

Until OIDC/service auth exists (see the hardening backlog), a platform-root operator may mint a short-lived OpenBao token for a stage role and hand the agent the file path — never the token value.

Requirements for a bootstrap token file:

  • mode 0600, owned by the invoking user;
  • located outside any Git worktree (e.g. ~/.secrets-engine/bootstrap/);
  • named by role + environment only, never by value (e.g. prod.token, not npm_abc123.token);
  • revocable and temporary — tracked with a revocation task;
  • referenced by path, e.g.:
secrets-engine apply whynot-design-npm-publish --stage prod \
  --bootstrap-token-file ~/.secrets-engine/bootstrap/prod.token

secrets-engine refuses a bootstrap token file that is group/other-readable or that lives inside the repo worktree (provision/apply check st_mode & 0o077).

Minting (operator, one-time, root context)

# Write each stage policy into OpenBao.
bao policy write secrets-engine-build policies/secrets-engine-build.hcl
bao policy write secrets-engine-test  policies/secrets-engine-test.hcl
bao policy write secrets-engine-prod  policies/secrets-engine-prod.hcl

# Mint a short-lived token for one stage role, store mode-0600 outside the repo.
install -m 700 -d ~/.secrets-engine/bootstrap
bao token create -policy=secrets-engine-prod -ttl=1h -field=token \
  > ~/.secrets-engine/bootstrap/prod.token
chmod 600 ~/.secrets-engine/bootstrap/prod.token

Revocation (always have a path)

# Revoke by accessor (preferred) or delete the file when the TTL is short.
bao token revoke -accessor <accessor>
shred -u ~/.secrets-engine/bootstrap/prod.token

Every minted bootstrap token MUST have a corresponding revocation task in the hardening backlog (docs/hardening-backlog.md).