Keycloak
Wiring Keycloak as Flux's OIDC identity provider — realm setup, audience mappers, and a docker-compose snippet for local trial.
Keycloak is the canonical self-hosted OIDC provider: open-source, realm-based, and well-supported. Flux integrates with it the same way it integrates with any OIDC IdP: discovery, JWKS, RS256-signed JWTs. But Keycloak has a quirk around audience claims that catches almost everyone the first time. The abstract Flux contract lives in Configuring OIDC; this page is the Keycloak-specific recipe.
What to register at Keycloak
You need a realm, a client, and an audience mapper:
-
Realm. Either reuse an existing one or create a dedicated
fluxrealm. The realm name appears in the issuer URL. Realm-level signing key is RS256 by default; leave that alone. -
Client. Create a client (e.g.,
flux-server):- Access Type: confidential for server-to-server callers using the client-credentials grant.
- Access Type: public if a browser-side flow is the only path (no client secret).
- Service accounts enabled: on if you want client-credentials.
- Valid Redirect URIs: only relevant for browser flows; Flux itself does not host a redirect endpoint.
-
Audience mapper. Under Client → Client Scopes → Dedicated → Add Mapper → “Audience” (or “Audience Resolve”), set:
- Included Client Audience:
flux-server(or whatever you wantaudto contain) - Add to access token: on
Without this mapper, Keycloak tokens carry
audequal toaccountor the realm management client, not your client ID. Flux comparesaudbyte-for-byte against the configuredaudienceand rejects mismatches asInvalid audience. This is the single most common Keycloak misconfiguration when integrating with anything that validatesaudstrictly. - Included Client Audience:
Issuer URL is https://<keycloak-host>/realms/<realm> — no trailing slash. Discovery is at <issuer>/.well-known/openid-configuration.
Vendor-specific gotchas
auddoes not default to the client ID. As described above. Keycloak’s reasoning is that the client requesting the token is identified byazp(authorized party), andaudis reserved for downstream resource servers. The Audience mapper is what bridges the two.- Realm management URL change in Keycloak 17+. Newer Keycloak drops the
/authprefix from URLs.https://kc.example.com/auth/realms/flux(Keycloak 16 and older) ishttps://kc.example.com/realms/flux(17+). Pick whichever matches your deployment and pin it; Flux compares theissclaim verbatim. - Realm roles vs client roles in tokens. Keycloak emits both as
realm_access.rolesandresource_access.<client>.rolesclaims. Flux does not read either —roles_claimis deprecated, and roles come from Flux’s principals registry. Keycloak roles are useful only as an out-of-band signal for sync jobs. subis a Keycloak UUID by default. Stable per user, but opaque. If you prefer human-readable subjects, configure a custom mapper to putpreferred_usernameintosub— or just live with the UUID and use Keycloak’s account UI to look up identities.- Service account principals. When a client uses client-credentials, Keycloak issues a token with
subequal to the service-account user’s UUID (every confidential client gets a paired service account). It is its own principal in Flux’s registry — not the same identity as a real user who logs in via the same client. - Token lifetime. Realm → Tokens → Access Token Lifespan. Defaults to 5 minutes. Short access-token lifetimes are a Keycloak strength — keep them short.
The minimal Flux config
[flux.security.auth.oidc]
enabled = true
issuer = "https://keycloak.example.com/realms/flux"
audience = "flux-server"
default_user_roles = ["viewer"]
Substitute your Keycloak host and realm name; the audience is whatever you set in the audience mapper. default_user_roles = ["viewer"] opts into auto-provisioning on first sign-in; remove it to force manual flux principals create for every user.
Defaults for jwks_cache_ttl (3600s) and clock_skew (30s) are fine; bump clock_skew only if you have NTP drift you cannot fix at the host level.
Trying it locally
A self-contained Keycloak for local development:
# docker-compose.yml
services:
keycloak:
image: quay.io/keycloak/keycloak:25.0
command: start-dev
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
Start it (docker compose up -d), log in to http://localhost:8080, create a realm called flux, create a confidential client called flux-server with service accounts enabled, add the Audience mapper as described above. Then point Flux at it:
[flux.security.auth.oidc]
enabled = true
issuer = "http://localhost:8080/realms/flux"
audience = "flux-server"
default_user_roles = ["viewer"]
The local issuer here is HTTP, which works — Flux’s httpx discovery call handles plain HTTP fine. In production, strongly prefer HTTPS: tokens travel in OIDC requests, and HTTP exposes them in transit.
Verification
curl -s https://keycloak.example.com/realms/flux/.well-known/openid-configuration | jq .
Confirm issuer matches Flux’s config exactly and id_token_signing_alg_values_supported contains RS256.
Smoke-test a client-credentials token:
curl -s --request POST https://keycloak.example.com/realms/flux/protocol/openid-connect/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials' \
--data 'client_id=flux-server' \
--data 'client_secret=<secret>' \
| jq -r .access_token
Decode the payload. Check aud is flux-server (not account) and that iss matches your config. If aud is wrong, the Audience mapper is not in place — go back to the client scope and add it.
Hit a Flux endpoint:
curl -H "Authorization: Bearer $TOKEN" https://flux.example.com/workflows
Flux logs OIDC token validation failed: Invalid audience if the mapper is missing, or Auto-provisioned principal <sub> on the first successful sign-in when default_user_roles is set.
See Configuring OIDC for the four required claims, the deprecation of roles_claim, and the Flux-side principal-resolution flow.
Derived against Keycloak docs 2026-05.