# KeyCape service authentication for OpenBao Date: 2026-08-23 Status: accepted provider contract; live client provisioning remains a deployment and custody operation ## Ownership boundary KeyCape owns static confidential-client registration, client authentication, JWT signing, IAM Profile service claims, expiry, and disabling future issuance. The OpenBao platform owner owns JWT auth mounts, exact-bound roles, policies, OpenBao token TTL/use limits, and value-safe authorization evidence. The consumer owns renewal and fail-closed use. OpenBao or its approved delivery path owns confidential-client secret custody. KeyCape accepts the `codex-railiance-platform` issuance identity and the `secrets-engine-openbao` service identity under this contract. It does not own the resulting OpenBao identity token or policy. ops-warden remains the SSH certificate issuer only, and user-engine is not in the machine-principal path. ## Exchange and claims The consumer sends `POST /token` with `grant_type=client_credentials`, HTTP Basic client authentication, and the allow-listed `scope=openbao:login`. KeyCape returns an RS256 access token and no ID token or refresh token. Every issued service token contains: | Claim | Contract | | --- | --- | | `iss` | the configured KeyCape issuer | | `sub` | the registration's exact `serviceSubject` | | `aud` | the exact `clientId` | | `iat`, `exp` | issue and expiry time; maximum per-client lifetime below | | `principal_type` | `service` | | `tenant` | the registration's exact tenant | | `roles` | the registration's allow-listed roles | | `groups` | an empty array | | `scope` | only requested scopes from the registration allow-list | | `assurance` | AAL1, `client_secret`, non-MFA, source `key-cape` | OpenBao JWT roles must bind the exact issuer, audience, subject, `principal_type`, tenant, and required role. A consumer must reject an issuer or claim mismatch before attempting OpenBao login. ## Accepted registrations The source-of-truth non-secret fragments are in `config/service-clients.example.yaml`. | Client | Subject | Role | Scope | Maximum JWT lifetime | | --- | --- | --- | --- | --- | | `codex-railiance-platform` | `service:codex:railiance-platform` | `coding-agent` | `openbao:login` | 15 minutes | | `secrets-engine-openbao` | `service:secrets-engine` | `secrets-engine` | `openbao:login` | 15 minutes | The confidential secret is referenced as an environment variable in KeyCape configuration and must be materialized without entering Git, State Hub, chat, process arguments, or normal logs. Registering or rotating the live value is a separate approved custody action. ## Renewal, expiry, and revocation - There is no refresh token. Obtain a new KeyCape JWT with a new `client_credentials` exchange when no more than three minutes remain. - A consumer may retry a transient exchange failure with bounded backoff only while its current JWT is still valid. It must stop the privileged operation at expiry. - KeyCape JWTs are self-contained and cannot be recalled after issue. Disabling the client or rotating its secret stops new issuance; the residual exposure is bounded by the 15-minute JWT lifetime and the OpenBao token's independent TTL/use limits. - After OpenBao login, the consumer must explicitly revoke the OpenBao token in its cleanup path. A cleanup failure is actionable evidence and must not print a JWT, OpenBao token, client secret, or token accessor. ## Failure semantics Authentication, scope, claim, signature, issuer, expiry, OpenBao login, and cleanup failures are fail-closed. Steady-state consumers must not implicitly fall back to bootstrap token files, an operator identity, a broader AppRole, or an unbound JWT role. Bootstrap files remain explicitly selected setup or break-glass providers and require their own authorization and evidence. Verification must inspect metadata and capabilities without reading a secret value. A coding-agent check must prove the `agent-high-risk-boundary` deny wins against any otherwise readable workload policy.