OAuth 2.0 client credentials

Obtain JWT access tokens directly from an independent OAuth authorization server and exchange them for OpenBao tokens.

Updated 11 November 2026 · 6 min read

On this page

Set auth.jwt.source: oauth2 to obtain JWT access tokens with the OAuth 2.0 client credentials grant. The provider requests a new access token before each OpenBao JWT login. It keeps access tokens and OpenBao tokens in memory.

The authorization server, its database, DNS, routing, and TLS trust must work while the protected Kubernetes API is unavailable. Provision the client secret on each control-plane host through an independent process. Fetching it from a Secret in the protected cluster creates a startup and recovery dependency. A separate management cluster can host the issuer if it recovers independently.

Supported protocol

The integration uses OAuth 2.0 client credentials and has no provider-specific endpoint paths or realm configuration.

Setting or behaviorContract
Token endpointAdministrator-configured HTTPS URL, including fixed query parameters; verified TLS; redirects are rejected.
Client authenticationclient_secret_basic or client_secret_post, selected in configuration. No automatic method fallback.
Request parametersscope, optional audience, and repeated resource parameters from RFC 8707 .
Successful responseHTTP 200 with a JSON body containing access_token and Bearer token_type. Unknown response fields are ignored.
Access tokenA compact, signed JWT with exp and matching configured claims. OpenBao verifies its signature and role bindings.
ExpirationJWT exp is required. If expires_in is present, the provider uses the shorter lifetime.
Client secretAbsolute path to a regular file; no symlink, group write, world access, or execute permission. The file is reread before each grant.
Other flowsRefresh-token grants, interactive login, opaque-token introspection, client assertions, and mutual TLS client authentication are unavailable.

The optional audience request parameter is an extension supported by some authorization servers. It does not replace expectedAudience, which checks the JWT returned by the server. Configure only the request parameters your issuer accepts. Scopes and resource indicators depend on the registered API and client.

Keycloak service accounts can issue JWT access tokens through this grant. Other OAuth providers can use the same integration when they meet this contract. Protocol tests exercise both client authentication methods with an HTTPS test issuer. OpenBao integration tests exercise signature validation and Transit. The maintained Kind OAuth lane uses the selected OpenBao, Kubernetes, and Keycloak images pinned by digest in .ci/versions.yaml and the generated provider static pod. It checks both authentication methods, Kubernetes Secret encryption in etcd, credential rotation without a provider restart, issuer outages, and cold provider recovery while the protected API server is stopped. Results apply to the exact candidate and pinned versions tested. Other providers and issuer deployment topologies require their own validation.

For the maintained Keycloak client setup, see Provision a Keycloak client .

Configure the source

Register a confidential client with permission to request a token for the OpenBao audience. Record the actual token issuer, audience, and service-account subject. Configure the OpenBao JWT role to bind that identity and grant only the provider policy; see OpenBao auth and policy .

Replace the JWT section in the provider configuration:

yaml
auth:
  method: jwt
  loginTimeout: 10s
  jwt:
    source: oauth2
    mountPath: auth/k8s-workload-a-jwt
    role: openbao-kms-control-plane
    minRemainingTtl: 2m
    clockSkewLeeway: 30s
    expectedIssuer: https://identity.example.com/realms/platform
    expectedAudience:
      - openbao-kms-production
    expectedSubject: "<service-account-subject>"
    oauth2:
      tokenUrl: https://identity.example.com/realms/platform/protocol/openid-connect/token
      clientId: kms-control-plane
      authMethod: client_secret_basic
      clientSecretFile: /etc/openbao-kms/credentials/client-secret
      caCertFile: /etc/openbao-kms/issuer/ca.pem

Omit caCertFile to use system certificate roots. If you set it, the file is the trust bundle for the token endpoint. OpenBao has separate TLS trust settings and separate issuer trust for JWT verification.

expectedIssuer and at least one expectedAudience are required for this source. Set expectedSubject when the issuer provides a stable subject. If identity binding uses other claims, configure those bindings on the OpenBao role. The init setup script requires a subject to generate its bound JWT role. Omit jwtFile; configuring both sources is rejected.

For issuers that require request parameters, add the applicable fields under oauth2. These are independent examples; use the values registered with your issuer:

yaml
scopes: ["https://api.example.com/.default"]
audience: https://api.example.com
resources: ["https://api.example.com"]

Use a dedicated directory for the client secret. For systemd, install the credential with owner root, group openbao-kms, and mode 0640; give the service user directory traversal permission. A single trailing newline is accepted. Keep the secret out of configuration YAML, command arguments, and logs.

For static pods, init --model static-pod mounts the credential directory read-only so atomic replacement is visible without a restart. The container UID must be able to read the secret. No Kubernetes Secret or TokenRequest is needed.

Run bao-kms-provider doctor --config /etc/openbao-kms/config.yaml as the service user. oauth2.local checks the credential file. oauth2.acquire reports token acquisition, followed by OpenBao JWT login and the usual Transit checks.

Lifetime and recovery

auth.loginTimeout covers token acquisition and OpenBao login together, including an attempted OpenBao renewal before fallback login. Set it for both network hops. Concurrent requests share one login attempt and failed attempts use the existing exponential backoff.

Acquire tokens with more remaining lifetime than auth.jwt.minRemainingTtl (default two minutes). The provider rejects a token at or below this threshold, even if the issuer has just minted it. Lower the threshold deliberately when using an issuer with a shorter token lifetime.

The provider renews its OpenBao token when renewal is available. Renewal does not acquire a new issuer token. An issuer outage can therefore leave an existing OpenBao token usable. New logins require the issuer; an expired or rejected OpenBao token cannot fall back to a JWT file or another authentication method.

Disabling the OAuth client does not revoke OpenBao tokens already issued to it. Use bounded OpenBao token lifetimes and an OpenBao revocation procedure when immediate removal of access is required.

To rotate the client secret, install the replacement as a private temporary file in the same directory, set its ownership and permissions, and rename it over clientSecretFile. The next grant reads the replacement. Use the issuer’s credential overlap procedure when available, and verify a new login before retiring the old secret. Changes to the endpoint or CA configuration require a provider restart.

Credential and endpoint failures

Local credential errors include a bounded reason: file_missing, file_unreadable, symlink_unsupported, regular_file_required, unsafe_permissions, file_changed, file_too_large, invalid_content, or absolute_path_required. Check the configured path and access as the provider user. Use a private regular file for the client secret; symlinked projected Secret files are unsupported. Complete atomic replacement before retrying a file_changed error. Keep the file within 64 KiB and use one nonempty line.

HTTP 429 and 5xx responses report oauth2_unavailable. Other non-200 responses report oauth2_rejected. Transport failures retain typed internal causes but print no endpoint URL, credentials, or response body. Login backoff applies to these failures; it does not retry inside the OAuth HTTP client.

Search the handbook

Try “doctor”, “rotation”, or “key_id”.