RPF-WP-0054-T05: fix issuer binding after the first probe
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

CCR-2026-0043 applied cleanly, but generate could not mint the grant token
(OpenBao refuses child tokens from a limited-use token; the role had
token_num_uses 8) and revoke-self was denied (missing from the issuer
policy). The role now uses 0, which the validator requires, and the issuer
policy grants revoke-self. Nothing was written by the probe.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 469992@bnt-lap001
Assistant-Session: e4b8e420-df0a-441d-83ae-7f19130594d9
This commit is contained in:
codex 2026-10-10 11:12:19 +02:00
parent b328d36a20
commit 6f77484785
8 changed files with 77 additions and 28 deletions

View file

@ -2,29 +2,40 @@ id: CCR-2026-0043
kind: credential-change-request
schema_version: 1
request_type: broker-issuer-binding
title: Operator OIDC role for the keycape-client-secret-generate broker issuer (net-kingdom-admins, aal2)
status: proposed
title: Operator OIDC role for the keycape-client-secret-generate broker issuer (net-kingdom-admins,
aal2)
status: applied
created: '2026-10-10'
updated: '2026-10-10'
requester:
agent: railiance-platform
reason: RPF-WP-0054-T05. ops-warden (23985370) names the caller as the human operator who
runs warden case run, using their own OIDC session. Founder chose net-kingdom-admins + aal2
on 2026-10-10. The issuer policy is live from CCR-2026-0040.
reason: RPF-WP-0054-T05. ops-warden (23985370) names the caller as the human operator
who runs warden case run, using their own OIDC session. Founder chose net-kingdom-admins
+ aal2 on 2026-10-10. The issuer policy is live from CCR-2026-0040.
review:
required: true
required_approvers:
- platform-owner
- key-cape-owner
comments: []
comments:
- at: '2026-10-10T08:27:32+00:00'
reviewer: platform-owner, key-cape-owner (founder Bernd Worsch)
decision: approved
comment: 'Approve issuer binding for keycape-client-secret-generate: net-kingdom-admins
+ aal2 (RPF-WP-0054).'
- at: '2026-10-10T08:27:32+00:00'
reviewer: founder
decision: binding_confirmed
comment: groups=[net-kingdom-admins] and /assurance/level=aal2, same binding as
CCR-2026-0034.
target:
domain: infotech
tenant: platform
workload: credential-broker
environment: production
purpose: Let a net-kingdom-admins member with a verified second factor log in for 15 minutes
with only the broker issuer policy, so credential.py generate can mint the create-only
keycape/client-secret-generate token.
purpose: Let a net-kingdom-admins member with a verified second factor log in for
15 minutes with only the broker issuer policy, so credential.py generate can mint
the create-only keycape/client-secret-generate token.
openbao:
issuer_policy: credential-broker-keycape-client-secret-generate-issuer
auth:
@ -32,41 +43,54 @@ openbao:
mount: netkingdom
role: credential-broker-keycape-client-secret-generate
role_file: openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json
bound_claims_confirmed: false
bound_claims_confirmed: true
founder_confirmation_items:
- bound_claims_confirmed stays false until the founder confirms groups=[net-kingdom-admins]
and /assurance/level=aal2, the same binding as CCR-2026-0034 (key-cape b2ed890f).
- The role carries only the issuer policy. It cannot read or write KV; it can mint
keycape-client-secret-generate tokens through that token role, and look up and
revoke tokens by accessor.
- Loopback CLI callbacks only (no UI callback); 15m, 8 uses, no default policy.
keycape-client-secret-generate tokens through that token role, and look up and revoke
tokens by accessor.
- Loopback CLI callbacks only (no UI callback); 15m with unlimited uses inside that
lifetime (a limited-use token cannot create the child grant token), no default policy.
access_frontdoor:
type: none
catalog_id: none
risk:
level: medium
notes:
- Membership of net-kingdom-admins is the access decision; that group already holds the
KeyCape operator reader (CCR-2026-0034).
- Worst case within 15m and 8 uses is a few unused keycape/<client_id> entries; existing
entries cannot be overwritten (CCR-2026-0040 is create-only).
- Membership of net-kingdom-admins is the access decision; that group already holds
the KeyCape operator reader (CCR-2026-0034).
- Worst case within 15m is unused keycape/<client_id> entries;
existing entries cannot be overwritten (CCR-2026-0040 is create-only).
verification:
positive:
- Readback after apply matches source for the role (done by applier-apply; prints nothing).
- A member with aal2 logs in with role credential-broker-keycape-client-secret-generate and
the token's policies are exactly [credential-broker-keycape-client-secret-generate-issuer].
- Readback after apply matches source for the role (done by applier-apply; prints
nothing).
- A member with aal2 logs in with role credential-broker-keycape-client-secret-generate
and the token's policies are exactly [credential-broker-keycape-client-secret-generate-issuer].
negative:
- A non-member or a member without aal2 cannot obtain the role.
- The issuer token is denied on platform/* and on token creation through any other role.
- The issuer token is denied on platform/* and on token creation through any other
role.
activation_conditions:
- Founder approval (two reviews) and bound_claims_confirmed true.
- CCR-2026-0040 applied (done 2026-10-09).
evidence: []
evidence:
- at: '2026-10-10T08:27:38+00:00'
actor: founder
kind: delegated_metadata_apply
result: passed
details:
- Delegated metadata applier ran as founder using local bao CLI ambient authority.
- 'Issuer policy confirmed live and matching source: sys/policies/acl/credential-broker-keycape-client-secret-generate-issuer'
- 'OIDC role metadata write: auth/netkingdom/role/credential-broker-keycape-client-secret-generate'
- Readback matched source for the role.
- No secret values were read, written, printed, or accepted in argv.
lifecycle:
deactivate: Delete auth/netkingdom/role/credential-broker-keycape-client-secret-generate.
rotate: Not applicable (no stored credential).
compromised: Delete the role, revoke issued tokens; review keycape/ metadata for entries
without an accepted case.
compromised: Delete the role, revoke issued tokens; review keycape/ metadata for
entries without an accepted case.
state_hub:
workplan: RPF-WP-0054
task: RPF-WP-0054-T05

View file

@ -18,6 +18,6 @@
"token_ttl": "15m",
"token_max_ttl": "15m",
"token_explicit_max_ttl": "15m",
"token_num_uses": 8,
"token_num_uses": 0,
"verbose_oidc_logging": false
}

View file

@ -12,6 +12,9 @@ path "auth/token/revoke-accessor" {
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

View file

@ -86,6 +86,7 @@ def generated_issuer_hcl(ccr: dict[str, Any]) -> str:
("auth/token/lookup-accessor", '"update"'),
("auth/token/revoke-accessor", '"update"'),
("auth/token/lookup-self", '"read"'),
("auth/token/revoke-self", '"update"'),
("sys/capabilities-self", '"update"'),
):
lines += [f'path "{path}" {{', f" capabilities = [{caps}]", "}"]

View file

@ -69,6 +69,10 @@ def validate(ccr: dict[str, Any], repo_dir: Path) -> list[str]:
role = _matrix._shared._load_json(repo_dir, str(auth.get("role_file") or ""), "openbao.auth.role_file", errors)
if role:
_matrix._validate_role(role, issuer, errors)
# OpenBao refuses child-token creation from a limited-use token, and
# minting the grant token is this role's whole purpose (probe 2026-10-10).
if role.get("token_num_uses") != 0:
errors.append("role token_num_uses must be 0: limited-use tokens cannot create child tokens")
return errors

View file

@ -93,6 +93,8 @@ class BrokerGrantLaneTests(unittest.TestCase):
issuer = stanzas((ROOT / ccr()["openbao"]["issuer_policy_file"]).read_text())
creates = [p for p in issuer if p.startswith("auth/token/create")]
self.assertEqual(creates, ["auth/token/create/keycape-client-secret-generate"])
# Probe 2026-10-10: the operator must be able to end its own session.
self.assertEqual(issuer["auth/token/revoke-self"], '"update"')
def test_validator_rejects_drift(self):
cases = [

View file

@ -54,11 +54,15 @@ json.dump(state, open(path, "w"))
class IssuerBindingLaneTests(unittest.TestCase):
def test_ccr_validates_and_waits_for_approval_and_binding(self):
def test_ccr_validates_and_is_ready_to_reapply(self):
_ccr, errors, _w = cc.validate_ccr(CCR_PATH)
self.assertEqual(errors, [])
blockers = cc.applier_readiness_blockers(_ccr)
self.assertIn("applier requires confirmed OpenBao auth binding", blockers)
self.assertEqual(cc.applier_readiness_blockers(_ccr), [])
def test_unconfirmed_binding_blocks_the_applier(self):
c = ccr()
c["openbao"]["auth"]["bound_claims_confirmed"] = False
self.assertIn("applier requires confirmed OpenBao auth binding", cc.applier_readiness_blockers(c))
def test_role_carries_only_the_issuer_policy(self):
role = json.loads((ROOT / ccr()["openbao"]["auth"]["role_file"]).read_text())
@ -73,6 +77,8 @@ class IssuerBindingLaneTests(unittest.TestCase):
("no aal2", {"bound_claims": {"groups": ["net-kingdom-admins"]}}),
("public callback", {"allowed_redirect_uris": ["https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback"]}),
("long ttl", {"token_ttl": "1h"}),
# Probe 2026-10-10: a limited-use token cannot create the grant token.
("limited uses", {"token_num_uses": 8}),
]
for label, change in cases:
with self.subTest(label):

View file

@ -254,6 +254,15 @@ cat ~/$P-dry.json ~/$P-1.json ~/$P-2.json
warden access openbao-platform-admin-login --exec -- sh -c "bao delete platform/metadata/workloads/keycape/$P >/dev/null 2>&1"
```
2026-10-10 first probe: CCR-2026-0043 applied cleanly and the OIDC login
succeeded, but all three `generate` runs exited 4 (`grant_token_create_failed`)
and the operator's `revoke-self` returned 403. Nothing was written. Causes: (1)
the role set `token_num_uses: 8`, and OpenBao refuses child-token creation from a
limited-use token. The role now uses 0, which the lane validator enforces. (2)
The issuer policy lacked `auth/token/revoke-self`; it is now added. Re-apply
CCR-2026-0040 (issuer policy) before CCR-2026-0043 (role), because the binding
apply refuses unless the live issuer policy matches source.
The policy denials (data read, update, delete, other paths) were already proven
by the CCR-2026-0040 readback and its policy tests. The probe proves the live
create-once behaviour.