diff --git a/credential-change-requests/CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml b/credential-change-requests/CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml index c3d624a..e4e0f7c 100644 --- a/credential-change-requests/CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml +++ b/credential-change-requests/CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml @@ -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/ 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/ 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 diff --git a/openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json b/openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json index 2b701de..1e53aa4 100644 --- a/openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json +++ b/openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json @@ -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 } diff --git a/openbao/policies/credential-broker-keycape-client-secret-generate-issuer.hcl b/openbao/policies/credential-broker-keycape-client-secret-generate-issuer.hcl index a019a74..9a18506 100644 --- a/openbao/policies/credential-broker-keycape-client-secret-generate-issuer.hcl +++ b/openbao/policies/credential-broker-keycape-client-secret-generate-issuer.hcl @@ -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"] } diff --git a/scripts/broker_grant_lane.py b/scripts/broker_grant_lane.py index c603416..b54ee06 100644 --- a/scripts/broker_grant_lane.py +++ b/scripts/broker_grant_lane.py @@ -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}]", "}"] diff --git a/scripts/issuer_binding_lane.py b/scripts/issuer_binding_lane.py index e3ce50b..2ba3194 100644 --- a/scripts/issuer_binding_lane.py +++ b/scripts/issuer_binding_lane.py @@ -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 diff --git a/tests/test_broker_grant_lane.py b/tests/test_broker_grant_lane.py index 803d292..2cb084f 100644 --- a/tests/test_broker_grant_lane.py +++ b/tests/test_broker_grant_lane.py @@ -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 = [ diff --git a/tests/test_issuer_binding_lane.py b/tests/test_issuer_binding_lane.py index 997a132..e521bc8 100644 --- a/tests/test_issuer_binding_lane.py +++ b/tests/test_issuer_binding_lane.py @@ -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): diff --git a/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md b/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md index 080e2bb..2434ca3 100644 --- a/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md +++ b/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md @@ -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.