# 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](../../credential-change-requests/CCR-2026-0017-keycape-secrets-engine-approval-client.yaml), [CCR-2026-0018](../../credential-change-requests/CCR-2026-0018-keycape-approval-engine-operator-client.yaml). 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//` 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_SECRET` → `ExternalSecret secretKey client-secret` → `secretKeyRef` → 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: ```bash warden access openbao-platform-admin-login --exec -- ``` 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](../evidence/2026-09-08-keycape-upstream-issuer-proof.json). 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 60–90 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](../evidence/2026-09-08-keycape-approval-custody-preflight.json). For a repeat after a role/policy change, use the existing contained WSL wrapper: ```bash python3 scripts/openbao-attended-exec.py -- /usr/bin/python3 \ scripts/keycape-approval-custody-preflight.py --receipt /tmp/.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.