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 <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
ce1c1a6338
commit
47912db30f
1 changed files with 191 additions and 0 deletions
191
workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md
Normal file
191
workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md
Normal file
|
|
@ -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
|
||||
<id> --case-ref <ref> [--receipt <file>] [--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-<date>`.
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue