diff --git a/credential-change-requests/CCR-2026-0040-keycape-client-secret-generate-grant.yaml b/credential-change-requests/CCR-2026-0040-keycape-client-secret-generate-grant.yaml index 1d6b996..d4f94c2 100644 --- a/credential-change-requests/CCR-2026-0040-keycape-client-secret-generate-grant.yaml +++ b/credential-change-requests/CCR-2026-0040-keycape-client-secret-generate-grant.yaml @@ -3,27 +3,33 @@ kind: credential-change-request schema_version: 1 request_type: custody-generate-grant title: Broker grant keycape/client-secret-generate (create-only, generate in custody) -status: proposed +status: applied created: '2026-10-09' updated: '2026-10-09' requester: agent: railiance-platform - reason: RPF-WP-0054-T04. net-kingdom SecurityCaseType oidc-browser-relying-party step - mint-secret (lane keycape-client-secret-generate); ops-warden d9f889b1, ops-mason - 789feb39. Founder chose the templated create-only grant 2026-10-09 (T02 option A). + reason: RPF-WP-0054-T04. net-kingdom SecurityCaseType oidc-browser-relying-party + step mint-secret (lane keycape-client-secret-generate); ops-warden d9f889b1, ops-mason + 789feb39. Founder chose the templated create-only grant 2026-10-09 (T02 option + A). review: required: true required_approvers: - platform-owner - key-cape-owner - comments: [] + comments: + - at: '2026-10-09T17:18:53+00:00' + reviewer: platform-owner, key-cape-owner (founder Bernd Worsch) + decision: approved + comment: Approve create-only broker grant keycape/client-secret-generate (RPF-WP-0054-T02 + option A). target: domain: infotech tenant: platform workload: keycape environment: production - purpose: Let the credential broker create a new KeyCape confidential-client secret at - platform/workloads/keycape/ once, without any value leaving custody. + purpose: Let the credential broker create a new KeyCape confidential-client secret + at platform/workloads/keycape/ once, without any value leaving custody. openbao: grant_id: keycape/client-secret-generate kv_prefix: platform/workloads/keycape @@ -36,8 +42,8 @@ openbao: token_role: keycape-client-secret-generate token_ttl: 5m founder_confirmation_items: -- This apply writes two policies and one token role. It binds the issuer policy to no - identity, so nothing can mint the token until a separate review binds it to the +- This apply writes two policies and one token role. It binds the issuer policy to + no identity, so nothing can mint the token until a separate review binds it to the caller named in RPF-WP-0054-T01. - Create-only on keycape/+ means a token can create a new client entry once and can never change an existing one. Its widest misuse is an unused entry for a client @@ -53,8 +59,8 @@ risk: - The token lives at most 5m and only inside the broker process. verification: positive: - - Readback after apply matches source for both policies and the token role (done by - applier-apply; prints nothing). + - Readback after apply matches source for both policies and the token role (done + by applier-apply; prints nothing). - Disposable probe (T05) creates probe-secret-generate- at version 1. negative: - A second create on the probe path, a data read, update, delete and any path outside @@ -63,7 +69,19 @@ verification: activation_conditions: - Founder approval (two reviews). - Issuer binding reviewed after RPF-WP-0054-T01; probe passed (T05). - evidence: [] + evidence: + - at: '2026-10-09T17:19:06+00:00' + actor: founder + kind: delegated_metadata_apply + result: passed + details: + - Delegated metadata applier ran as founder using local bao CLI ambient authority. + - 'Policy metadata write: sys/policies/acl/keycape-client-secret-generate' + - 'Issuer policy metadata write: sys/policies/acl/credential-broker-keycape-client-secret-generate-issuer' + - 'Token role metadata write: auth/token/roles/keycape-client-secret-generate' + - Readback matched source for both policies and the token role. + - Issuer policy is not bound to any identity by this apply. + - No secret values were read, written, printed, or accepted in argv. lifecycle: deactivate: Delete auth/token/roles/keycape-client-secret-generate, then both policies; generated entries stay in custody with their cases. diff --git a/scripts/credential-change.py b/scripts/credential-change.py index 5388684..3eaca25 100755 --- a/scripts/credential-change.py +++ b/scripts/credential-change.py @@ -1331,10 +1331,19 @@ def record_evidence_state_hub( ccr: dict[str, Any], base_url: str, actor: str, kind: str, result: str, details: list[str] ) -> dict[str, Any]: openbao = ccr["openbao"] + # Lane request types differ: workload-kv-read has kv_path, auth lanes have + # auth.mount, custody-generate-grant has kv_prefix. Never fail after an apply. + auth_mount = (openbao.get("auth") or {}).get("mount") + target = ( + openbao.get("kv_path") + or openbao.get("kv_prefix") + or openbao.get("campaign_path") + or (f"auth/{auth_mount}" if auth_mount else "unknown") + ) summary = ( f"CCR {ccr['id']} evidence {kind}/{result} by {actor}: " - f"status={ccr['status']} path={openbao.get('kv_path') or 'auth/' + openbao['auth']['mount']} " - f"policy={openbao['policy_name']}; " + f"status={ccr['status']} path={target} " + f"policy={openbao.get('policy_name', 'unknown')}; " + "; ".join(details) ) return state_hub_post_json( diff --git a/tests/test_broker_grant_lane.py b/tests/test_broker_grant_lane.py index 9a23357..803d292 100644 --- a/tests/test_broker_grant_lane.py +++ b/tests/test_broker_grant_lane.py @@ -65,11 +65,17 @@ def stanzas(text: str) -> dict[str, str]: class BrokerGrantLaneTests(unittest.TestCase): - def test_ccr_validates_and_waits_for_approval(self): + def test_ccr_validates_and_records_the_delegated_apply(self): _ccr, errors, _w = cc.validate_ccr(CCR_PATH) self.assertEqual(errors, []) - blockers = cc.applier_readiness_blockers(_ccr) - self.assertTrue(any("got proposed" in b for b in blockers), blockers) + self.assertEqual(_ccr["status"], "applied") + self.assertTrue(any(e["kind"] == "delegated_metadata_apply" and e["result"] == "passed" + for e in _ccr["verification"]["evidence"])) + + def test_unapproved_ccr_blocks_the_applier(self): + c = ccr() + c["status"] = "proposed" + self.assertTrue(any("got proposed" in b for b in cc.applier_readiness_blockers(c))) def test_grant_policy_is_create_only(self): policy = stanzas((ROOT / ccr()["openbao"]["policy_file"]).read_text()) @@ -102,6 +108,16 @@ class BrokerGrantLaneTests(unittest.TestCase): mutate(c) self.assertNotEqual(lane.validate(c, ROOT), [], label) + def test_state_hub_evidence_summary_handles_prefix_lanes(self): + posted = {} + original = cc.state_hub_post_json + cc.state_hub_post_json = lambda base, route, body: posted.update(body) or {"id": "x"} + try: + cc.record_evidence_state_hub(ccr(), "http://hub", "founder", "delegated_metadata_apply", "passed", ["ok"]) + finally: + cc.state_hub_post_json = original + self.assertIn("path=platform/workloads/keycape", posted["summary"]) + def _apply(self, tamper=False): tmp = Path(tempfile.mkdtemp()) (tmp / "state.json").write_text(json.dumps({"policies": {}, "roles": {}, "tamper": tamper})) 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 93eddbe..227d892 100644 --- a/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md +++ b/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md @@ -183,6 +183,12 @@ python3 scripts/credential-change.py approve CCR-2026-0040 --reviewer "platform- && warden access openbao-platform-admin-login --exec -- python3 scripts/credential-change.py applier-apply CCR-2026-0040 --confirm "DELEGATED APPLY CCR-2026-0040" --actor founder --record-state-hub --quiet ``` +2026-10-09: CCR-2026-0040 applied by the founder at 17:19Z; readback matched for +both policies and the token role. The run then printed a traceback from the +State Hub evidence step (KeyError on `openbao.auth` for a prefix lane), so warden +revoked the session. That was fixed and the hub event was posted afterwards. The +OpenBao objects were complete before the crash. + After T01 and T03 (separate session): review and apply the issuer binding for the named caller, then run `generate` once for a disposable client_id `probe-secret-generate-`. Verify by metadata only that it is version 1.