net-kingdom/security-case-types/oidc-service-client/README.md
tegwick 5d5f15df55
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0056-T14 done: SecurityCaseType oidc-service-client 0.1 (proposed)
Confidential client_credentials service clients: one audience, closed
resource scopes, fixed service subject and tenant, at most 15m; secret minted
in custody, read only by KeyCape (ESO) and the consumer's own bound
Kubernetes-auth role (no Secret copy). First case nk-bind-executor-approval
(approval-engine create/read/consume as service:nk-bind-executor). Capability
net-kingdom.keycape-service-client-registration; keycape-client.py renders
service clients in key-cape's shape (adoption of secrets-engine-approval
reproduces key-cape's record). Acknowledgement memo r1 validated against
informed-decision intake and State Hub dispatcher; hub decision f8044f81.
Test fixture's synthetic signing key is now computed, not a literal (secret
scan generic-api-key).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
2026-10-31 21:30:21 +01:00

7.1 KiB
Raw Blame History

oidc-service-client 0.1

An internal workload obtains short-lived KeyCape access tokens for one resource server with the client_credentials grant. The type covers:

  • the client registration: audience, resource scopes, service subject, tenant, roles and lifetime;
  • the client secret, minted straight into OpenBao custody and readable only by KeyCape (through ESO) and by the consuming workload (through its own Kubernetes-auth login).

Canon: canon/standards/security-case-types_v0.1.md. Status: proposed (2026-10-31): complete, tests green, Decision Memo prepared (decision/, hub decision f8044f81). Next: acknowledged by the founder through informed-decision. Its first case and reference instance is the bounded executor's approval client (NK-WP-0056-T14, examples/nk-bind-executor-approval.yaml).

Purpose

KeyCape supports confidential client_credentials clients (src/internal/config/validate.go: confidential, env: secretRef, serviceSubject and tenant required, no redirect URIs, lifetime 1m–1h). Every such client so far was registered by its own script:

  • sso-mfa/k8s/keycape/approval-clients-rollout.py (secrets-engine-approval, approval-engine-operator);
  • secrets-engine-openbao-client-rollout.py.

Each script hard-codes its client. The browser type (oidc-browser-relying-party) names service clients with a resource audience, tenant and custom scopes as needing "a separate type". This is that type.

Its first case is driven by a live need. approval-engine accepts POST /v1/approvals only when binding.actor equals the token's sub, so the bounded executor must hold its own client to file the approval it later consumes.

Reference instances and fit

Client Fits 0.1 Note
nk-bind-executor-approval (new) yes first case
secrets-engine-approval yes adoption would not change its live registration (canon section 9)
approval-engine-operator partly its scope list is wider than any single workload should hold; adopt only after review

Threat model

Assets:

  • the client secret, which lets its holder obtain tokens as service_subject;
  • the tokens, which a resource server treats as the workload's identity: for approval-engine, the right to create approvals with that actor and to consume them.

Actors:

  • an anonymous network caller;
  • another workload in the same namespace;
  • a coding agent;
  • an operator;
  • a compromised consuming workload.

Controls:

  • The secret is generated inside custody (mint-secret, generated_in_custody). It is never returned, never written by hand and never in Git.
  • Only two readers exist: KeyCape through the ESO role, and the consumer through its own role. The consumer role is bound to one ServiceAccount in one namespace with audience openbao, and its policy reads one path. No metadata, list, parent or sibling access.
  • There is no Secret copy in the consumer's namespace. The consumer reads natively per call, so a pod in that namespace without the ServiceAccount has nothing to mount.
  • Tokens last at most 15m and there is no refresh token.
  • Scopes are a closed list per case. KeyCape refuses an excess scope (invalid_scope), and the type verifies that refusal.
  • The subject and tenant are fixed by the registration. The workload cannot choose them.
  • What a compromised consumer can do is bounded by its scopes at one audience. For approval-engine that means:
    • it can create approvals naming itself as actor, which still need a human entry under human_control;
    • it can consume approvals of its tenant, which APPROVAL-WP-0007-T02 actor_consumes narrows.

Alternatives considered

  • Keep per-client scripts. Each repeats the custody, merge and verification logic, and each needs its own attended founder run. This is the pattern the security case canon replaced for browser clients.
  • Extend oidc-browser-relying-party. The two shapes differ in grant, required fields, the meaning of sub, and how the consumer receives the secret. A combined type would carry parameters that are forbidden in one mode (KeyCape rejects serviceSubject on browser clients). It would be a looser validation, which is a major change by canon section 6.
  • ESO-deliver the secret into the consumer's namespace (as the browser type does). Rejected for service clients:
    • a long-lived Secret copy beside the workload is readable by anything that can mount Secrets there;
    • the bounded executor's lanes already read natively per call (CCR-2026-0058 and CCR-2026-0059).
  • A static bearer instead of a client. approval-engine expects KeyCape-issued tokens with aud, sub, tenant and assurance. A stored bearer would bypass expiry and the subject binding.

Attended steps

bind-env and register-client change the live identity provider: the KeyCape Deployment and its client registry, which is a Kubernetes Secret. Agents do not read or write Secrets. Both steps run in one attended operator session, bind-env first, because KeyCape resolves a confidential client's secret at start and fails closed without it. The restart drops in-flight logins for every client, so runs go in a quiet window. mint-secret and deliver-secret are unattended.

Lanes, value handling and gates

Step Lane Value handling
mint-secret keycape-client-secret-generate (railiance-platform broker) generated in custody, never returned
deliver-secret ESO role external-secrets-keycape-<client_id> none
bind-env, register-client key-cape-oidc-login (attended) none
hand-over the consumer's own deployment path none; the consumer reads through workload-kv-read-keycape-<client_id>-<consuming_service>

Known limits

  • One audience per case. A workload that calls two resource servers holds two cases.
  • Scopes are not checked against the audience's own scope list. The type admits any resource:verb shape; the audience owner reviews the case. A stricter check would need the audience to publish its scopes.
  • KeyCape's client registry is still a Secret. Registration stays attended until KeyCape loads clients from a non-secret registry.
  • Rotation is mint-secret, then the consumer's next exchange. KeyCape reads the secret at start, so a rotation also restarts KeyCape. That is not automated.
  • Adopting approval-engine-operator needs a scope review first; see the reference table.

Verification and rollback

Verification never reads the secret or a token:

  • the consumer reports the claims of its own exchanges (sub, aud, tenant, scope and lifetime);
  • an excess scope is refused;
  • a default identity has no access to custody;
  • the consumer role admits only its ServiceAccount and audience;
  • the consumer policy grants read on the one path;
  • ESO delivery to KeyCape reports Ready.

Rollback reverses every follow-through step and every materialized item in dependency order: consumer configuration, registration, env binding, delivery, custody, roles before their policies, then the catalog entry.

History

  • 0.1 (2026-10-31): first version, for NK-WP-0056-T14; proposed with memo nk-wp-0056-t14-oidc-service-client-r1.