railiance-platform/workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md
codex e3bc1ab32b RPF-WP-0065-T04: done; founder applied and verified the platform-admin aal2 binding
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
2026-10-27 23:23:11 +01:00

11 KiB

id type title domain repo status flavor owner created updated related state_hub_workstream_id
RPF-WP-0065 workplan NetKingdom OpenBao role claim bindings: coding-agent principal_type and platform-admin aal2 financials railiance-platform proposed implementation claude 2026-10-24 2026-10-27
NK-WP-0054
RPF-WP-0022
RPF-WP-0035
a042bef1-3370-5731-9f62-a974fa0cb7be

RPF-WP-0065 — Role claim bindings from the NK-WP-0054 review

Receiving workplan for hub message 8da287e2 from net-kingdom (NK-WP-0054, net-kingdom b774927; standard canon/standards/service-identity-privilege_v0.1.md draft-3, review history/2026-10-24-security-identity-and-agent-review.md). These are proposals from net-kingdom. This workplan is source preparation only; nothing is applied live.

Evidence

F-1 coding agent. The token is minted by KeyCape client_credentials for client codex-railiance-platform (docs/coding-agent-openbao-identity.md, KeyCape docs/openbao-service-auth-contract.md). KeyCape src/internal/server/oidc/token.go (~line 382) hardcodes principal_type: "service" and an assurance of aal1/client_secret, with no agent object. So today's real tokens say service; IAM Profile v0.3 "Agent Principal Flow" requires principal_type: agent plus agent.{id,mode}. Flipping the role to agent alone would reject every real token. The role is a migration target: no client secret is delivered and no JWT exchange is established (docs section "Source clarification — 2026-10-11"); the operational identity is the AppRole.

aal2 on the platform-admin path. Humans log in via client openbao-admin (authorization code, public PKCE). KeyCape token.go sets claims["assurance"] for that path with level: aal2 only when MFA was verified in that authorization. KeyCape docs/secrets-engine-openbao-owner-packet.md states the same and recommends the binding. Live evidence: KeyCape docs/evidence/2026-10-07-secrets-engine-openbao-issuance.json 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 gates the apply on a machine-checked aal2 login and keeps a fresh-login rollback plus the break-glass path.

Tasks

id: RPF-WP-0065-T01
status: done
priority: high
state_hub_task_id: "1e8f9164-dc8e-5f75-8fb8-58182092347a"

Coding-agent transition (option a). coding-agent-jwt-role.json now binds principal_type to ["service", "agent"] (OpenBao list = any-of). Sub, audience, tenant, roles, TTL 15m, 8 uses, no default policy are unchanged. Doc table updated. Exception record (compensating controls while service is accepted): the exact sub service:codex:railiance-platform, audience, tenant and coding-agent role stay bound; 15-minute, eight-use token without default policy (inside the SI-14 60-minute ceiling); agent-high-risk-boundary deny wins over the issue-core read policy. Removal condition: drop "service" once (1) KeyCape emits principal_type: agent with agent.id and agent.mode for this client, (2) a value-safe login with such a token succeeded, and (3) the AppRole path is no longer the standing identity. At removal also bind /agent/mode to the agreed mode(s). Review by 2026-12-31.

id: RPF-WP-0065-T02
status: done
priority: high
state_hub_task_id: "67b76351-eb4c-5236-af9f-c5c9eca4d862"

platform-admin aal2 (founder decision 2026-10-24). netkingdom-platform-admin-role.json now binds groups: [net-kingdom-admins] and "/assurance/level": "aal2", as the credential-broker role does. Closes exception SI-X4 once applied.

id: RPF-WP-0065-T03
status: done
priority: medium
state_hub_task_id: "b7bfdb19-a806-589b-b287-fb3abffc2e78"

Tests. tests/test_netkingdom_role_files.py (new) and the bound-claims assertion in tests/test_openbao_platform_admin_callback_prune.py now follow the declaration: python3 -m pytest -q tests/test_netkingdom_role_files.py tests/test_openbao_platform_admin_callback_prune.py.

id: RPF-WP-0065-T04
status: done
priority: high
state_hub_task_id: "abd78db2-03ad-5942-a9ef-12bbada54b28"

Founder live step: apply platform-admin aal2 (needs the founder). See runbook below, rebuilt

2026-10-27, founder-run through the warden lane with scripts/openbao-platform-admin-aal2.sh: prepare (22:19Z, role backup plus /assurance/level claim mapping), apply (22:21Z, gate passed; bound_claims read back as {"groups":["net-kingdom-admins"],"/assurance/level":"aal2"}), verify (22:21Z, fresh MFA login). Value-free receipts at ~/.local/state/railiance/rpf-wp-0065-t04/ on the founder workstation. Two repeated prepare runs refused on the existing receipt path before contacting OpenBao. SI-X4 closed in net-kingdom. 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.

id: RPF-WP-0065-T05
status: wait
priority: medium
state_hub_task_id: "e0e8d3f5-24c2-5106-a2d7-3ebe0845f840"

Coding-agent: issuer first, then role. Blocked on the KeyCape owner (see rollout order). Then the founder applies the role and later removes service.

Runbook for the founder (T04)

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.

set +x; umask 077; unset BAO_TOKEN VAULT_TOKEN
cd ~/railiance-platform
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

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

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

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.

"$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)

  1. KeyCape owner changes the issuer: client_credentials tokens for codex-railiance-platform carry principal_type: agent and agent.id, agent.mode (IAM v0.3). This must come first.
  2. Apply the transitional role (accepts both), value-safe check per docs/coding-agent-openbao-identity.md: bao write auth/netkingdom/role/coding-agent @openbao/auth/coding-agent-jwt-role.json.
  3. After a successful agent-token login, remove "service" and add the /agent/mode binding, apply, re-verify. Rollback: bao delete auth/netkingdom/role/coding-agent (AppRole remains the operational identity).