Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
17 KiB
| id | type | title | domain | repo | status | owner | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|
| RPF-WP-0054 | workplan | Broker lane keycape-client-secret-generate: generate KeyCape client secrets in custody | financials | railiance-platform | finished | claude-code | 2026-10-09 | 2026-10-10 | 73694adc-12a0-5226-a814-ee096d158f60 |
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
id: RPF-WP-0054-T01
status: done
priority: high
state_hub_task_id: "38f92db0-317e-5b2b-8bbd-b4af3272cc21"
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.
2026-10-09 ops-warden (23985370): the caller is the human operator who runs
warden case run. Warden runs credential.py generate as a child process in
the caller's environment and holds no token or grant, so the caller's own OIDC
session is the acting identity. Warden refuses the step under WARDEN_AGENT_ID.
The case reference is <case_id>@sha256:<hex64> over the case file's exact
bytes (warden case plan prints it). The platform must also take --case-file,
re-derive the digest, and check that status is materialized and that
parameters.client_id equals --client-id. The client_id pattern is taken from
the case type, ^[a-z0-9][a-z0-9-]{1,62}$ (authoritative; it replaces the
proposal above). Still open: key-cape confirmation of length and alphabet,
asked 2026-10-09.
2026-10-10 key-cape (4e0213b3): format confirmed as proposed, 48 random bytes
as URL-safe base64 without padding (64 characters) in the single field
CLIENT_SECRET, read only through ESO Secret sso/keycape-{client_id}-client.
KeyCape compares secrets as opaque strings in constant time and imposes no
length or alphabet limit. It requires only a non-empty clientId; the pattern
belongs to the case type. T01 is closed.
T02 — Decide the grant shape
id: RPF-WP-0054-T02
status: done
priority: high
state_hub_task_id: "4183abc4-4acc-5937-9f27-005feb181025"
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-generateholds policykeycape-client-secret-generate:createonplatform/data/workloads/keycape/+,readonplatform/metadata/workloads/keycape/+(absence proof and version verification, no value), and explicitdenyon existing reserved entries (todaykeycape/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-pathartifact 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 withattended: falsefor 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).
Decision 2026-10-09 (founder): option A, one templated create-only grant
keycape/client-secret-generate with the broker-side case-ref and
absence checks above. T04 implements it; per-case exact grants (B) are not
built.
T03 — Implement the broker generate operation
id: RPF-WP-0054-T03
status: done
priority: high
depends_on: [RPF-WP-0054-T01, RPF-WP-0054-T02]
state_hub_task_id: "3e159e92-f5ee-5a92-b887-e06daf7dfbdb"
Add credential.py generate --grant keycape/client-secret-generate --client-id <id> --case-ref <ref> [--receipt <file>] [--dry-run]:
- Validate the grant, the client_id pattern, the reserved-entry denylist and the case-ref shape. Refuse wildcard, parent or extra segments.
- Obtain the grant's short-lived token through the existing lease path. The caller's identity is used, and the token is never printed.
- Prove absence: metadata read returns 404. Anything else refuses (exit 3 if present).
- Generate in process and write
{"options":{"cas":0},"data":{"CLIENT_SECRET":...}}through stdin. - Verify by metadata only: current_version 1, not deleted. Never read the data
path. The case-ref goes in the receipt and the hub metadata, not in OpenBao
custom_metadata: writing that needs metadataupdate, which would also let the token changedelete_version_after/max_versionsof other clients' entries (decided while doing T04, 2026-10-09). - 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.
2026-10-09 progress: scripts/keycape_client_secret_generate.py, reached as
credential.py generate --grant keycape/client-secret-generate --client-id <id> --case-file <case.yaml> --case-ref <ref> [--receipt f] [--dry-run] [--record-state-hub]. It refuses agents, root, and callers without the issuer
policy. It verifies the case file as ops-warden specified, mints the 5m grant
token, and makes every KV call with that token only. It proves absence, writes
once with CAS 0, verifies by metadata (version 1), revokes the token, and prints
nothing. There are 8 offline tests. Remaining before done: key-cape's format
confirmation (SECRET_BYTES is the proposed 48 bytes, 64 URL-safe characters).
2026-10-10: key-cape confirmed the format; SECRET_BYTES stays as is. T03 is done.
The live probe passed under T05. The first real run (Forgejo) needs the case
net-kingdom/security-cases/forgejo-netkingdom-oidc.yaml to reach materialized
(today accepted; ops-mason's step).
T04 — Grant, policy and token role source plus CCR
id: RPF-WP-0054-T04
status: done
priority: high
depends_on: [RPF-WP-0054-T02]
state_hub_task_id: "74eb1309-df16-5163-9840-ffb88072d09e"
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.
2026-10-09 done: CCR-2026-0040 (request type custody-generate-grant,
scripts/broker_grant_lane.py) writes the grant policy
keycape-client-secret-generate (create on keycape/+ data, read on keycape/+
metadata, deny on keycape/secrets-engine-openbao, revoke-self), the issuer
policy credential-broker-keycape-client-secret-generate-issuer and the token
role (5m, orphan, non-renewable, no default policy), then reads all three back.
The catalog grant is pilot/approval-required, exec-env only. The apply binds
the issuer policy to no identity, so it is inert until T01 names the caller.
Agent boundary: no coding-agent policy grants any keycape/ path, and the
create-only grant is never attached to an agent identity. No new boundary deny
is needed.
T05 — Attended apply and disposable probe
id: RPF-WP-0054-T05
status: done
priority: high
depends_on: [RPF-WP-0054-T03, RPF-WP-0054-T04]
state_hub_task_id: "a21d1915-fee9-5c31-89e7-a4560c645950"
The CCR apply can run any time; it is inert until the issuer binding exists:
cd ~/railiance-platform
export BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200
python3 scripts/credential-change.py approve CCR-2026-0040 --reviewer "platform-owner, key-cape-owner (founder Bernd Worsch)" --comment "Approve create-only broker grant keycape/client-secret-generate (RPF-WP-0054-T02 option A)." \
&& 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.
2026-10-10: the founder chose net-kingdom-admins + aal2 for the caller.
CCR-2026-0043 (request type broker-issuer-binding,
scripts/issuer_binding_lane.py) adds OIDC role
credential-broker-keycape-client-secret-generate on auth/netkingdom. It
carries only the issuer policy and has loopback callbacks, a 15m TTL, 8 uses and
no default policy. The apply refuses unless the live issuer policy matches source.
Founder session (probe uses a synthetic, founder-written case file; the real Forgejo run waits for key-cape's format confirmation):
cd ~/railiance-platform
export BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200
# 1. binding
python3 scripts/credential-change.py approve CCR-2026-0043 --reviewer "platform-owner, key-cape-owner (founder Bernd Worsch)" --comment "Approve issuer binding for keycape-client-secret-generate: net-kingdom-admins + aal2 (RPF-WP-0054)." \
&& python3 scripts/credential-change.py confirm-binding CCR-2026-0043 --reviewer founder --comment "groups=[net-kingdom-admins] and /assurance/level=aal2, same binding as CCR-2026-0034." \
&& warden access openbao-platform-admin-login --exec -- python3 scripts/credential-change.py applier-apply CCR-2026-0043 --confirm "DELEGATED APPLY CCR-2026-0043" --actor founder --record-state-hub --quiet
# 2. probe case (synthetic, materialized, never registered in KeyCape)
P=probe-secret-generate-20261010
cat > ~/$P-case.yaml <<YAML
apiVersion: netkingdom.io/security-case/v0.1
kind: SecurityCase
spec:
id: $P
type: oidc-browser-relying-party@1.0
status: materialized
parameters:
client_id: $P
client_type: confidential
YAML
REF="$P@sha256:$(sha256sum ~/$P-case.yaml | cut -d' ' -f1)"
# 3. log in as the bound operator role (token goes to the bao token helper)
bao login -no-print -method=oidc -path=netkingdom role=credential-broker-keycape-client-secret-generate
G="python3 scripts/credential.py generate --grant keycape/client-secret-generate --client-id $P --case-file $HOME/$P-case.yaml --case-ref $REF"
$G --dry-run --receipt ~/$P-dry.json
$G --receipt ~/$P-1.json # expect generated, kv_version 1
$G --receipt ~/$P-2.json # expect exit 3, already_present
bao token revoke -self
cat ~/$P-dry.json ~/$P-1.json ~/$P-2.json
# 4. remove the probe entry
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.
2026-10-10 second probe passed after re-applying both CCRs (09:15Z). The
caller token held only the issuer policy with num_uses 0. Results: dry-run
dry_run_would_generate; generate generated, kv_version 1; second run exit 3
already_present. The grant token was revoked every time, operator
revoke-self succeeded, and no value was read. The probe entry was then
metadata-deleted. Receipts: docs/evidence/2026-10-10-keycape-client-secret-generate-probe.json.
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.
Record the receipts under docs/evidence/.
T06 — Hand the lane to ops-warden and net-kingdom
id: RPF-WP-0054-T06
status: done
priority: medium
depends_on: [RPF-WP-0054-T05]
state_hub_task_id: "d7015584-3429-5f32-b4bc-0a9b64017c15"
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").
2026-10-10: ops-warden catalogued the lane as draft (ops-warden 0ae8a0e,
playbook wiki/playbooks/keycape-client-secret-generate.md) and activates it
when platform confirms. Probe receipt sent. Activation is held until key-cape
confirms the secret format (T01/T03).
2026-10-10: ops-warden activated the lane (ops-warden 6414252). The playbook
leads with the issuer-role login, and warden case check-type reports
oidc-browser-relying-party 1.0 as plannable. WARDEN-WP-0044-T03 now waits only
on the Forgejo case reaching materialized. The c3.runtime-secrets
capability is left for a later declaration; mint-secret works as a
follow-through step. T06 is done.
T07 — Rotation lane (separate, later)
id: RPF-WP-0054-T07
status: cancel
priority: low
depends_on: [RPF-WP-0054-T05]
state_hub_task_id: "d5f0beb6-54dc-502b-9c13-0cb6892ab87d"
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.
Closure — 2026-10-10
The first-generation lane is complete: T01–T06 record owner contract acceptance,
source implementation, CCR-2026-0040/0043 attended apply, the disposable CAS-0
probe and self-revocation/cleanup receipts, and ops-warden activation at 6414252.
The live proof is docs/evidence/2026-10-10-keycape-client-secret-generate-probe.json.
This closes the lane itself; it does not claim the Forgejo reference case is live
or replace its independent MASON-WP-0010 / WARDEN-WP-0044 acceptance.
T07 was explicitly outside this lane's scope (separate, later rotation). It is
cancelled here as transferred, not implemented. The actionable follow-up is
RPF-WP-0055, flavor: residual, origin_ref: RPF-WP-0054; no demand or risk
promotion is claimed. Rotation requires its own reviewed CAS/update authority,
ESO and KeyCape coordination, verification and rollback.