railiance-platform/docs/credential-lane-designs/keycape-approval-clients.md
codex c6dc4286e2
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
feat: verify live KeyCape custody and preserve versions on resume
Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-09 02:18:12 +02:00

10 KiB

KeyCape approval-engine client custody admission

2026-09-09 accepted: verifier custody and the compatible image/config are live; both service verifiers and a fresh existing-human OpenBao login passed. Both CCRs are verified. Live evidence supersedes the preparation status below. Client-side reads and factory operating grants remain separate.

Answer to KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260, KeyCape packet key-cape: docs/approval-engine-provisioning-request.yaml). Tracked here as RPF-WP-0035-T05. Requests: CCR-2026-0017, CCR-2026-0018.

Both CCRs are verified following explicit user approval in both reviewer roles. Version-1 custody, ESO delivery, the compatible KeyCape rollout and the fresh existing-human login all passed. The contract below defines the completed verifier-side scope; client-side reads remain a separate admission.

a) Custody paths and field names

Item Proposed by KeyCape Admitted Change
Client 1 path platform/workloads/secrets-engine/approval-client same confirmed
Client 2 path platform/workloads/approval-engine/operator-client same confirmed
Field client_secret CLIENT_SECRET corrected

Both paths are correct as proposed. They match the platform/workloads/<workload>/<lane> shape that scripts/credential-change.py enforces for the platform mount and that every admitted platform lane uses (platform/workloads/core-hub/runtime, platform/workloads/user-engine/runtime, platform/workloads/telemetry/grafana-admin). Naming each path under the client's own workload prefix rather than under KeyCape is also right: the credential belongs to the client identity; KeyCape is its verifier, not its owner.

The field name is the one correction. KV field names on this mount are uppercase (PROVISIONER_TOKEN, CORE_HUB_API_TOKEN, NPM_AUTH_TOKEN), and the CCR validator rejects anything that does not match ^[A-Z][A-Z0-9_]*$. So the OpenBao field is CLIENT_SECRET. This does not change the Kubernetes Secret key or the environment name — see below.

b) Kubernetes delivery references

Confirmed as proposed, with the mapping made explicit:

Client 1 Client 2
Secret sso/keycape-secrets-engine-approval-client sso/keycape-approval-engine-operator-client
Secret key client-secret client-secret
KeyCape env KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
ClusterSecretStore openbao-keycape-secrets-engine-approval openbao-keycape-approval-engine-operator
OpenBao policy workload-kv-read-keycape-secrets-engine-approval workload-kv-read-keycape-approval-engine-operator
Kubernetes auth role external-secrets-keycape-secrets-engine-approval external-secrets-keycape-approval-engine-operator

OpenBao CLIENT_SECRETExternalSecret secretKey client-secretsecretKeyRef → env. That shape is the one already running in sso: the live keycape pod on main-153258b resolves KEYCAPE_RAPP_QONTO_CLIENT_SECRET from secretKeyRef{name: keycape-rapp-qonto-client, key: client-secret}. Read-only observation 2026-09-08, and it confirms KeyCape's own statement that the image carries only that one client reference.

Both delivery manifests are applied and live-verified: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml and keycape-approval-clients.externalsecrets.yaml.

Three deliberate choices, so they are not read as accidents:

  • Two stores, two roles, two policies. The scope sets differ — the operator client can create, approve, revoke and supersede; the secrets-engine client can read and consume. Separate identities mean either can be revoked without touching the other. A shared store would erase that boundary.
  • Kubernetes auth, not AppRole. The neighbouring sso store (openbao-sso-user-engine-runtime) and openbao-rapp-qonto use AppRole for cutover reasons. New same-cluster lanes use Kubernetes auth against external-secrets/external-secrets, as openbao-core-hub-runtime does, so there is no AppRole secret to hold in custody.
  • Verifier side only. This admits the copy KeyCape needs to verify presented credentials. It does not admit the client-side lane by which secrets-engine or the approval-engine operator would read its own copy. That is a separate request with its own consumer gate, and no part of it is implied here.

creationPolicy: Owner means the ExternalSecret creates the Secret. Do not pre-create either Secret by hand.

c) Authority for the attended first provision

Your reading is right, and your refusal to act on the warden plan result was also right: a generic database-lane match is not authorization for this path.

The authority is the governed openbao-platform-admin-login lane — founder_required, attended OIDC through bao login -method=oidc -path=netkingdom role=platform-admin, invoked as:

warden access openbao-platform-admin-login --exec -- <command>

with a unique --receipt path. Nothing else in this repo carries write authority against platform/workloads/. Two constraints that come with it:

  • It is a founder-attended session. No agent, including this one, can execute it unattended, and neither KeyCape nor railiance-platform can self-authorize it.
  • The owner command is never run directly, only through the warden access --exec envelope, so output containment applies (RPF-WP-0017). No value in chat, command arguments, shell history or logs; receipts carry metadata only.

Custody writes are seeded with compare-and-set zero on first version.

d) Rollout window

Agreed in shape: one attended session, everything below in order, no partial landing. Registrations and the reading build deploy together because a KeyCape image that reads an absent env fails closed, and a delivered secret with no build to read it is custody without a consumer.

Prerequisites before the window opens:

  1. Both CCRs approved by platform-operator and the KeyCape owner.
  2. KeyCape image that reads both environment names is built and pinned, not deployed. main-153258b is not that image.
  3. Founder available for the attended OpenBao session.
  4. The actual signed issuer is verified as https://auth.coulomb.social. The admitted probe passed signature/audience/time/nonce verification on 2026-09-08 at 21:44:44 UTC, exited 0 and removed every temporary resource. Receipt. NetKingdom completed the exact authelia.issuer pin on 2026-09-09, with unchanged signing key and unrelated configuration; Secret revision 58713343. Pin receipt. This prerequisite is closed. Keep the existing-human-login regression in the separately admitted image/configuration rollout checks.

In-window order:

  1. Apply the two policies and the two Kubernetes auth roles (attended, via the authority in (c)).
  2. Seed both KV paths, CAS=0, with the client secrets KeyCape mints. Record metadata-only receipts.
  3. Apply the two ClusterSecretStores; confirm both reach Valid.
  4. Apply the two ExternalSecrets; confirm SecretSynced and that both Secrets exist with key client-secret. No value is printed at any point.
  5. Roll out the KeyCape image.
  6. KeyCape runs its verification: live JWKS signature, exact claim bindings (subject, tenant tenant:platform, audience approval-engine, 15m lifetime), and denial of excess scopes — operator consume and human consume — without logging any value or token.
  7. Platform runs its negative checks: cross-path read denied, wrong service account denied, out-of-namespace store use denied, parent listing denied.
  8. Both sides record evidence into the CCRs and a receipt into the workplan.

Rollback: revert the KeyCape image to main-153258b, delete the two ExternalSecrets, detach the policies from the roles. The KV versions are retained until KeyCape confirms whether the registrations stay; if they are abandoned, KeyCape disables the registrations first and platform then destroys the versions.

The attended window completed on 2026-09-09 under the user's explicit approval. Failed checks restored the compatible config/image and detached ESO delivery. The final resume preserved both initial KV versions and passed all service checks plus a fresh existing-human OpenBao login.

What is not admitted

  • No client-side (secrets-engine / approval-engine) read lane.
  • No adoption of the hand-created sso/keycape-rapp-qonto-client Secret into ESO.
  • No change to the separate codex-railiance-platform JWT issuance contract (KEY-WP-0009-T04) or to RPF-WP-0035-T02's service-JWT lane.
  • No approval for approval:consume on the operator client, now or by later scope edit. Adding it is a new lane decision.

Attended authority return — 2026-09-08

The metadata-only first-provision preflight is now observed on the exact six policy/role/data paths: create/update capabilities are present. The successful session self-revoked and its helper was removed. This settles the availability of the named operator authority, not the CCR approvals or the upstream issuer precondition. See the receipt.

For a repeat after a role/policy change, use the existing contained WSL wrapper:

python3 scripts/openbao-attended-exec.py -- /usr/bin/python3 \
  scripts/keycape-approval-custody-preflight.py --receipt /tmp/<new-private-receipt>.json

Use an absolute path to the owner command if invoking from another directory. It emits no output or values; inspect its allowlisted capability metadata only after Warden has completed revocation and cleanup. No custody activation or approval is performed by this preflight.

2026-09-09: the live issuer pin is complete. The next review is captured in keycape-approval-clients-review.md, with the resolved Hub decisions, both recorded reviewer roles and completed live verification.