railiance-platform/docs/credential-lane-designs/keycape-approval-clients.md
tegwick 8f40d73d0f
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
docs(identity): record verified live upstream issuer and completed cleanup
Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-08 23:50:55 +02:00

9.4 KiB
Raw Blame History

KeyCape approval-engine client custody admission

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.

Nothing below is an activation. Both CCRs are proposed; no value has been generated, no KV version written, no manifest applied. Source preparation is not live completion.

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.

Manifests are written and client-validated but unapplied: 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. The configuration owner must still ensure authelia.issuer is pinned to that exact value before this window. The probe left normal configuration unchanged. Keep the existing-human-login regression in the 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.

Date is not set here. It depends on the founder's availability, which is not mine to schedule. Propose a slot from 2026-09-10 and I will confirm the platform side; the window needs roughly 6090 minutes with both owners present.

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.