oauth2-proxy Configuration#
oauth2-proxy is a reverse proxy that gates access to upstream services using OAuth2/OIDC authentication. This article covers three key configuration areas: OIDC provider setup (automatic discovery vs. manual endpoint configuration), Keycloak-specific wiring, and ForwardAuth integration with Traefik in Kubernetes.
Configuration delivery supports three methods in decreasing precedence order: CLI flags, environment variables (prefixed OAUTH2_PROXY_, with hyphens replaced by underscores), and a TOML config file.
OIDC Discovery (Automatic Endpoint Resolution)#
The default OIDC mode — set --oidc-issuer-url and leave --skip-oidc-discovery false — triggers automatic discovery. On startup, NewProvider appends /.well-known/openid-configuration to the issuer URL and parses the response into authorization_endpoint, token_endpoint, jwks_uri, userinfo_endpoint, and supported signing algorithms .
By default, the discovered issuer field must exactly match the configured --oidc-issuer-url; mismatches are rejected . Use --insecure-oidc-skip-issuer-verification when the provider returns a different issuer (e.g., some Azure multi-tenant setups).
Signing algorithm resolution: if --oidc-enabled-signing-algs is set, the effective set is the intersection of the configured list and the provider's discovered id_token_signing_alg_values_supported. If there is no intersection, startup fails .
The ProviderVerifier interface exposes DiscoveryEnabled() to distinguish between auto-discovered and manually-configured providers — callers must check this before using endpoint data from the provider .
Manual Endpoint Configuration (Skip Discovery)#
Use --skip-oidc-discovery when the provider does not publish a discovery document, or when the issuer URL doesn't match the document. When this flag is set, you must supply either --oidc-jwks-url or --oidc-public-key-files — not both — along with --oidc-issuer-url (still required for token issuer validation) .
| Flag | TOML key | Purpose |
|---|---|---|
--login-url | login_url | Authorization endpoint |
--redeem-url | redeem_url | Token redemption endpoint |
--oidc-jwks-url | oidc_jwks_url | JWKS URI for token verification |
--profile-url | profile_url | UserInfo endpoint |
--validate-url | validate_url | Access token validation endpoint |
Internally, legacy flags are converted to the OIDCOptions struct: SkipDiscovery , JwksURL , and IssuerURL . When discovery is skipped, the proxy creates a remote JWKS key set from the provided URL rather than fetching it from the discovery document.
If you have a local PEM public key instead of a JWKS URL, pass --oidc-public-key-files pointing to PEM-encoded files — newKeySetFromStatic loads them into a static key set.
Keycloak OIDC Provider#
Use --provider=keycloak-oidc (not the legacy keycloak provider). The issuer URL format changed in Keycloak 17+:
- v17+:
https://<keycloak-host>/realms/<realm> - pre-v17:
https://<keycloak-host>/auth/realms/<realm>
Audience mapper (required): The Keycloak OIDC client must have an audience mapper that adds the client's name to the aud claim of the JWT. oauth2-proxy validates the token against --client-id or --oidc-extra-audience, and will reject tokens without a matching audience. Configure this in the Keycloak console under Clients → Client scopes → dedicated → Mappers.
Authorization options:
| Flag | Example | Notes |
|---|---|---|
--allowed-role | myrole | Realm role |
--allowed-role | myclient:myrole | Client role (uses <client-id>:<role-name> format) |
--allowed-group | /groupname | Requires "groups" client scope attached to client |
Standard Keycloak installations include role mappers for realm and client roles via the pre-defined "roles" client scope . Group membership requires creating a separate "groups" client scope and attaching it to the client.
Other recommended settings: --code-challenge-method=S256 for PKCE, and scope = "openid email profile". See Keycloak OIDC provider docs for the full Keycloak client setup walkthrough.
ForwardAuth Integration with Traefik#
--reverse-proxy=true is required for all Traefik ForwardAuth setups so oauth2-proxy accepts and correctly uses X-Forwarded-* headers from Traefik .
There are two patterns:
Pattern 1 — ForwardAuth + errors middleware (redirect on 401)#
The forwardAuth middleware points at /oauth2/auth. oauth2-proxy returns 202 (authenticated) or 401 (unauthenticated) without proxying the request. The errors middleware catches 401–403 responses and redirects the browser to /oauth2/sign_in.
middlewares:
oauth-auth:
forwardAuth:
address: https://oauth.example.com/oauth2/auth
trustForwardHeader: true
oauth-errors:
errors:
status: ["401-403"]
service: oauth-backend
query: "/oauth2/sign_in"
Pattern 2 — ForwardAuth to / with static://202 upstream (self-redirecting)#
oauth2-proxy is configured with --upstream=static://202. The forwardAuth middleware points to oauth2-proxy's / endpoint, which handles unauthenticated requests by redirecting directly to the sign-in flow — no separate errors middleware needed. A second middleware variant pointing at /oauth2/auth returns 401 without redirecting for routes where silent failure is preferred.
middlewares:
oauth-auth-redirect: # auto-redirects to sign-in
forwardAuth:
address: https://oauth.example.com/
trustForwardHeader: true
authResponseHeaders:
- X-Auth-Request-Access-Token
- Authorization
oauth-auth-wo-redirect: # returns 401 without redirect
forwardAuth:
address: https://oauth.example.com/oauth2/auth
trustForwardHeader: true
authResponseHeaders:
- X-Auth-Request-Access-Token
- Authorization
Header forwarding options:
| Flag | Purpose |
|---|---|
--set-xauthrequest | Sets X-Auth-Request-User, X-Auth-Request-Email, X-Auth-Request-Groups response headers |
--pass-authorization-header | Forwards the OIDC ID token to upstream as Authorization: Bearer |
--pass-access-token | Forwards OAuth access token via X-Forwarded-Access-Token; adds X-Auth-Request-Access-Token when combined with --set-xauthrequest |
Gotcha — pass_host_header: defaults to true. Setting pass_host_header = false rewrites the Host header to the upstream URL, which breaks host-based routing inside Kubernetes .
Known Issue: CSRF Token Mismatch with Traefik#
When using Traefik's errors middleware with skip_provider_button = true, browsers with concurrent subresource requests (JS, CSS, service workers) cause Traefik to make multiple simultaneous calls to /oauth2/sign_in. Each call generates a new CSRF cookie that overwrites the previous one. By the time the OAuth callback arrives, the browser's CSRF cookie no longer matches the state parameter, resulting in a 403 "CSRF token mismatch" error .
Fix: enable cookie_csrf_per_request = true. This makes each authentication attempt write a unique CSRF cookie keyed by its state nonce rather than using a single shared _oauth2_proxy_csrf cookie, so concurrent requests each carry distinct cookies .
Related cookie options :
--cookie-csrf-expire— lifetime of CSRF cookies (default 15m)--cookie-csrf-samesite— SameSite policy for CSRF cookies (independent of session cookie)--cookie-csrf-per-request-limit— caps the number of concurrent CSRF cookies to avoid HTTP 431 errors
Alternative: set skip_provider_button = false. This makes /oauth2/sign_in serve a static HTML page (no cookies set), and CSRF state is only generated when the user clicks through to /oauth2/start — a single browser navigation .