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

7.3 KiB
Raw Permalink Blame History

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:

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.

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:

{"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.