RPF-WP-0054-T05: fix issuer binding after the first probe
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:
parent
b328d36a20
commit
6f77484785
8 changed files with 77 additions and 28 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}]", "}"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue