diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index b089e21..b47cf80 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -44,6 +44,7 @@ | workplan | RPF-WP-0062 | finished | — | workplans/RPF-WP-0062-custodian-agent-observer-identity.md | | workplan | RPF-WP-0063 | finished | — | workplans/RPF-WP-0063-wave2-resource-right-sizing.md | | workplan | RPF-WP-0064 | active | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | +| workplan | RPF-WP-0065 | proposed | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | | task | RPF-WP-ADHOC-2026-10-10-T01 | done | — | workplans/ADHOC-2026-10-10.md | | task | RPF-WP-ADHOC-2026-10-11-T01 | done | — | workplans/ADHOC-2026-10-11.md | | task | RPF-WP-ADHOC-2026-10-11-T02 | done | — | workplans/ADHOC-2026-10-11.md | @@ -201,6 +202,11 @@ | task | RPF-WP-0064-T06 | done | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | | task | RPF-WP-0064-T07 | done | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | | task | RPF-WP-0064-T08 | done | — | workplans/RPF-WP-0064-isolated-original-openbao-restore.md | +| task | RPF-WP-0065-T01 | done | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | +| task | RPF-WP-0065-T02 | done | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | +| task | RPF-WP-0065-T03 | done | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | +| task | RPF-WP-0065-T04 | wait | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | +| task | RPF-WP-0065-T05 | wait | — | workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md | | intake | RPF-IN-0001 | closed | — | intakes/intakes.md | | intake | RPF-IN-0002 | open | — | intakes/intakes.md | | intake | RPF-IN-0003 | open | — | intakes/intakes.md | diff --git a/docs/coding-agent-openbao-identity.md b/docs/coding-agent-openbao-identity.md index 1a024ee..0a20b63 100644 --- a/docs/coding-agent-openbao-identity.md +++ b/docs/coding-agent-openbao-identity.md @@ -55,7 +55,7 @@ coordinates: | --- | --- | | `aud` / client id | `codex-railiance-platform` | | `sub` | `service:codex:railiance-platform` | -| `principal_type` | `service` | +| `principal_type` | `service` today; `agent` after the KeyCape change (RPF-WP-0065). The role accepts both during the transition | | `tenant` | `tenant:platform` | | `roles` | contains `coding-agent` | | requested scope | `openbao:login` | diff --git a/openbao/auth/coding-agent-jwt-role.json b/openbao/auth/coding-agent-jwt-role.json index 8fa6147..f4343c5 100644 --- a/openbao/auth/coding-agent-jwt-role.json +++ b/openbao/auth/coding-agent-jwt-role.json @@ -7,7 +7,7 @@ "bound_claims_type": "string", "bound_claims": { "sub": "service:codex:railiance-platform", - "principal_type": "service", + "principal_type": ["service", "agent"], "tenant": "tenant:platform", "roles": "coding-agent" }, diff --git a/openbao/auth/netkingdom-platform-admin-role.json b/openbao/auth/netkingdom-platform-admin-role.json index 103061a..6e32fdf 100644 --- a/openbao/auth/netkingdom-platform-admin-role.json +++ b/openbao/auth/netkingdom-platform-admin-role.json @@ -8,7 +8,8 @@ "bound_claims": { "groups": [ "net-kingdom-admins" - ] + ], + "/assurance/level": "aal2" }, "bound_claims_type": "string", "bound_subject": "", diff --git a/tests/test_netkingdom_role_files.py b/tests/test_netkingdom_role_files.py new file mode 100644 index 0000000..31cbdcc --- /dev/null +++ b/tests/test_netkingdom_role_files.py @@ -0,0 +1,37 @@ +"""Source checks for the netkingdom OpenBao role files (RPF-WP-0065). No live access.""" +import json +from pathlib import Path + +AUTH = Path(__file__).resolve().parents[1] / 'openbao/auth' + + +def role(name): + return json.loads((AUTH / name).read_text()) + + +def test_platform_admin_binds_group_and_aal2(): + r = role('netkingdom-platform-admin-role.json') + assert r['bound_claims_type'] == 'string' + assert r['bound_claims'] == {'groups': ['net-kingdom-admins'], '/assurance/level': 'aal2'} + assert r['groups_claim'] == 'groups' + assert r['token_policies'] == ['platform-admin', 'operator-custody'] + + +def test_platform_admin_matches_credential_broker_assurance_binding(): + admin = role('netkingdom-platform-admin-role.json')['bound_claims'] + broker = role('credential-broker-keycape-client-secret-generate-oidc-role.json')['bound_claims'] + assert admin['/assurance/level'] == broker['/assurance/level'] == 'aal2' + + +def test_coding_agent_accepts_service_and_agent_during_transition(): + r = role('coding-agent-jwt-role.json') + claims = r['bound_claims'] + assert sorted(claims['principal_type']) == ['agent', 'service'] + # nothing else loosened + assert claims['sub'] == 'service:codex:railiance-platform' + assert claims['tenant'] == 'tenant:platform' + assert claims['roles'] == 'coding-agent' + assert r['bound_audiences'] == ['codex-railiance-platform'] + assert r['token_ttl'] == r['token_max_ttl'] == '15m' + assert r['token_no_default_policy'] is True + assert 'default' not in r['token_policies'] diff --git a/tests/test_openbao_platform_admin_callback_prune.py b/tests/test_openbao_platform_admin_callback_prune.py index 2efb379..3bf97b8 100644 --- a/tests/test_openbao_platform_admin_callback_prune.py +++ b/tests/test_openbao_platform_admin_callback_prune.py @@ -32,7 +32,7 @@ def test_prunes_only_the_two_retired_callbacks_and_keeps_settings(): assert not set(m.RETIRED) & set(uris) assert m.KEEP in uris and 'http://localhost:8250/oidc/callback' in uris assert state['role']['token_policies'] == ['platform-admin', 'operator-custody'] - assert state['role']['bound_claims'] == {'groups': ['net-kingdom-admins']} + assert state['role']['bound_claims'] == DECLARED['bound_claims'] def test_idempotent_when_already_pruned(): diff --git a/workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md b/workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md new file mode 100644 index 0000000..5af9c32 --- /dev/null +++ b/workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md @@ -0,0 +1,146 @@ +--- +id: RPF-WP-0065 +type: workplan +title: "NetKingdom OpenBao role claim bindings: coding-agent principal_type and platform-admin aal2" +domain: financials +repo: railiance-platform +status: proposed +flavor: implementation +owner: claude +created: "2026-10-24" +updated: "2026-10-24" +related: + - NK-WP-0054 + - RPF-WP-0022 + - RPF-WP-0035 +--- + +# 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 keeps an open admin session and a one-command rollback. + +## Tasks + +```task +id: RPF-WP-0065-T01 +status: done +priority: high +``` +**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. + +```task +id: RPF-WP-0065-T02 +status: done +priority: high +``` +**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. + +```task +id: RPF-WP-0065-T03 +status: done +priority: medium +``` +**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`. + +```task +id: RPF-WP-0065-T04 +status: wait +priority: high +``` +**Founder live step: apply platform-admin aal2 (needs the founder).** See runbook below. + +```task +id: RPF-WP-0065-T05 +status: wait +priority: medium +``` +**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) + +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. + +```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"} +``` + +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 `. + +Rollback (if step 4 fails, from the still-open session): + +```bash +git show HEAD:openbao/auth/netkingdom-platform-admin-role.json > /tmp/pa-role.new # post-change; for the old body use the commit before this workplan: +git show "$(git log --format=%h -n1 --diff-filter=M -- openbao/auth/netkingdom-platform-admin-role.json)^: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 +``` + +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. + +## 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).