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

180 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<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 `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 -- <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](../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 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](../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/<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.