secrets-engine/docs/approval-service-auth.md
tegwick 7688445184
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
feat: exchange scoped approval service tokens per request
Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-09 07:06:05 +02:00

4.6 KiB

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:

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.

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.