Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
9.4 KiB
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_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
ssostore (openbao-sso-user-engine-runtime) andopenbao-rapp-qontouse AppRole for cutover reasons. New same-cluster lanes use Kubernetes auth againstexternal-secrets/external-secrets, asopenbao-core-hub-runtimedoes, 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 --execenvelope, 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:
- Both CCRs approved by platform-operator and the KeyCape owner.
- KeyCape image that reads both environment names is built and pinned, not
deployed.
main-153258bis not that image. - Founder available for the attended OpenBao session.
- 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 ensureauthelia.issueris 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:
- Apply the two policies and the two Kubernetes auth roles (attended, via the authority in (c)).
- Seed both KV paths, CAS=0, with the client secrets KeyCape mints. Record metadata-only receipts.
- Apply the two ClusterSecretStores; confirm both reach
Valid. - Apply the two ExternalSecrets; confirm
SecretSyncedand that both Secrets exist with keyclient-secret. No value is printed at any point. - Roll out the KeyCape image.
- KeyCape runs its verification: live JWKS signature, exact claim bindings
(subject, tenant
tenant:platform, audienceapproval-engine, 15m lifetime), and denial of excess scopes — operatorconsumeand humanconsume— without logging any value or token. - Platform runs its negative checks: cross-path read denied, wrong service account denied, out-of-namespace store use denied, parent listing denied.
- 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-clientSecret into ESO. - No change to the separate
codex-railiance-platformJWT issuance contract (KEY-WP-0009-T04) or to RPF-WP-0035-T02's service-JWT lane. - No approval for
approval:consumeon 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.