secrets-engine/docs/keycape-openbao-consumer-custody.md
tegwick 1732c419d2
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Reflect the published service-JWT login contract and proven login
SCOPE, layer.yaml and the custody doc said the platform JWT role was
unpublished. The role is applied, its consumer contract is published
(railiance-platform 21b8af3) and login was proven on 2026-10-10. The role
is login-only, so engine actions still use the named providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 544698@bnt-lap001
Assistant-Session: 279b8520-335f-4041-ba2a-9491335473db
2026-10-10 12:41:16 +02:00

133 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# KeyCape `secrets-engine-openbao` client secret: consumer custody
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.
## Direction
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.
## Coordinates (non-secret)
| 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=http://127.0.0.1:18200` (operator tunnel; bao.coulomb.social retired 2026-09-15), `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>` |
The mount, path, field and role are pinned in
`service_auth.OpenBaoOidcClientSecretSource`; any other value is refused.
## What the code does (per command that selects `service-jwt`)
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.
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.
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`.
## First live use (founder-run, attended)
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 role applied (CCR-2026-0038, applied
2026-10-08). This is the evidence step for KEY-WP-0037-T04 / RPF-WP-0035-T02 and
plan verification item 6.
CCR-2026-0038 publishes the consumer contract file only after the login proof
passes, so the probe cannot wait for it. Write the non-secret probe input
locally from the applied CCR values first:
```bash
mkdir -p ~/.secrets-engine/keycape && (umask 077; cat > ~/.secrets-engine/keycape/openbao-jwt-login.yaml <<'YAML'
mount: keycape-services
role: secrets-engine-openbao
bound_issuer: https://kc.coulomb.social
YAML
)
```
The proof passed on 2026-10-10
(`docs/evidence/2026-10-10-keycape-openbao-probe.json`). railiance-platform then
published the consumer contract as `docs/contracts/secrets-engine-openbao-jwt-login.yaml`
(21b8af3). That contract is identical to this probe input, so the local file
stays as it is.
```bash
cd /home/worsch/secrets-engine && \
env -u BAO_TOKEN -u VAULT_TOKEN -u SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE \
BAO_ADDR=http://127.0.0.1:18200 \
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
```
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:
```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>"}
```
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.
## Rotation, revocation, rollback
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 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.