From 47912db30fdaa197adc8ea093d741e5fd92d20a9 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 9 Oct 2026 14:43:51 +0200 Subject: [PATCH] RPF-WP-0054: propose keycape-client-secret-generate broker lane Generate-in-custody operation for the oidc-browser-relying-party mint-secret step (ops-warden d9f889b1, ops-mason 789feb39). Create-only, first generation only; grant shape (templated keycape/+ vs per-case exact) left to the founder with a recommendation; rotation split into a later task. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 469992@bnt-lap001 Assistant-Session: e4b8e420-df0a-441d-83ae-7f19130594d9 --- ...054-keycape-client-secret-generate-lane.md | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md diff --git a/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md b/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md new file mode 100644 index 0000000..b8623ce --- /dev/null +++ b/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md @@ -0,0 +1,191 @@ +--- +id: RPF-WP-0054 +type: workplan +title: "Broker lane keycape-client-secret-generate: generate KeyCape client secrets in custody" +domain: financials +repo: railiance-platform +status: proposed +owner: claude-code +created: "2026-10-09" +updated: "2026-10-09" +--- + +Demand: net-kingdom SecurityCaseType `oidc-browser-relying-party` 1.0 +(`net-kingdom/security-case-types/oidc-browser-relying-party/type.yaml`), +follow-through step `mint-secret`: owner railiance-platform, lane +`keycape-client-secret-generate`, `warden_executes: true`, `attended: false`, +`value_handling: generated_in_custody`. It must "generate a client secret inside +custody at `bao:platform/workloads/keycape/{client_id}` using the caller's +broker grant for the case's KV path, and never return the value". Requests: +ops-mason `789feb39` (2026-10-08) and ops-warden `d9f889b1` (2026-10-09); this +gates WARDEN-WP-0044-T02 and the Forgejo reference case NK-WP-0048-T06. + +Today the broker (`scripts/credential.py`: request, exec, status, revoke) only +leases credentials, and `credential-change.py` only applies or moves material +the caller provides. Nothing generates a value. ops-warden needs a command, a +grant shape and a verification that does not read the value. + +**Meaning of "in custody".** OpenBao KV cannot generate values, and +`sys/tools/random` returns bytes to its caller. The value is therefore generated +in the broker process (`secrets` module) and goes only to that process's memory +and the `bao` child's stdin, the same rule as the attended seed/move helpers. It +never appears in argv, stdout/stderr, files, receipts, State Hub or the caller's +environment. The caller receives a receipt holding the path, version, timestamps +and an outcome code. + +**Scope.** Only the first generation for a new client (KV version 1, CAS 0). +Rotation needs `update` on an existing entry plus coordination with the KeyCape +restart and ESO refresh. The case type marks it `automatable false until a run +has been proven`, so it is a separate, later task (T07), not part of this lane. +Delivery (`deliver-secret`, ESO) and the per-case read policies remain the +case's `client-secret-path` artifacts and are unchanged here. + +## T01 — Confirm the value and caller contract with key-cape and ops-warden + +```task +id: RPF-WP-0054-T01 +status: todo +priority: high +``` + +Confirm with key-cape: client_id pattern (proposed `^[a-z0-9][a-z0-9-]{2,62}$`), +field name (`CLIENT_SECRET`, single field), secret length and alphabet (proposed +48 random bytes, URL-safe base64 without padding, 64 characters), and that +KeyCape reads it only through the ESO-delivered Secret. Confirm with ops-warden +which identity calls the broker under its caller proxy, and how the accepted +case reference (case id + acceptance record digest) is passed. That reference +goes into the receipt and into OpenBao `custom_metadata` (non-secret). + +**Done when:** both replies are recorded here, with no open question about +format, field or caller. + +## T02 — Decide the grant shape + +```task +id: RPF-WP-0054-T02 +status: todo +priority: high +needs_human: true +``` + +Two options, both create-only (ACL `create` without `update`, so an existing +client's secret can never be overwritten through this lane): + +- **A — one templated grant (recommended).** Grant `keycape/client-secret-generate` + holds policy `keycape-client-secret-generate`: `create` on + `platform/data/workloads/keycape/+`, `read` on + `platform/metadata/workloads/keycape/+` (absence proof and version + verification, no value), and explicit `deny` on existing reserved entries + (today `keycape/secrets-engine-openbao`). The broker additionally refuses a + client_id with no accepted case reference and refuses if metadata already + exists. Each case needs no new policy or CCR; the reach is "create any new + KeyCape client path", and the caller cannot read values. +- **B — one exact grant per case.** Each case's `client-secret-path` artifact + adds a create-only stanza for its own path, applied through a CCR before + mint-secret. This keeps the tightest ACL, but every case needs an attended + policy apply, which conflicts with `attended: false` for this step. + +Recommendation: A. Its widest misuse is creating an unused entry for a client +that does not exist. That cannot leak or overwrite a value, and metadata audit +plus the case-reference check make it visible. + +**Done when:** the founder records the choice here (and on a State Hub decision +if one is opened). + +## T03 — Implement the broker generate operation + +```task +id: RPF-WP-0054-T03 +status: todo +priority: high +depends_on: [RPF-WP-0054-T01, RPF-WP-0054-T02] +``` + +Add `credential.py generate --grant keycape/client-secret-generate --client-id + --case-ref [--receipt ] [--dry-run]`: + +1. Validate the grant, the client_id pattern, the reserved-entry denylist and the + case-ref shape. Refuse wildcard, parent or extra segments. +2. Obtain the grant's short-lived token through the existing lease path. The + caller's identity is used, and the token is never printed. +3. Prove absence: metadata read returns 404. Anything else refuses (exit 3 if + present). +4. Generate in process and write + `{"options":{"cas":0},"data":{"CLIENT_SECRET":...}}` through stdin, then set + `custom_metadata` (case-ref, generator, schema). +5. Verify by metadata only: current_version 1, not deleted, custom_metadata + matches. Never read the data path. +6. Revoke the lease. Write the receipt (0600, O_EXCL, versions, booleans and + outcome only). stdout/stderr stay silent apart from the fixed outcome code. + +Tests with a fake `bao`: success, already-present refusal, CAS-0 race loser, +reserved path, bad client_id, missing case-ref, write failure, verify mismatch, +silence contract, and an assertion that no data-path read occurs. No secret +text appears in any receipt. + +## T04 — Grant, policy and token role source plus CCR + +```task +id: RPF-WP-0054-T04 +status: todo +priority: high +depends_on: [RPF-WP-0054-T02] +``` + +Add the grant to `credential-grants/catalog.yaml` (class per T02, TTL 5m, exec +delivery only, no response-wrap). Add the policy under `openbao/policies/`, the +issuing token role, and a CCR for the attended apply. Agent boundary: +coding-agent identities keep `deny` on the keycape data paths. Check whether +the existing `agent-high-risk-boundary` lane entries need a `keycape/+` deny +and run `make agent-high-risk-boundary-check`. `make credential-grants-validate` +and the token-grant dry-runs must pass. + +## T05 — Attended apply and disposable probe + +```task +id: RPF-WP-0054-T05 +status: todo +priority: high +needs_human: true +depends_on: [RPF-WP-0054-T03, RPF-WP-0054-T04] +``` + +Founder session: +1. Apply the CCR. +2. Run `generate` once for a disposable client_id `probe-secret-generate-`. +3. Verify metadata-only that it is version 1 with custom_metadata present. +4. Confirm a second run refuses, and that the grant token is denied `read` on the + data path, `update`, `delete`, and every path outside `keycape/`. +5. Metadata-delete the probe. + +Record the receipts under `docs/evidence/`. + +## T06 — Hand the lane to ops-warden and net-kingdom + +```task +id: RPF-WP-0054-T06 +status: todo +priority: medium +depends_on: [RPF-WP-0054-T05] +``` + +Reply to ops-warden `d9f889b1` and ops-mason `789feb39` with the command, the +grant shape and the verification (metadata version plus capabilities, no value +read). ops-warden catalogs the lane (pointer-only entry); net-kingdom can then +plan NK-WP-0048-T06. Consider declaring the `c3.runtime-secrets` capability so +the case type can compose mint-secret instead of treating it as a follow-through +step (net-kingdom case README "Known limits"). + +## T07 — Rotation lane (separate, later) + +```task +id: RPF-WP-0054-T07 +status: wait +priority: low +depends_on: [RPF-WP-0054-T05] +``` + +Design `keycape-client-secret-rotate`: `update` with CAS = current version, an +attended window coordinated with ESO refresh and the KeyCape restart, and +rollback to the previous version. Only after one proven first-generation run. +Not part of the mint-secret gate.