Founder direction 2026-10-06: the secret lives in OpenBao. The plan confirms platform/workloads/keycape/secrets-engine-openbao:CLIENT_SECRET (KV v2, cas_required, max_versions=2) and keeps the sso-only ESO issuer reader. It replaces the workstation consumer file with an attended operator OIDC reader (5m, 4 uses, revoke-self, value in memory only) and records why the dependency graph is acyclic. Status: reviewed, awaiting founder approvals A1-A3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 132360@bnt-lap001 Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
16 KiB
| id | demand_source | consumer_repo | credential_type | status | approved_by | approved_at | created | updated |
|---|---|---|---|---|---|---|---|---|
| keycape-secrets-engine-openbao-client-custody | Founder direction 2026-10-06 (relayed by key-cape): the secrets-engine-openbao client secret lives in OpenBao; ops-mason decides where and how. KEY-WP-0037-T02, RPF-WP-0035-T02/T09, SECRETS-WP-0012-T02, CCR-2026-0033 | secrets-engine | openbao-kv-v2-two-reader (kubernetes ESO issuer reader + attended OIDC operator reader) | reviewed | null | null | 2026-10-06 | 2026-10-06 |
Construction plan: KeyCape secrets-engine-openbao client secret custody
1. Demand
KeyCape will register a confidential service client, secrets-engine-openbao
(client_credentials, scope openbao:login, 15m JWTs). Two parties must know
the one client secret: the KeyCape issuer, which checks it, and the
secrets-engine CLI, which presents it to get the service JWT it then uses for
its OpenBao JWT login. The founder directed on 2026-10-06 that the secret lives
in OpenBao and that ops-mason decides where and how. This replaces the
persistent workstation consumer file (secrets-engine decisions D1–D4 in
SECRETS-WP-0012-T02 are superseded). ops-mason designs the structure only and
never handles the value.
2. Existing-structure survey
Source survey only (2026-10-06). This task forbids bao commands, so no
live bao secrets/auth/policy list output is attached. The live metadata-only
survey (scripts/ survey generator, MASON-WP-0008) is a pre-build gate in
MASON-WP-0009-T04, run by the building owner.
CCR-2026-0033(railiance-platform,proposed) already defines the authoritative entryplatform/workloads/keycape/secrets-engine-openbao, fieldCLIENT_SECRET, and the issuer reader: Kubernetes auth roleexternal-secrets-keycape-secrets-engine-openbao, policyworkload-kv-read-keycape-secrets-engine-openbao-eso, 15m, no default policy,metadata_read: false, sso-only ClusterSecretStore, target Secretsso/keycape-secrets-engine-openbao-clientkeyclient-secret.bound_claims_confirmed: false.- The same CCR's custody child (
scripts/keycape_secrets_engine_openbao_custody.py,df24a77, never executed) also writes a persistent consumer file under/home/worsch/.secrets-engine/keycape/. That part is now out of direction. - The platform-mount applier only accepts
platform/workloads/(WARDEN-WP-0028). ops-mason's own lanes followplatform/workloads/<workload>/<entry>(e.g.platform/workloads/inbound-library/imap). - Attended operator reads already exist as OIDC roles on
auth/netkingdom(railiance-backup-workload-kv-read,forgejo-admin-workload-kv-read, boundgroups=net-kingdom-admins, confirmed via the liveplatform-adminrole; callbacks UI +localhost:8250+127.0.0.1:8250). Seerailiance-platform/docs/workload-kv-access-lanes.md. - secrets-engine's provider (
src/secrets_engine/service_auth.py) reads the secret only fromSECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE(read_client_secret_file); the approval profile shares that provider.
Existing-lane answer: the issuer reader in CCR-2026-0033 satisfies the issuer side and is reused unchanged. No lane lets the secrets-engine operator read this entry; one narrow attended reader is needed.
3. Proposed changes
3.1 Location (confirmed, with metadata added)
| Item | Decision |
|---|---|
| Mount / engine | platform, KV version 2 (existing) |
| CLI path | platform/workloads/keycape/secrets-engine-openbao (API: platform/data/..., platform/metadata/...) |
| Field | CLIENT_SECRET (the only field; no other data at this path) |
Metadata (kv metadata put, by the custody child) |
cas_required=true, max_versions=2, delete_version_after=0s |
custom_metadata |
owner=railiance-platform, client_id=secrets-engine-openbao, issuer=keycape, readers=eso-sso-issuer,oidc-secrets-engine-operator, ccr=CCR-2026-0033, plan=ops-mason/keycape-secrets-engine-openbao-client-custody, classification=high, rotation=event-driven+90d |
Platform's proposed path is confirmed. It is under the applier's
platform/workloads/ scope and matches ops-mason's <workload>/<entry> shape
(workload = the issuer that owns the registration). cas_required makes every
write version-guarded server-side, not only by the child's discipline.
max_versions=2 keeps exactly one predecessor for audit during a rotation;
recovery is always roll-forward (see 3.4), never a read of an old version.
3.2 Issuer reader (confirmed as in CCR-2026-0033)
Kubernetes auth (not JWT/OIDC): ESO in external-secrets logs in with role
external-secrets-keycape-secrets-engine-openbao, policy
workload-kv-read-keycape-secrets-engine-openbao-eso = read on
platform/data/workloads/keycape/secrets-engine-openbao only; no metadata, no
list, token_no_default_policy=true, 15m/15m. ClusterSecretStore restricted to
namespace sso; one ExternalSecret produces
sso/keycape-secrets-engine-openbao-client key client-secret; KeyCape reads
it as env KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET (NK-WP-0046).
This reader must stay on Kubernetes auth: moving it to a KeyCape-issued
identity would make KeyCape's own startup depend on KeyCape.
3.3 Consumer reader (new): attended operator OIDC, value in memory only
| Object | Shape |
|---|---|
Policy workload-kv-read-keycape-secrets-engine-openbao-operator |
read on platform/data/workloads/keycape/secrets-engine-openbao; update on auth/token/revoke-self; nothing else (no metadata, list, parent or sibling) |
OIDC role keycape-secrets-engine-openbao-operator-workload-kv-read on auth/netkingdom |
role_type=oidc, token_policies=[the policy above], token_no_default_policy=true, token_ttl=5m, token_max_ttl=5m, token_explicit_max_ttl=5m, token_num_uses=4, token_type=service, user_claim=sub, groups_claim=groups, oidc_scopes=openid,profile,email,groups, bound_claims={groups: [net-kingdom-admins]}, the three existing callbacks |
Use-time flow, per secrets-engine command that selects --auth service-jwt
for this profile:
- secrets-engine starts the OIDC login for exactly this role (browser + MFA
through KeyCape/Authelia/privacyIDEA; an existing browser session is
reused) without a token helper (
-no-storesemantics), capturing the resulting token through a pipe into process memory. Ambient tokens (BAO_TOKEN,~/.vault-token, a platform-admin session) are never used for this read. - One KV GET of
CLIENT_SECRET, token in the request header, in process. auth/token/revoke-selfon the operator token immediately.- The value goes in memory to the KeyCape
/tokenexchange (HTTP Basic), then the existing JWT login on rolesecrets-engine-openbaoand the existing self-revoke at command end. The value is dropped when the command ends.
No workstation copy exists at any time: no file, tmpfs, env var, argv, keyring or cache. That is why no transient artefact is designed in: the read is cheap, and any cache would recreate the file this direction removes.
Alternatives rejected.
- AppRole for secrets-engine: its secret-id becomes a persistent workstation credential with the same power as the old file — the same exposure under another name. It also falls inside MASON-IN-0004's open AppRole escalation finding.
- Response-wrapped delivery per use: needs a second attended platform admin for every command. Kept only as a manual fallback, not a lane.
- Using the operator's OIDC token for the engine's work directly: removes the
separately reviewed
secrets-engine-openbaoidentity and policy.
Why a human OIDC lane still adds something. The human token can only read this one field. The engine's OpenBao work runs under its own audited service identity and policy, not the operator's broader admin rights.
3.4 Circularity: resolved, acyclic
Read dependencies (arrow = "needs"):
- secrets-engine OpenBao work → service JWT →
CLIENT_SECRET→ operator OIDC token → KeyCape human login (Authelia + privacyIDEA) → nothing in this chain. - KeyCape issuer →
ssoSecret → ESO → Kubernetes auth → nothing in this chain. - Writes/rotation → platform custody child under the attended
platform-adminenvelope → never the secrets-engine identity.
There is no cycle. The bootstrap credential for the consumer is a human's
MFA-backed login, not a stored secret. The read lane never accepts the
service JWT it serves, the issuer reader never uses a KeyCape token, and
secrets-engine cannot write or rotate this entry. KeyCape is a shared
dependency of the human login and the service exchange, not a loop: if
KeyCape is down, service-jwt is unavailable wherever the secret lives.
Break-glass stays the existing --auth auto bootstrap-file/BAO_TOKEN
providers, and service-jwt keeps refusing to fall back. Three invariants
keep it acyclic and become negative checks (section 4):
(i) no auth/jwt role grants this path;
(ii) the secrets-engine-openbao JWT policy has no capability on this path;
(iii) the ESO reader is never moved off Kubernetes auth.
3.5 Generation, write, rotation, rollback
- Generation/write (railiance-platform custody child, revised): attended,
platform-adminenvelope, noBAO_TOKEN/VAULT_TOKENin env,umask 077, tracing off. Onesecrets.token_urlsafe(48)value in memory. Metadata-only preflight readscurrent_version. KV-v2 write withcas=0(create) orcas=<observed version>(rotation); value on stdin/HTTP body only. Thenkv metadata putwith 3.1's settings (idempotent, compared before write). The consumer-file write, its parent/temp handling and thecheck-secret-filestep are removed. Receipt: booleans, KV version, request id, CCR/decision ids, source digest. Never argv/stdout/logs/Git/hub/chat. - Issuer pickup: ESO refresh, then the KeyCape restart (secrets load at startup).
- Consumer pickup: automatic — the next command reads the latest version. The cutover window runs from the KV write to the KeyCape restart. In that window exchanges fail closed (401). The child's runbook therefore puts the restart directly after the write.
- Cadence: event-driven (compromise, operator change, KeyCape signing or registration incident) plus every 90 days, through the same attended child under a standing rotation approval for this exact CCR.
- Revocation on compromise: KeyCape/NetKingdom disable the registration first. Then platform rotates (roll-forward) and detaches the operator OIDC role policy if the operator lane is suspected. Issued JWTs live ≤15m. OpenBao tokens from either lane self-revoke and are TTL-bound. No instant JWT recall is claimed.
- Rollback: always roll-forward: a new version with
cas=<current>, never a read or restore of an old value. Lane teardown order: (1) the operator OIDC role, then its policy; (2) ExternalSecret (uid precondition), then the ESO role/policy; (3) the KV entry is retained pending a separately approved destroy. A failed cleanup is recorded as open evidence.
3.6 Who builds what
ops-mason's executor has no OIDC role writer and its build grant has the open MASON-IN-0004 finding, so ops-mason builds nothing here. railiance-platform applies every object through its CCR flow. ops-mason verifies capabilities and proposes the pointer-only ops-warden route.
4. Review notes (phase 2)
- Naming: follows platform's validator (
workload-kv-read-policy prefix,-workload-kv-readOIDC role suffix) and the existing-esosibling. - TTL 5m / 4 uses: one KV read plus revoke-self per command. 4 allows one
retry. It is tighter than the 15m/30m human precedents because the token
exists only inside one process. If the platform CCR schema cannot express
token_num_usesfor OIDC roles, platform extends it or the plan returns to review. TTL alone is not accepted as a substitute. revoke-selfexplicit:token_no_default_policy=trueremoves the default policy'srevoke-self, so the policy grants it explicitly.- Group binding:
net-kingdom-adminsis the confirmed claim of the live admin lanes. This role is strictly narrower than what members already hold, so it widens nothing. A dedicated group would be cleaner but is unconfirmed. KeyCape must confirm (a) the claim and (b) that the OpenBao OIDC client requires the MFA assurance. An unconfirmed binding blocks the build. - One value, two copies: the KV entry (authoritative) and the
ssoSecret (issuer delivery). The consumer holds none at rest. This narrows the founder's earlier two-copy boundary decision632a8893…. - Value handling: ops-mason never reads it. Proof is capabilities-only
(
sys/capabilities) plus the first live exchange (KEY-WP-0037-T04). - CCR target: CCR-2026-0033 records
target.domain: financials. KeyCape and secrets-engine are infotech, so platform should correct it or explain it.
Verification required before routing activation
- Operator lane token:
readon the data path;denyon the metadata path, the parent (list), the siblingplatform/data/workloads/keycape/*, and any write. - ESO lane token:
readon the data path only;denyon metadata/parent/sibling. - A non-member OIDC identity cannot obtain the operator role.
- Circularity invariants 3.4 (i)–(iii) report
deny/absent. - Metadata shows
cas_required=true,max_versions=2, the custom_metadata above, and the expected version (value-free). - A secrets-engine
service-jwtrun reads in memory and logs in. Evidence: provider name, accessor fingerprints, and both revoke-self results. A file source is refused for this profile.
5. Executive summary (phase 3)
- One-line ask. Keep the secrets-engine login secret only in OpenBao. KeyCape gets it through the cluster as today. Bernd's workstation reads it fresh, after a KeyCape login with MFA, for each secrets-engine command, and never stores it.
- Who/what gets access. Two readers. ESO in the cluster, for the
ssonamespace only (already proposed). A new 5-minute operator login role limited to members ofnet-kingdom-admins. - To what. Read-only on exactly one OpenBao entry,
platform/workloads/keycape/secrets-engine-openbaofieldCLIENT_SECRET. - For how long. Operator tokens: 5 minutes, 4 uses, revoked after the read. ESO tokens: 15 minutes. The secret rotates every 90 days and on any incident.
- Blast radius if it leaks. The holder can obtain 15-minute
secrets-engineservice JWTs and, with them, whatever the separately reviewedsecrets-engine-openbaoOpenBao role allows. No disk copy exists to leak. Theft needs an admin's MFA-backed login or thessonamespace. - Cost to reverse. Isolated. Remove one OIDC role, one policy, one ExternalSecret and one ESO role. The entry stays until a separate destroy approval. Rotation is a roll-forward.
Approvals requested (exact):
- A1 — this plan's design as written in section 3 (location + metadata, the issuer reader unchanged, the new operator OIDC role and policy, no consumer copy at rest, roll-forward rotation with event-driven + 90-day cadence). Supersedes secrets-engine D1–D4.
- A2 — railiance-platform's revised CCR-2026-0033 (consumer file removed,
operator reader added or filed as a sibling CCR), approved with its three
reviewers and
bound_claims_confirmed: truefor both readers. - A3 — the exact revised custody-child decision id, for the one attended
generation/write run. It must differ from the boundary decision
632a8893….
Decision: approve / reject / send back with changes.