RPF-WP-0065: coding-agent accepts service+agent transitionally; platform-admin binds aal2 (source only, NK-WP-0054)

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

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 328925@bnt-lap001
Assistant-Session: 14b795fd-c632-4c01-8d28-e7e490c7b0ed
This commit is contained in:
codex 2026-10-24 23:49:27 +02:00
parent bab4f2ca6a
commit 2126457a16
7 changed files with 194 additions and 4 deletions

View file

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

View file

@ -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` |

View file

@ -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"
},

View file

@ -8,7 +8,8 @@
"bound_claims": {
"groups": [
"net-kingdom-admins"
]
],
"/assurance/level": "aal2"
},
"bound_claims_type": "string",
"bound_subject": "",

View file

@ -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']

View file

@ -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():

View file

@ -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 <new-file>`.
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).