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
This commit is contained in:
parent
be8fab0332
commit
a3897c2317
6 changed files with 475 additions and 6 deletions
154
docs/keycape-openbao-consumer-custody.md
Normal file
154
docs/keycape-openbao-consumer-custody.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# 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.
|
||||
|
|
@ -17,6 +17,13 @@ outside every Git worktree. It is sent with HTTP Basic authentication and never
|
|||
placed in a request body, command argument, evidence record, or object
|
||||
representation.
|
||||
|
||||
Since 2026-10-06 the file must also be a canonical regular file (no symlink
|
||||
component) owned by the running user, in a parent directory owned by that
|
||||
user with mode 0700, and hold exactly one token.
|
||||
`secrets-engine service-auth check-secret-file [--path P] [--json]` reports
|
||||
those checks without the value. Consumer host, path, delivery and lifecycle
|
||||
coordinates: [keycape-openbao-consumer-custody.md](keycape-openbao-consumer-custody.md).
|
||||
|
||||
JWT payload parsing does not prove a signature. OpenBao must verify RS256
|
||||
against the configured issuer before issuing a token. railiance-platform owns
|
||||
the exact OpenBao JWT auth mount/role, issuer keys, claim bindings, token
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue