# 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-engine** — `roles.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/` paths. A bad plan fails closed **before** any OpenBao call. Run the negative checks: ```bash 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.: ```bash 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) ```bash # 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) ```bash # Revoke by accessor (preferred) or delete the file when the TTL is short. bao token revoke -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`).