feat: exchange scoped approval service tokens per request
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 07:06:05 +02:00
parent 3a19069b4b
commit 7688445184
14 changed files with 859 additions and 35 deletions

View file

@ -154,7 +154,8 @@ did before. Production additionally needs:
| Variable | Meaning |
| --- | --- |
| `SECRETS_ENGINE_APPROVAL_URL` | approval-engine base URL (claim + consume) |
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | mode-0600 credential, outside Git |
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | explicit bearer credential outside Git; mutually exclusive with the client-secret provider |
| `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` | protected temporary KeyCape approval-client secret; fresh in-memory token per request ([contract](approval-service-auth.md)) |
| `SECRETS_ENGINE_AUTHORIZATION_SUBJECT_ID` / `_SUBJECT_TYPE` | the acting principal |
| `SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE` / `_VERSION` | the live pin (step 2) |
| `SECRETS_ENGINE_PDP_URL` / `_PDP_TOKEN_FILE` | the per-consumer access-engine pin |

View file

@ -0,0 +1,83 @@
# Approval consumer identity
Implemented for SECRETS-WP-0009-T03 on 2026-09-09. KeyCape's live verifier
registration is accepted under KEY-WP-0013-T02 and CCR-2026-0017; this consumer
implementation does not grant retrieval of its confidential client secret.
The CLI exchanges a separately supplied `secrets-engine-approval` client secret
for a fresh access token immediately before each approval HTTP request. Claim
requests ask for `approval:read`; consume requests ask for `approval:consume`.
The token exists only in memory for that request. There is no refresh token,
access-token file, background renewal, retry loop, or fallback identity.
| Input | Value |
| --- | --- |
| `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` | Explicit, protected temporary file outside Git, no group/other access |
| `SECRETS_ENGINE_KEYCAPE_ISSUER` | `https://kc.coulomb.social` |
| `SECRETS_ENGINE_KEYCAPE_TOKEN_URL` | `https://kc.coulomb.social/token` |
| `SECRETS_ENGINE_APPROVAL_URL` | Admitted HTTPS endpoint or literal loopback owner tunnel |
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | Unset when using the client-secret provider |
The existing token-file provider remains explicitly selectable by configuring
only `SECRETS_ENGINE_APPROVAL_TOKEN_FILE`. Configuring both providers fails
before either credential is read. An exchange failure never reads that file,
`BAO_TOKEN`, or `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` (the different
OpenBao identity's credential).
The approval profile fixes client `secrets-engine-approval`, audience
`approval-engine`, subject `service:secrets-engine`, tenant `tenant:platform`,
role `secrets-engine`, service principal, empty groups and one requested scope.
It enforces RS256 shape, at most 900 seconds of life, strict expiry and at most
30 seconds of future issue time. The KeyCape assurance shape is
`level: aal1`, `methods: [client_secret]`, `mfa: false`, `source: key-cape`.
The same wire-shape correction applies to the existing OpenBao exchange; its
client, audience, tenant and authorization remain separate.
Claim parsing is a preflight, not signature verification or authorization.
Approval Engine verifies signature, issuer, audience and claims against JWKS.
The existing exact-action claim, access-engine decision and atomic consume gates
still precede a production OpenBao operation.
Credentials are sent through a non-redirecting transport. The KeyCape endpoint
must be the configured HTTPS issuer's `/token`. The new consumer refuses
cluster Service DNS from this workstation and plaintext non-loopback approval
endpoints. A literal loopback address alone cannot identify the responder: the
operator must establish a `kubectl port-forward` for the admitted cluster and
named Approval Engine pod. No tunnel or cluster deployment is created here.
## Reproducible component exercise
Run from this repository with Docker available and the owner source checkouts:
```bash
uv run --with 'PyJWT[crypto]>=2.7,<3' python tools/exercise_approval_identity.py \
--keycape-source /home/worsch/key-cape \
--approval-engine-source /home/worsch/approval-engine \
--receipt /tmp/<new-metadata-receipt>.json
```
The exercise starts the immutable admitted KeyCape image behind local HTTPS,
uses synthetic signing keys/client secrets, and serves the real Approval Engine
source with its JWT/JWKS verifier and SQLite store. It drives the actual Secrets
Engine production gate, with only the PDP represented by a sequencing double.
Operator create/approve, consumer claim/consume, same-digest retry, different
digest, spent claim, wrong action, wrong secret and operator-scope denial are
checked. All temporary processes and synthetic credentials are removed.
This exercise caught the previous fixture-only `aal`/`method` assumption:
the real issuer has always emitted `level`/`methods`. The regression suite now
refuses that obsolete shape. See
[the component receipt](evidence/2026-09-09-approval-identity-exercise.json).
## Live handoff
RPF-WP-0035-T06 owns the separate client-side read admission for the existing
`platform/workloads/secrets-engine/approval-client`, field `CLIENT_SECRET`.
The admission must name the actual consumer/placement, bounded reader, protected
temporary delivery and cleanup, refusal/revocation checks and lifecycle owner.
Do not use the verifier's `sso` Secret or reseed version-1 custody.
Approval Engine's operator is a separate identity and read lane. Audit receiver
and sender custody remains AUDIT-WP-0009-T09 / APPROVAL-WP-0002-T01. The live
Approval Engine deployment and native OpenBao delivery remain open. Synthetic
acceptance grants no production access, action approval or model spending.

View file

@ -39,6 +39,8 @@ secrets-engine --version
| `SECRETS_ENGINE_CATALOG` | `./catalog` | catalog directory |
| `SECRETS_ENGINE_EVIDENCE` | `./.evidence` | local non-secret evidence log |
| `SECRETS_ENGINE_UNSAFE_DEMO` | _(unset)_ | allow a prod-labeled lane only when Hub is disabled and OpenBao is loopback; throwaway demos only |
| `SECRETS_ENGINE_APPROVAL_CLIENT_SECRET_FILE` | _(unset)_ | separate temporary approval-client secret; [per-request exchange](approval-service-auth.md) |
| `SECRETS_ENGINE_APPROVAL_TOKEN_FILE` | _(unset)_ | explicit approval bearer file; conflicts with approval client-secret provider |
| `SECRETS_ENGINE_KEYCAPE_TOKEN_URL` | _(unset)_ | KeyCape token endpoint for `service-jwt` |
| `SECRETS_ENGINE_KEYCAPE_ISSUER` | _(unset)_ | KeyCape issuer; must match the JWT login contract |
| `SECRETS_ENGINE_KEYCAPE_CLIENT_SECRET_FILE` | _(unset)_ | mode-0600 out-of-repo client secret |

View file

@ -0,0 +1,34 @@
{
"schema_version": 1,
"target": "disposable local processes; synthetic credentials",
"started_at": "2026-09-09T01:49:09.187005+00:00",
"keycape_image": "forgejo.coulomb.social/coulomb/key-cape@sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611",
"approval_engine_commit": "b46b0f26669dc83c944ee5145426bad03d5ef720",
"keycape_contract_commit": "0f5535eed95f1223c83a28f5a0bd6fa594cecae8",
"consumer_source_sha256": {
"approval_auth.py": "6f3b033e7928e1c527bc19f5a1e01bf243540769a8646bd8cd47268da25637b5",
"approval_consume.py": "cd79b3e4534a5abf55af2b5ab2939c485a3140a1174f593e0aa7719250051d92",
"config.py": "2f2f1b60664d89bbb806aed0c768378923062077740e1f96e4c359238eb06955",
"service_auth.py": "653723ef5babce2157771416d93cb15cdbb7caca3c45509c3730520c55e4515c"
},
"limitations": [
"PDP sequencing double",
"local Approval Engine source, not deployed image",
"no live custody or client-side read grant",
"no OpenBao effect or model execution"
],
"checks": {
"operator_issued_and_approved_via_verified_jwt": true,
"wrong_action_refused_before_consume": true,
"actual_consumer_claim_check_consume": true,
"same_digest_retry_idempotent": true,
"different_digest_refused": true,
"spent_claim_refused": true,
"operator_consume_scope_denied_by_issuer": true,
"wrong_secret_refused": true,
"no_access_token_file_created": true
},
"status": "passed",
"cleanup_complete": true,
"finished_at": "2026-09-09T01:49:11.402008+00:00"
}

View file

@ -31,3 +31,11 @@ those named providers and `--auth service-jwt` fail-closes. See
Canonical provider contract:
`key-cape/docs/openbao-service-auth-contract.md` (reviewed 2026-08-23).
## Approval identity is separate
[Approval service authentication](approval-service-auth.md) uses its own
client-secret input, resource audience, exact tenant:platform and per-request
read/consume scopes. Never put that credential into the OpenBao provider.
Both providers now validate KeyCapes actual `level`/`methods` assurance shape;
the old `aal`/`method` fixture was incompatible with the issuer.