155 lines
9.4 KiB
Markdown
155 lines
9.4 KiB
Markdown
|
|
# 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.
|