RPF-WP-0065-T04: rebuild founder runbook for the silent warden lane
All checks were successful
CCR intake gate / gate (push) Successful in 12s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

The warden openbao-platform-admin-login lane runs one silent child per login,
fails closed on any output and self-revokes; the old runbook printed output
and rolled back from a standing session that cannot exist.

- scripts/openbao-platform-admin-aal2.sh (+ .py): prepare (backup + assurance
  claim mapping), apply (gated on this login's assurance_level=aal2, readback
  of bound_claims), verify, rollback from the backup receipt via a fresh lane
  login, rollback-body for the docs/openbao.md break-glass path. 0600 O_EXCL
  receipts, value-free; no stdout/stderr.
- Role declaration maps /assurance/level to token metadata assurance_level.
- tests/test_openbao_platform_admin_aal2.py: fake bao end-to-end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4515@bnt-lap001
Assistant-Session: 0bf96127-6e04-41be-bfda-d11717c3f354
This commit is contained in:
codex 2026-10-27 22:46:31 +01:00
parent 87a488cc66
commit bedab6f5ab
5 changed files with 523 additions and 30 deletions

View file

@ -8,7 +8,7 @@ status: proposed
flavor: implementation
owner: claude
created: "2026-10-24"
updated: "2026-10-24"
updated: "2026-10-27"
related:
- NK-WP-0054
- RPF-WP-0022
@ -43,7 +43,8 @@ Live evidence: KeyCape `docs/evidence/2026-10-07-secrets-engine-openbao-issuance
records the founder's OIDC login through `openbao-admin` under bound `/assurance/level aal2`
(role `credential-broker-...` family, same client and mount). Conclusion: verified from source and
prior live evidence; one residual risk is that this exact role has not been exercised with the claim,
so the rollout below keeps an open admin session and a one-command rollback.
so the rollout below gates the apply on a machine-checked aal2 login and keeps a fresh-login
rollback plus the break-glass path.
## Tasks
@ -90,7 +91,9 @@ status: wait
priority: high
state_hub_task_id: "abd78db2-03ad-5942-a9ef-12bbada54b28"
```
**Founder live step: apply platform-admin aal2 (needs the founder).** See runbook below.
**Founder live step: apply platform-admin aal2 (needs the founder).** See runbook below, rebuilt
2026-10-27 for the silent warden lane (`scripts/openbao-platform-admin-aal2.sh`). The declaration
now also maps `/assurance/level` to token metadata `assurance_level` for the pre-apply gate.
```task
id: RPF-WP-0065-T05
@ -103,44 +106,83 @@ Then the founder applies the role and later removes `service`.
## Runbook for the founder (T04)
Preconditions: you are on the workstation with the `openbao-ui-railiance01` tunnel up
(`bridge status`), in the repo checkout, and you are a `net-kingdom-admins` member who completes MFA.
Role JSON contains no secrets. Keep one existing admin session open in another terminal
until step 4 passes.
Rebuilt 2026-10-27 for the warden lane. `warden access openbao-platform-admin-login --exec -- <cmd>`
runs one reviewed command after one OIDC/MFA login, fails closed on any child output (even
whitespace) and self-revokes the token on every path. There is no interactive or standing session,
so every step below is its own login, every child is silent, and rollback never assumes an open
terminal. The previous runbook (bao read/write printing output, rollback from a still-open session)
cannot work in this lane and is retired; the founder's `--exec -- bash` attempt changed nothing.
The child is `scripts/openbao-platform-admin-aal2.sh` (Python: `scripts/openbao_platform_admin_aal2.py`,
tests `tests/test_openbao_platform_admin_aal2.py`). It reuses the role read/payload/write helpers
of `openbao_operator_loopback_callback.py`, prints nothing, writes one `0600` receipt per run
(refuses an existing path before touching OpenBao) and exits non-zero on any refusal or mismatch.
Receipts hold role configuration and the session's policy set, role name and assurance level only;
never a token, accessor, email or username. `scripts/openbao-platform-admin-check.sh` remains the
read-only policy/self-revocation check; it does not compare bound claims, so it is optional here.
**Why three logins.** OpenBao token metadata carries no assurance level today, so the only
value-free proof that the founder's `admin` login is `aal2` needs a claim mapping first:
`prepare` backs up the live role and adds only `claim_mappings["/assurance/level"] = "assurance_level"`
(no new binding, nobody is locked out). `apply` then refuses unless *its own* login's token metadata
shows `role=platform-admin` and `assurance_level=aal2`, and only then writes the declared role and
checks the readback, including `bound_claims == {"groups":["net-kingdom-admins"],"/assurance/level":"aal2"}`.
`verify` proves a fresh login is accepted under the new role. In parallel, the KeyCape sign-in
diagnosis page for `openbao-admin` (NK-WP-0051-T02) shows `assurance` for each login: it must read
`aal2` at steps 2 and 3. A reused KeyCape/Authelia session can be `aal1` even after earlier MFA; if
the page shows `aal1`, step 2 refuses with `gate_login_not_aal2` and nothing changes; report to
KeyCape and do not pin MFA on the client without its own go.
Preconditions: workstation, `openbao-ui-railiance01` tunnel up (`bridge status`), browser opener
working, you are `admin` in `net-kingdom-admins` and complete the one-time code at every login.
```bash
set +x; umask 077; unset BAO_TOKEN VAULT_TOKEN
cd ~/railiance-platform
python3 -m pytest -q tests/test_netkingdom_role_files.py tests/test_openbao_platform_admin_callback_prune.py
# 1 backup of the live role (rollback source)
warden access openbao-platform-admin-login --exec -- \
bao read -format=json auth/netkingdom/role/platform-admin > ~/.local/state/railiance/platform-admin-role.before.json
# 2 apply
warden access openbao-platform-admin-login --exec -- \
bao write auth/netkingdom/role/platform-admin @openbao/auth/netkingdom-platform-admin-role.json
# 3 read back
warden access openbao-platform-admin-login --exec -- \
bao read -format=json auth/netkingdom/role/platform-admin | jq '.data.bound_claims'
# expect {"groups":["net-kingdom-admins"],"/assurance/level":"aal2"}
python3 -m pytest -q tests/test_openbao_platform_admin_aal2.py tests/test_netkingdom_role_files.py
R=~/.local/state/railiance/rpf-wp-0065-t04; mkdir -p -m 700 "$R"
S="$PWD/scripts/openbao-platform-admin-aal2.sh"
# 1 back up the live role and add the assurance mapping (no new restriction)
warden access openbao-platform-admin-login --exec -- "$S" prepare --receipt "$R/1-prepare.json"
# 2 gate + apply: refuses unless this login is aal2 (diagnosis page must show aal2)
warden access openbao-platform-admin-login --exec -- "$S" apply --receipt "$R/2-apply.json"
# 3 fresh login under the new role: aal2, platform-admin + operator-custody, role == declaration
warden access openbao-platform-admin-login --exec -- "$S" verify --receipt "$R/3-verify.json"
jq -r '.mode + " " + .status' "$R"/*.json # prepare prepared / apply applied / verify verified
```
Step 4, prove a fresh MFA login still works: in a new terminal run
`warden access openbao-platform-admin-login --exec -- bao token lookup -format=json | jq '.data.policies'`
and confirm `platform-admin` and `operator-custody`. Only then close the old session.
Optionally record a receipt with `scripts/openbao-platform-admin-check.sh --receipt <new-file>`.
Each `warden` call must exit 0. On a non-zero exit read only the receipt's `status` (`jq -r .status`)
before anything else; codes 11-14 do not prove that nothing changed. Step 1 or 2 refused: the role
has at most the harmless mapping; fix the cause and rerun with a new receipt name.
Rollback (if step 4 fails, from the still-open session):
**Rollback A, login still works** (step 3 failed with warden exit 11, e.g. `role_not_declared`,
`token_policies_missing`): restore the backed-up role with a fresh lane login.
```bash
# previous declaration = the file as of the parent of this workplan's commit
git show 2126457^:openbao/auth/netkingdom-platform-admin-role.json > ~/.local/state/railiance/pa-role.prev.json
grep -c assurance ~/.local/state/railiance/pa-role.prev.json # expect 0
bao write auth/netkingdom/role/platform-admin @$HOME/.local/state/railiance/pa-role.prev.json
warden access openbao-platform-admin-login --exec -- \
"$S" rollback --backup "$R/1-prepare.json" --receipt "$R/4-rollback.json"
jq -r .status "$R/4-rollback.json" # rolled_back
```
If no session is open, use the break-glass recovery path in `docs/openbao.md`.
If the login fails because the token lacks `assurance.level`, check the sign-in diagnosis page of
`openbao-admin` (NK-WP-0051-T02) and report to KeyCape; do not pin MFA on the client without its own go.
**Rollback B, login itself is refused** (step 3 exits 10 `login_failed`, or OpenBao names the bound
claim): no OpenBao session can be obtained, so use the break-glass operator path of
`docs/openbao.md` (`kubectl exec` with the offline-escrowed initial token, "Bootstrap Ceremony"
step 7). The role body is derived locally from the step 1 backup and contains no secret. The
break-glass token is typed at a hidden prompt inside the pod, never in argv, history, a file or a receipt.
```bash
"$S" rollback-body --backup "$R/1-prepare.json" --out "$R/rollback-body.json"
kubectl exec -i -n openbao openbao-0 -- sh -c 'cat > /tmp/pa-rollback.json' < "$R/rollback-body.json"
kubectl exec -it -n openbao openbao-0 -- sh -c 'printf "break-glass token: "; stty -echo; read -r BAO_TOKEN; stty echo; echo; export BAO_TOKEN; bao write auth/netkingdom/role/platform-admin @/tmp/pa-rollback.json >/dev/null && echo rolled-back; rm -f /tmp/pa-rollback.json'
```
Then confirm logins work with a lane `verify` under a new receipt name: warden exit 11 with
`role_not_declared` (not exit 10) is the expected result after rollback. If the initial token was
revoked rather than escrowed, this is a break-glass event under `docs/openbao.md`
(custody owners, recovery shares): stop and raise it, do not improvise. Record the outcome as a
progress event; receipts stay in `$R`, never in Git.
## Rollout order for the coding-agent role (T05)