--- id: KEY-WP-0006 type: workplan title: "Client credentials and service-token issuance" domain: infotech repo: key-cape status: finished owner: codex topic_slug: netkingdom created: "2026-07-27" updated: "2026-07-27" state_hub_workstream_id: "d8d20d8f-61e0-54e2-9a88-a3958f0b6f2d" --- # KEY-WP-0006 - Client credentials and service tokens Implement the missing service-principal half of the IAM Profile so `KEY-WP-0004` can issue a least-privilege identity to `rapp-qonto`. ## T01 - Model confidential service clients ```task id: KEY-WP-0006-T01 status: done priority: high state_hub_task_id: "993913a7-f951-50ee-9b10-d5b9e67b523c" ``` Extend static client registration with an explicit service subject, tenant, roles, and an environment-backed secret reference. Refuse startup when a `client_credentials` client is public, lacks its identity fields, or cannot resolve its secret. Never serialize or log the resolved secret. 2026-07-27: Added explicit service subject, tenant, roles, and in-memory-only resolved secret fields. Configuration validation rejects public or incomplete service clients, and startup resolves only `env:` secret references while reporting the variable name—not its value—on failure. ## T02 - Implement secure client_credentials exchange ```task id: KEY-WP-0006-T02 status: done priority: high state_hub_task_id: "801a6d55-2f91-51b1-88e4-cc7f0867238d" ``` Accept confidential client authentication through HTTP Basic, compare secrets in constant time, restrict requested scopes to the registered allowlist, and issue a short-lived access token with `principal_type=service`, tenant-specific subject/roles, empty groups, `aal1` client-secret assurance, and optional cached `tenant_roles`. Do not issue an ID token for this non-human grant. 2026-07-27: Implemented HTTP Basic confidential-client authentication with fixed-length SHA-256 constant-time comparison, allowlisted scopes, short-lived service claims, optional tenant-role cache, token-issued telemetry, and no ID token. ## T03 - Prove positive and negative conformance ```task id: KEY-WP-0006-T03 status: done priority: high state_hub_task_id: "e5c2a8b1-0879-57fa-a197-250a34c75bdb" ``` Cover valid exchange, unknown client, public client, missing/wrong secret, unsupported grant, excess scope, tenant isolation, telemetry redaction, and discovery metadata. Run the full Go build, vet, and test suite. 2026-07-27: Added positive service-claim and negative wrong-secret/excess-scope tests, updated discovery to advertise only implemented grants/auth methods, and verified `go test ./...`, `go vet ./...`, and `go build ./...` across the module. ## T04 - Provision and verify rapp-qonto ```task id: KEY-WP-0006-T04 status: done priority: high state_hub_task_id: "31af6b8c-96ff-5060-87db-3803b9a9dd12" ``` Generate the client secret without disclosure, store it through the approved OpenBao lane, register the static `rapp-qonto` client, deploy KeyCape, and verify a real exchange yields only the documented Binky service claims. Publish non-secret evidence to `KEY-WP-0004-T03` through T05. 2026-07-27: Provisioned the confidential client secret at `platform/workloads/rapp-qonto/keycape-client`, injected it into `sso/keycape-rapp-qonto-client`, registered `rapp-qonto-client`, and deployed image `main-e877d27-2` on railiance01. A real exchange produced only `sub=rapp-qonto`, `tenant=tenant:friendly:binky`, `principal_type=service`, `roles=[qonto-reader]`, empty groups, and `scope=qonto:read`; excessive scope and a wrong secret were denied. Public DNS still resolves `kc.coulomb.social` to the older CoulombCore endpoint, so the deployment proof used TLS-preserving direct resolution to railiance01 (`92.205.62.239`). ## T05 - Closure review ```task id: KEY-WP-0006-T05 status: done priority: low state_hub_task_id: "8c72d7d9-4086-5e64-b939-a12bff076124" ``` Close after T01-T04 pass and the repeatable verification path is documented. 2026-07-27: Closure review passed. Build, vet, unit/profile tests, live positive claims, negative authentication/scope checks, and discovery metadata all passed. The stale public DNS target is handed off as routing work rather than weakening the service-token contract.