Read the secrets-engine-openbao client secret in memory from OpenBao
Founder direction 2026-10-06 and ops-mason plan keycape-secrets-engine-openbao-client-custody (658cdbc): no workstation copy. service-jwt now gets CLIENT_SECRET from platform/workloads/keycape/ secrets-engine-openbao through one attended OIDC login on keycape-secrets-engine-openbao-operator-workload-kv-read (-no-store, token from a pipe, header-only GET, unconditional revoke-self). The consumer file is refused for this profile and kept for the approval client. service-auth probe-openbao prints value-free first-use evidence. SECRETS-WP-0012: T02 cancelled (D1-D4 superseded), T04 done, T03 is the founder-run first live use. Synthetic tests only; 524 passed, 1 skipped. 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
6941610b8d
commit
b8094f701b
12 changed files with 688 additions and 185 deletions
|
|
@ -43,7 +43,7 @@ secrets-engine --version
|
|||
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | _(unset)_ | explicit approval bearer file; conflicts with approval client-secret provider |
|
||||
| `SECRETS_ENGINE_KEYCAPE_TOKEN_URL` | _(unset)_ | KeyCape token endpoint for `service-jwt` |
|
||||
| `SECRETS_ENGINE_KEYCAPE_ISSUER` | _(unset)_ | KeyCape issuer; must match the JWT login contract |
|
||||
| `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` | _(unset)_ | mode-0600 out-of-repo client secret |
|
||||
| `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` | _(unset)_ | **refused** since 2026-10-06: the secrets-engine-openbao secret is read in memory from OpenBao per command (see keycape-openbao-consumer-custody.md) |
|
||||
| `SECRETS_ENGINE_OPENBAO_JWT_LOGIN` | _(unset)_ | platform JWT mount/role contract YAML |
|
||||
|
||||
## Commands
|
||||
|
|
|
|||
|
|
@ -1,154 +1,114 @@
|
|||
# KeyCape `secrets-engine-openbao` consumer custody packet
|
||||
# KeyCape `secrets-engine-openbao` client secret: consumer custody
|
||||
|
||||
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.
|
||||
Rewritten 2026-10-06 by secrets-engine. 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.
|
||||
|
||||
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.
|
||||
## Direction
|
||||
|
||||
## What the code supports (determined from source)
|
||||
Founder direction 2026-10-06: the `secrets-engine-openbao` KeyCape client
|
||||
secret lives **only in OpenBao**. There is no persistent workstation copy.
|
||||
ops-mason decided placement and the read lane in
|
||||
`ops-mason/plans/keycape-secrets-engine-openbao-client-custody.md` (commit
|
||||
`658cdbc`, workplan MASON-WP-0009; status *reviewed*, founder approvals A1–A3
|
||||
pending). That direction and plan **supersede the earlier consumer-file packet
|
||||
and its decisions D1–D4** (host, OS user, persistent file path, rotation
|
||||
cadence). The consumer-file custody child in CCR-2026-0033 is out of direction
|
||||
and is being revised by railiance-platform.
|
||||
|
||||
- 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.
|
||||
## Coordinates (non-secret)
|
||||
|
||||
## Consumer coordinates
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Authoritative entry | KV v2 mount `platform`, path `workloads/keycape/secrets-engine-openbao` (API `platform/data/workloads/keycape/secrets-engine-openbao`), field `CLIENT_SECRET` |
|
||||
| Issuer copy | `sso` Secret `keycape-secrets-engine-openbao-client` key `client-secret`, via ESO on Kubernetes auth (platform/KeyCape; unchanged) |
|
||||
| Consumer copy | **none** — the value exists only in process memory for one command |
|
||||
| Consumer read lane | OIDC mount `auth/netkingdom`, role `keycape-secrets-engine-openbao-operator-workload-kv-read`, policy `workload-kv-read-keycape-secrets-engine-openbao-operator` (read on the data path, `update` on `auth/token/revoke-self`, nothing else), 5m TTL, 4 uses, bound `groups=net-kingdom-admins` |
|
||||
| Companion config | `BAO_ADDR=https://bao.coulomb.social`, `SECRETS_ENGINE_KEYCAPE_TOKEN_URL=https://kc.coulomb.social/token`, `SECRETS_ENGINE_KEYCAPE_ISSUER=https://kc.coulomb.social`, `SECRETS_ENGINE_OPENBAO_JWT_LOGIN=<platform-published contract file>` |
|
||||
|
||||
| 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 |
|
||||
The mount, path, field and role are pinned in
|
||||
`service_auth.OpenBaoOidcClientSecretSource`; any other value is refused.
|
||||
|
||||
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.
|
||||
## What the code does (per command that selects `service-jwt`)
|
||||
|
||||
## Delivery (platform custody → this file)
|
||||
1. Runs `bao login -method=oidc -path=netkingdom -no-store -format=json
|
||||
role=keycape-secrets-engine-openbao-operator-workload-kv-read`. The
|
||||
browser/MFA prompt goes to the operator's terminal (stderr); the login
|
||||
response is captured from a **stdout pipe into memory**. `BAO_TOKEN` and
|
||||
`VAULT_TOKEN` are removed from the child environment, and `-no-store` keeps
|
||||
the token out of the token helper. For non-token methods the CLI does not
|
||||
send a helper-stored token. The token never appears in argv, env, a file,
|
||||
a log or evidence.
|
||||
2. One in-process HTTPS `GET /v1/platform/data/workloads/keycape/secrets-engine-openbao`
|
||||
with the token only in the `X-Vault-Token` header; extracts `CLIENT_SECRET`
|
||||
(must be exactly one non-empty token).
|
||||
3. `POST /v1/auth/token/revoke-self` with the same token — always, also when
|
||||
the read fails.
|
||||
4. The value goes in memory to the KeyCape `/token` `client_credentials`
|
||||
exchange (HTTP Basic only), then the existing OpenBao JWT login on the
|
||||
platform role and its self-revoke at command end. The value is dropped
|
||||
when the exchange request is built.
|
||||
|
||||
Attended operator path, executed on the host above as `worsch`, inside the
|
||||
platform-owned custody child of CCR-2026-0033:
|
||||
Refusals: `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` set → refused for this
|
||||
profile; a `KeyCapeServiceAuthConfig` with a file → refused; a provider for this
|
||||
profile without the in-memory source → refused. `service-jwt` still never falls
|
||||
back to `--bootstrap-token-file`, `BAO_TOKEN` or AppRole. The approval client
|
||||
(`secrets-engine-approval`) keeps its own file source and
|
||||
`service-auth check-secret-file` for it.
|
||||
|
||||
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.
|
||||
Evidence is value-free: `secret_source=openbao-oidc-in-memory`, the operator
|
||||
and engine accessor fingerprints (12 hex chars of SHA-256), and both
|
||||
revoke-self results. Tests with synthetic values:
|
||||
`tests/test_openbao_secret_source.py`.
|
||||
|
||||
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.
|
||||
## First live use (founder-run, attended)
|
||||
|
||||
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.
|
||||
Run only after **all** of: founder approval of the ops-mason plan (A1), the
|
||||
revised CCR-2026-0033 applied by railiance-platform (A2/A3: KV entry written,
|
||||
operator OIDC role and policy, ESO reader, JWT role), the KeyCape
|
||||
`secrets-engine-openbao` registration live and KeyCape restarted after the KV
|
||||
write (KEY-WP-0037), and the platform JWT login contract file published. This
|
||||
is the evidence step for KEY-WP-0037-T04 / RPF-WP-0035-T02 and plan
|
||||
verification item 6.
|
||||
|
||||
## Detecting a failed delivery
|
||||
```bash
|
||||
cd /home/worsch/secrets-engine && \
|
||||
env -u BAO_TOKEN -u VAULT_TOKEN -u SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE \
|
||||
BAO_ADDR=https://bao.coulomb.social \
|
||||
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 \
|
||||
uv run secrets-engine service-auth probe-openbao --json
|
||||
```
|
||||
|
||||
- `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.
|
||||
A browser opens for the KeyCape login with MFA (an existing session is
|
||||
reused); stderr shows the `bao` OIDC prompt. Expected stdout, exit 0 — one
|
||||
line, only these keys, `<fp>` being 12 hex characters:
|
||||
|
||||
## Rotation and revocation
|
||||
```json
|
||||
{"auth_provider": "service-jwt", "established": true, "ok": true, "operator_revocation_attempted": true, "operator_revocation_succeeded": true, "operator_session_handle": "<fp>", "revocation_attempted": true, "revocation_succeeded": true, "secret_source": "openbao-oidc-in-memory", "session_handle": "<fp>"}
|
||||
```
|
||||
|
||||
| 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 |
|
||||
Anything else is a failure to record, not to retry blindly: exit 1 with
|
||||
`error: ...` on stderr names the failed step (OIDC login exit code, KV HTTP
|
||||
status, KeyCape HTTP status, JWT login) and, for a source failure, both
|
||||
operator revoke flags. `ok: false` means a revoke-self did not succeed; the
|
||||
token stays TTL/use-bound (5m/4 uses, engine token per platform role). Record
|
||||
the JSON line only; never a value, token or full accessor.
|
||||
|
||||
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, revocation, rollback
|
||||
|
||||
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.
|
||||
Platform-owned (plan §3.5): roll-forward KV writes with `cas=<current>`,
|
||||
event-driven plus 90 days, then KeyCape restart. The consumer needs no action:
|
||||
the next command reads the latest version. Exchanges fail closed (401) between
|
||||
the KV write and the KeyCape restart. On compromise KeyCape/NetKingdom disable
|
||||
the registration first. secrets-engine can neither write nor rotate this entry
|
||||
and never reads an old version.
|
||||
|
||||
## 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.
|
||||
No live OIDC login, KV read, revoke-self, KeyCape exchange or JWT login has
|
||||
run. The operator role, policy, KV entry and JWT contract file do not exist
|
||||
yet as far as this repository knows. `bao login -no-store -format=json`
|
||||
output shape for OIDC is taken from the CLI contract and is first exercised by
|
||||
the founder run above.
|
||||
|
|
|
|||
|
|
@ -12,17 +12,21 @@ the non-cryptographic contract before any future OpenBao login:
|
|||
and renewal when no more than three minutes remain;
|
||||
- no ID token, refresh token, implicit retry provider, or fallback identity.
|
||||
|
||||
The confidential-client secret must be an explicitly selected mode-0600 file
|
||||
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).
|
||||
Since 2026-10-06 (founder direction; ops-mason plan
|
||||
`keycape-secrets-engine-openbao-client-custody`) the `secrets-engine-openbao`
|
||||
client secret has **no consumer file**. Each command that selects
|
||||
`service-jwt` reads it in memory from OpenBao (`platform/workloads/keycape/secrets-engine-openbao`,
|
||||
field `CLIENT_SECRET`) through one attended `bao login -method=oidc -no-store`
|
||||
on role `keycape-secrets-engine-openbao-operator-workload-kv-read`, with the
|
||||
token captured from a pipe, used only in a request header, and revoked with
|
||||
`revoke-self` right after the read. Ambient `BAO_TOKEN`/`VAULT_TOKEN` and the
|
||||
token helper are never used, and `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE`
|
||||
is refused for this profile. The value is sent only with HTTP Basic
|
||||
authentication and never placed in a request body, command argument, env,
|
||||
file, evidence record, or object representation.
|
||||
`secrets-engine service-auth probe-openbao [--json]` runs the whole chain and
|
||||
prints value-free evidence. Coordinates, first-use command and lifecycle:
|
||||
[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
|
||||
|
|
@ -42,7 +46,8 @@ Canonical provider contract:
|
|||
## Approval identity is separate
|
||||
|
||||
[Approval service authentication](approval-service-auth.md) uses its own
|
||||
client-secret input, resource audience, exact tenant:platform and per-request
|
||||
client-secret file (`SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE`, checked by
|
||||
`service-auth check-secret-file`), resource audience, exact tenant:platform and per-request
|
||||
read/consume scopes. Never put that credential into the OpenBao provider.
|
||||
Both providers now validate KeyCape’s actual `level`/`methods` assurance shape;
|
||||
the old `aal`/`method` fixture was incompatible with the issuer.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue