secrets-engine/docs/keycape-openbao-consumer-custody.md
tegwick a3897c2317
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Name the secrets-engine-openbao consumer custody coordinates.
Adds the consumer custody packet for KEY-WP-0037-T02 / RPF-WP-0035-T02,
strict value-free client-secret file checks with a check-secret-file CLI,
synthetic tests, and SECRETS-WP-0012 tracking founder decisions and the
attended delivery as needs_human wait tasks.

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
2026-10-06 13:39:29 +02:00

9.4 KiB

KeyCape secrets-engine-openbao consumer custody packet

Prepared 2026-10-06 by secrets-engine for KEY-WP-0037-T02 and RPF-WP-0035-T02 (proposed CCR-2026-0033 custody child). Tracked in SECRETS-WP-0012. Source and documentation only. No value was generated, fetched or written; no host, cluster, OpenBao or KeyCape state was changed. The founder's custody-boundary decision 632a8893-0d6a-4591-a50b-40bbdb798f32 approves the two-copy boundary, not generation, fetch, delivery, registration or deployment. Each live step below still needs its own approval.

This packet names the consumer copy only: the secrets-engine-owned mode-0600 file. The authoritative copy (platform KV platform/services/keycape/secrets-engine-openbao, field CLIENT_SECRET) and the issuer-side ESO copy (sso Secret keycape-secrets-engine-openbao-client, key client-secret) are platform's and KeyCape's. Issuer ESO does not supply this copy, and nothing here adds a client-side OpenBao reader.

What the code supports (determined from source)

  • secrets-engine is a CLI, not a daemon. The OpenBao JWT login runs inside whichever process an operator starts with --auth service-jwt (or auto once SECRETS_ENGINE_OPENBAO_JWT_LOGIN is set); it logs in for one command and self-revokes (engine_auth.login_service_jwt). There is no service account, unit file or container for the engine.
  • The provider reads the secret from the path in SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE (config.py), rereads it before every exchange and sends it only as HTTP Basic (service_auth.py).
  • As of this packet the read goes through read_client_secret_file, which refuses, without exposing the value: a relative path; a missing file; any symlink or non-canonical path component; a non-regular file; a file or parent directory not owned by the effective user; any group/other mode bit on the file (must be 0600) or its parent (must be 0700); a path inside a Git worktree; empty content; content that is not exactly one token. Before this change only the file mode, worktree and empty checks existed. The approval profile shares the same provider and therefore the same checks.
  • secrets-engine service-auth check-secret-file [--path P] [--json] runs those checks and prints check names and pass/fail only — never the value, its length or any hash. Exit 0 = accepted, 1 = refused. This is the bounded validation interface RPF-WP-0035-T02 asked for.

Consumer coordinates

Coordinate Value Basis
Host Operator workstation (the WSL2 Linux host where every documented live secrets-engine OpenBao operation runs against BAO_ADDR=https://bao.coulomb.social) Documented practice; founder decision D1
OS user / owner worsch, file and parent worsch:worsch The account that runs the CLI; founder decision D2
File /home/worsch/.secrets-engine/keycape/secrets-engine-openbao.client-secret Follows existing ~/.secrets-engine/bootstrap/ convention
File mode 0600, regular file, single token, no trailing data other than one newline Enforced by code
Parent /home/worsch/.secrets-engine/keycape, 0700, owned by worsch Enforced by code
Grandparent /home/worsch/.secrets-engine exists today as 0755 with only 0700 children; not changed by this packet Observed metadata only
Git Outside every worktree (/home/worsch and ~/.secrets-engine are not worktrees) Enforced by code
Storage Persistent ext4 home, not tmpfs Founder decision D3
Config reference SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE=<file above> config.py
Companion config SECRETS_ENGINE_KEYCAPE_TOKEN_URL=https://kc.coulomb.social/token, SECRETS_ENGINE_KEYCAPE_ISSUER=https://kc.coulomb.social, SECRETS_ENGINE_OPENBAO_JWT_LOGIN=/home/worsch/.secrets-engine/keycape/openbao-jwt-login.yaml (non-secret mount/role: secrets-engine-openbao/bound_issuer, content published by platform) engine_auth.py, platform design

One host and one file. A second host or user would be a third copy and needs its own approval; the engine has no mechanism that distributes the file.

Delivery (platform custody → this file)

Attended operator path, executed on the host above as worsch, inside the platform-owned custody child of CCR-2026-0033:

  1. Preconditions: founder approval of the exact custody child; platform's operator OpenBao session for the KV write; umask 077; shell tracing and credential-bearing debug logs off.
  2. The custody child generates one random value in process memory and compare-before-writes it to the authoritative KV field. If that write fails, no consumer file is written.
  3. In the same process, it writes the consumer copy: create the parent with 0700 if absent (refuse if it exists with other owner/mode); open a new temp file in that directory with O_CREAT|O_EXCL|O_WRONLY, mode 0600; write, fsync, rename onto the final path; fsync the directory. The value never appears in argv, environment, stdout/stderr, logs, Git or the hub.
  4. It then runs secrets-engine service-auth check-secret-file --json and records only that result plus non-secret IDs in the native receipt.

No client-side read of OpenBao is used or required: the consumer copy is written from the same in-memory value that creates the authoritative version. If the founder rejects same-process delivery, the only alternative is a separately admitted, single-path client-side reader (as RPF-WP-0035-T06 does for the approval client); this packet does not request one.

Ownership: platform owns generation, the authoritative write and the delivery code path; secrets-engine owns these destination coordinates, the acceptance check and the file's lifecycle on the host.

Detecting a failed delivery

  • check-secret-file exits 1 and names the failing check (missing, mode, owner, parent, symlink, worktree, empty). The receipt must carry ok: true.
  • At use time the same checks refuse the exchange with ProvisioningError (exit 6); service-jwt never falls back to bootstrap, BAO_TOKEN or AppRole.
  • A value that is well-formed but does not match the issuer is detectable only by the first live exchange (KEY-WP-0037-T04): KeyCape answers HTTP 401 and the engine reports KeyCape token exchange failed with HTTP 401. No hash or fingerprint of the value is recorded to compare copies; that is deliberate.
  • Crash between the KV write and the rename: the KV version exists and the consumer file does not (or a stale temp file remains in the 0700 directory). Recovery is a fresh rotation, not a KV read; the operator removes the exact temp file after inspecting its name and metadata only.

Rotation and revocation

Event Owner Action
Generation / authoritative version railiance-platform New KV version via the custody child
Issuer pickup KeyCape / NetKingdom ESO refresh plus KeyCape restart (secrets load at startup)
Consumer copy replacement secrets-engine (file), executed in platform's attended child Atomic rename as above, then check-secret-file
Disable on compromise KeyCape / NetKingdom Remove or disable the registration first
Retire versions railiance-platform Destroy/retire per CCR
Remove consumer copy secrets-engine operator See rollback below

KeyCape holds one secret per client, so rotation has a cutover window: hold the new value in the attended process, replace the consumer file only after the issuer reload is confirmed, and accept that exchanges fail closed (401) in between. Already-issued JWTs live at most 15 minutes; OpenBao tokens from this login are self-revoked per command and bounded by platform's TTL/use limits. No instant JWT recall is claimed.

Rotation cadence is founder decision D4.

Rollback and cleanup

  1. Unset SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE (and the JWT login contract variable) in the operator environment.
  2. Remove the exact file path (no recursive removal); rmdir the parent only if it is empty.
  3. Verify with check-secret-file --path <file>: it must exit 1 on exists.
  4. Record cleanup status only. Unlink on ext4 is not secure erasure and not revocation; revocation is the KeyCape and platform steps above.

Founder / owner decisions (with recommendation)

  • D1 Host. Recommended: the operator workstation only. Alternative: railiance01, which has no documented secrets-engine OpenBao operator use and would need its own user and path decision.
  • D2 OS user. Recommended: the operator account worsch, since the engine runs only as that user's CLI. A dedicated system user only makes sense once an unattended engine runtime exists, and no source supports one today.
  • D3 Persistence. Recommended: persistent home storage. tmpfs would require re-delivery after every reboot, and every re-delivery would need either a rotation or an unadmitted client-side KV read. Open owner question: whether any workstation backup or sync tool covers ~/.secrets-engine. If one does, exclude it there before delivery.
  • D4 Rotation cadence. Recommended: event-driven (compromise, host or user change, operator departure) plus a 90-day scheduled rotation run through the same attended child. Platform's CCR sets the binding value.

Not established

No registration, issued JWT, KV version, ESO Secret or consumer file exists by implication of this packet. Code tests use synthetic values only and prove file acceptance/refusal, not live delivery, issuer match or OpenBao login.