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:

  1. Realm. Either reuse an existing one or create a dedicated flux realm. The realm name appears in the issuer URL. Realm-level signing key is RS256 by default; leave that alone.

  2. 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.
  3. Audience mapper. Under Client → Client Scopes → Dedicated → Add Mapper → “Audience” (or “Audience Resolve”), set:

    • Included Client Audience: flux-server (or whatever you want aud to contain)
    • Add to access token: on

    Without this mapper, Keycloak tokens carry aud equal to account or the realm management client, not your client ID. Flux compares aud byte-for-byte against the configured audience and rejects mismatches as Invalid audience. This is the single most common Keycloak misconfiguration when integrating with anything that validates aud strictly.

Issuer URL is https://<keycloak-host>/realms/<realm> — no trailing slash. Discovery is at <issuer>/.well-known/openid-configuration.

Vendor-specific gotchas

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.