id: CCR-2026-0017 kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: KeyCape verifier custody for the secrets-engine-approval confidential client status: proposed created: '2026-09-08' updated: '2026-09-08' requester: agent: claude reason: >- KEY-WP-0013-T02 (State Hub message 278a3ebe-b529-49f6-bd1a-e3ebcf318260) asks railiance-platform to admit custody for two confidential client_credentials registrations. This CCR covers the first: client secrets-engine-approval, subject service:secrets-engine, audience approval-engine, tenant:platform per resolved decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6, 15m token lifetime. KeyCape owns the registration and issuance; this request establishes only the custody path and the KeyCape-side delivery of the client secret it must verify presented credentials against. It does not admit the client-side lane by which secrets-engine would read its own copy — that is a separate request with a separate consumer gate. review: required: true required_approvers: - platform-operator - key-cape-owner comments: - at: '2026-09-08' reviewer: railiance-platform (codex/claude) decision: paths_confirmed_field_corrected comment: >- KV path platform/workloads/secrets-engine/approval-client is confirmed unchanged: it matches the platform/workloads// convention enforced by scripts/credential-change.py and used by every admitted platform lane. The proposed field name client_secret is corrected to CLIENT_SECRET; KV field names are uppercase by convention (PROVISIONER_TOKEN, CORE_HUB_API_TOKEN) and lowercase names fail FIELD_NAME_RE validation. The Kubernetes Secret key client-secret and the KeyCape environment name are confirmed as proposed. target: domain: financials tenant: platform workload: secrets-engine environment: production purpose: >- Hold the secrets-engine-approval confidential client secret in platform custody and project it into the KeyCape runtime so KeyCape can verify presented client_credentials without the value living in Git, a chart value, or a hand-created Kubernetes Secret. openbao: mount: platform kv_path: platform/workloads/secrets-engine/approval-client fields: - CLIENT_SECRET policy_name: workload-kv-read-keycape-secrets-engine-approval policy_file: openbao/policies/workload-kv-read-keycape-secrets-engine-approval.hcl auth: method: kubernetes mount: kubernetes role: external-secrets-keycape-secrets-engine-approval bound_claims: service_account_names: - external-secrets service_account_namespaces: - external-secrets bound_claims_confirmed: true policies: - workload-kv-read-keycape-secrets-engine-approval ttl: 15m access_frontdoor: type: ops-warden catalog_id: keycape-secrets-engine-approval-client selector: KeyCape secrets-engine-approval confidential client secret command: warden access keycape-secrets-engine-approval-client --fetch CLIENT_SECRET resolvable: false readiness: pending-review delivery: surface: external-secrets target: >- ClusterSecretStore openbao-keycape-secrets-engine-approval, limited to namespace sso, to ExternalSecret sso/keycape-secrets-engine-approval-client and Secret sso/keycape-secrets-engine-approval-client with key client-secret. KeyCape resolves it as KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET through a secretKeyRef, matching the live KEYCAPE_RAPP_QONTO_CLIENT_SECRET shape observed on image main-153258b. Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstore.yaml and keycape-approval-clients.externalsecrets.yaml. risk: classification: high notes: - >- This is a verifier-side copy. KeyCape holding the client secret lets it authenticate the client; it does not let KeyCape act as the client, but a leak of this value allows anyone to present as secrets-engine for approval:read and approval:consume until the registration is disabled. - >- Custody path is under the client's own workload prefix, not KeyCape's, because the credential belongs to the secrets-engine client identity. The reader admitted here is the External Secrets Operator on behalf of the sso namespace only; a future secrets-engine-side read is a separate lane and separate policy. - >- The existing keycape-rapp-qonto-client Secret is hand-created and not ESO-managed. Do not extend this store or ExternalSecret to adopt it; that is a distinct migration with its own owner. - >- The two approval clients are deliberately kept on separate policies, roles and stores so the operator client can be revoked without disturbing this one. Their scope sets differ; a shared policy would erase that boundary. - >- Compromise response is KeyCape disabling the client registration plus rotation of this KV version. An OpenBao token expiry does not invalidate an already issued client secret. verification: positive: - >- The ExternalSecret in namespace sso syncs CLIENT_SECRET to Secret key client-secret without printing the value. - >- The KeyCape build that reads KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET starts and issues a token for subject service:secrets-engine with audience approval-engine, tenant:platform, 15m lifetime, verified against live JWKS signature. - Exact claim bindings are asserted by KeyCape without any value or token logged. negative: - >- A namespace outside the approved ClusterSecretStore condition cannot use this store to read the path. - >- A service account outside external-secrets/external-secrets cannot authenticate through role external-secrets-keycape-secrets-engine-approval. - >- The role cannot read the sibling approval-engine operator-client path, any parent listing, or any other platform workload path. - >- The secrets-engine-approval client is denied scopes outside approval:read and approval:consume, and human identities are denied approval:consume. activation_conditions: - >- KeyCape and railiance-platform agree the single attended rollout window in docs/credential-lane-designs/keycape-approval-clients.md; the KeyCape image that reads both environment names is built and pinned but not yet deployed. - >- Attended first provision runs only through the governed openbao-platform-admin-login lane (founder_required, attended OIDC via netkingdom role=platform-admin), with a unique receipt path and no value in chat, command arguments or shell history. - >- Policy, Kubernetes auth role and ClusterSecretStore applied before the ExternalSecret; sync confirmed before the KeyCape image is rolled out. - >- Positive and negative results recorded with non-secret request ids or timestamps. evidence: [] lifecycle: deactivate: >- KeyCape disables the secrets-engine-approval registration; platform detaches the policy from role external-secrets-keycape-secrets-engine-approval and removes the ExternalSecret. The KV version is retained until KeyCape confirms disablement. rotate: >- KeyCape mints a replacement client secret; platform writes the new KV version under the same attended authority; ESO refresh delivers it and KeyCape restarts or re-reads. Rotation is independent of the operator client. compromised: >- Disable the registration at KeyCape first (that is what stops token issuance), then rotate the KV version and confirm no other namespace consumed the store. state_hub: workplan_id: RPF-WP-0035 task_id: RPF-WP-0035-T05 related_message: 278a3ebe-b529-49f6-bd1a-e3ebcf318260 related_decision: 5ed3fb35-eca9-413a-82b9-95171ba85bf6