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:
parent
bab4f2ca6a
commit
2126457a16
7 changed files with 194 additions and 4 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"bound_claims": {
|
||||
"groups": [
|
||||
"net-kingdom-admins"
|
||||
]
|
||||
],
|
||||
"/assurance/level": "aal2"
|
||||
},
|
||||
"bound_claims_type": "string",
|
||||
"bound_subject": "",
|
||||
|
|
|
|||
37
tests/test_netkingdom_role_files.py
Normal file
37
tests/test_netkingdom_role_files.py
Normal 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']
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
146
workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md
Normal file
146
workplans/RPF-WP-0065-netkingdom-role-claim-bindings.md
Normal 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue