Keycloak + Traefik: Reverse Proxy Authentication Setup
Last updated: March 2026
Introduction
Traefik is one of the most popular reverse proxies for containerized environments. Its Docker-native label system and automatic service discovery make it a natural fit for microservices architectures. When paired with Keycloak through the ForwardAuth middleware, Traefik can enforce authentication on any backend service without modifying the service itself.
This guide covers the full setup: Traefik ForwardAuth middleware with an OIDC verification service, Docker Compose configuration, header forwarding to upstream services, and Kubernetes IngressRoute configuration. By the end, you will have a working setup where any service behind Traefik requires a valid Keycloak session.
If you are comparing reverse proxy options for Keycloak, see our general guide on running Keycloak behind a reverse proxy. For Kong-specific integration, see Keycloak + Kong API Gateway.
How ForwardAuth Works
Traefik’s ForwardAuth middleware intercepts incoming requests and forwards them to an authentication service before allowing the request to reach the backend. The flow:
- Client sends a request to
app.example.com. - Traefik intercepts the request and forwards it to the auth service.
- The auth service checks if the user has a valid session (cookie/token).
- If authenticated: the auth service returns
200with user info headers. - If not authenticated: the auth service returns
302redirecting to Keycloak login. - After login, Keycloak redirects back. The auth service creates a session and returns
200. - Traefik forwards the original request to the backend with user info headers.
The backend service never sees unauthenticated requests. It receives identity information via trusted headers set by the auth middleware.
Prerequisites
- Docker and Docker Compose
- A domain or local DNS (we use
*.localhostwhich resolves to127.0.0.1) - Basic familiarity with Keycloak and OIDC
Docker Compose Setup
Here is the complete docker-compose.yml with Traefik, Keycloak, the ForwardAuth service, and a sample protected application:
version: "3.9"
services:
# --- Traefik Reverse Proxy ---
traefik:
image: traefik:v3.2
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
- "8081:8080" # Traefik dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# --- Keycloak ---
keycloak-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak_pass
volumes:
- keycloak_data:/var/lib/postgresql/data
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak_pass
KC_HOSTNAME: keycloak.localhost
KC_HOSTNAME_STRICT: "false"
KC_PROXY_HEADERS: xforwarded
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
depends_on:
- keycloak-db
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.rule=Host(`keycloak.localhost`)"
- "traefik.http.routers.keycloak.entrypoints=web"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
# --- ForwardAuth Service ---
forward-auth:
image: thomseddon/traefik-forward-auth:2
environment:
PROVIDERS_OIDC_ISSUER_URL: "http://keycloak:8080/realms/traefik-demo"
PROVIDERS_OIDC_CLIENT_ID: "traefik-auth"
PROVIDERS_OIDC_CLIENT_SECRET: "traefik-auth-secret"
DEFAULT_PROVIDER: "oidc"
SECRET: "a-random-secret-string-change-this"
AUTH_HOST: "auth.localhost"
COOKIE_DOMAIN: "localhost"
URL_PATH: "/_oauth"
LOG_LEVEL: "debug"
labels:
- "traefik.enable=true"
# Auth host route
- "traefik.http.routers.forward-auth.rule=Host(`auth.localhost`)"
- "traefik.http.routers.forward-auth.entrypoints=web"
- "traefik.http.services.forward-auth.loadbalancer.server.port=4181"
# ForwardAuth middleware definition
- "traefik.http.middlewares.keycloak-auth.forwardauth.address=http://forward-auth:4181"
- "traefik.http.middlewares.keycloak-auth.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.keycloak-auth.forwardauth.authResponseHeaders=X-Forwarded-User"
# --- Protected Application ---
whoami:
image: traefik/whoami
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`app.localhost`)"
- "traefik.http.routers.whoami.entrypoints=web"
- "traefik.http.routers.whoami.middlewares=keycloak-auth"
# --- Another Protected Application ---
dashboard:
image: hashicorp/http-echo
command: ["-text", '{"service":"dashboard","status":"running"}']
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`dashboard.localhost`)"
- "traefik.http.routers.dashboard.entrypoints=web"
- "traefik.http.routers.dashboard.middlewares=keycloak-auth"
- "traefik.http.services.dashboard.loadbalancer.server.port=5678"
volumes:
keycloak_data:
Start everything:
docker compose up -d
Configuring Keycloak
Create the Realm and Client
- Open the Keycloak admin console at
http://keycloak.localhost/admin(username:admin, password:admin). - Create a new realm:
traefik-demo. - Create a client:
| Setting | Value |
|---|---|
| Client ID | traefik-auth |
| Client Authentication | ON |
| Valid Redirect URIs | http://auth.localhost/_oauth |
| Web Origins | * |
- On the Credentials tab, set the client secret to
traefik-auth-secret(matching the Docker Compose config).
Create a Test User
- Go to Users > Add user.
- Username:
testuser, Email:[email protected]. - Under Credentials, set a password and disable Temporary.
For SSO across multiple applications, Keycloak’s single sign-on capability means the user logs in once and is automatically authenticated across all services behind Traefik.
Testing the Setup
Unauthenticated Request
Open http://app.localhost in your browser. You should be redirected to the Keycloak login page.
Authenticated Request
- Log in with
testuserand your password. - After successful authentication, you are redirected back to
http://app.localhost. - The
whoamiservice shows the request headers, includingX-Forwarded-User.
SSO Across Services
Now visit http://dashboard.localhost. Because you already authenticated with Keycloak, you should access the dashboard without a second login prompt. This is OIDC SSO in action.
Testing with curl
# This will return a 307 redirect to Keycloak
curl -v http://app.localhost
# Look for: Location: http://keycloak.localhost/realms/traefik-demo/...
# After authentication, the forward-auth service sets a cookie.
# Simulate an authenticated request:
curl -v -b "cookie-from-browser" http://app.localhost
Forwarding User Identity to Backend Services
The ForwardAuth middleware can pass authenticated user information to backends via headers. Configure which headers to forward:
# In the forward-auth labels:
- "traefik.http.middlewares.keycloak-auth.forwardauth.authResponseHeaders=X-Forwarded-User,X-Auth-Email,X-Auth-Groups"
The thomseddon/traefik-forward-auth image sets these headers automatically:
| Header | Value |
|---|---|
X-Forwarded-User |
The user’s email or preferred username |
Your backend applications can trust these headers because Traefik strips them from incoming client requests before the ForwardAuth middleware sets them.
Custom ForwardAuth Service
For more control over the authentication flow, you can build a custom ForwardAuth service. Here is a Node.js implementation:
// custom-auth/server.ts
import express from "express";
import session from "express-session";
import { Issuer, generators } from "openid-client";
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET || "change-me",
resave: false,
saveUninitialized: false,
cookie: {
domain: ".localhost",
httpOnly: true,
secure: false, // Set true in production with HTTPS
maxAge: 3600_000, // 1 hour
},
}));
let oidcClient: any;
async function initClient(): Promise<void> {
const issuer = await Issuer.discover(
process.env.OIDC_ISSUER
|| "http://keycloak:8080/realms/traefik-demo",
);
oidcClient = new issuer.Client({
client_id: "traefik-auth",
client_secret: "traefik-auth-secret",
redirect_uris: ["http://auth.localhost/callback"],
response_types: ["code"],
});
}
// ForwardAuth endpoint - Traefik calls this for every request
app.get("/verify", (req, res) => {
if (req.session?.user) {
// User is authenticated - set headers and return 200
res.set("X-Forwarded-User", req.session.user.email);
res.set("X-Auth-Roles",
(req.session.user.roles || []).join(","));
res.set("X-Auth-Sub", req.session.user.sub);
return res.sendStatus(200);
}
// Not authenticated - redirect to login
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
req.session!.codeVerifier = codeVerifier;
req.session!.originalUrl = req.headers["x-forwarded-uri"]
|| "/";
req.session!.originalHost = req.headers["x-forwarded-host"]
|| "app.localhost";
const authUrl = oidcClient.authorizationUrl({
scope: "openid profile email",
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
res.redirect(authUrl);
});
// Callback endpoint after Keycloak authentication
app.get("/callback", async (req, res) => {
try {
const params = oidcClient.callbackParams(req);
const tokenSet = await oidcClient.callback(
"http://auth.localhost/callback",
params,
{ code_verifier: req.session!.codeVerifier },
);
const userInfo = await oidcClient.userinfo(tokenSet);
req.session!.user = {
sub: userInfo.sub,
email: userInfo.email,
username: userInfo.preferred_username,
roles: tokenSet.claims()?.realm_access?.roles || [],
};
const originalHost = req.session!.originalHost
|| "app.localhost";
const originalPath = req.session!.originalUrl || "/";
res.redirect(`http://${originalHost}${originalPath}`);
} catch (err) {
console.error("Auth callback error:", err);
res.status(500).send("Authentication failed");
}
});
// Logout endpoint
app.get("/logout", async (req, res) => {
const idToken = req.session?.idToken;
req.session?.destroy(() => {});
const logoutUrl = oidcClient.endSessionUrl({
id_token_hint: idToken,
post_logout_redirect_uri: "http://app.localhost",
});
res.redirect(logoutUrl);
});
initClient().then(() => {
app.listen(4181, () => {
console.log("Custom ForwardAuth service on port 4181");
});
});
Update the Docker Compose ForwardAuth middleware to use this custom service:
# Updated middleware labels for custom auth service
- "traefik.http.middlewares.keycloak-auth.forwardauth.address=http://custom-auth:4181/verify"
- "traefik.http.middlewares.keycloak-auth.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.keycloak-auth.forwardauth.authResponseHeaders=X-Forwarded-User,X-Auth-Roles,X-Auth-Sub"
Selective Authentication
Not every route needs authentication. Apply the middleware selectively:
# Public service - no middleware
public-api:
image: my-public-api
labels:
- "traefik.enable=true"
- "traefik.http.routers.public.rule=Host(`api.localhost`) && PathPrefix(`/public`)"
- "traefik.http.routers.public.entrypoints=web"
# No middleware = no auth required
# Protected service - with middleware
private-api:
image: my-private-api
labels:
- "traefik.enable=true"
- "traefik.http.routers.private.rule=Host(`api.localhost`) && PathPrefix(`/admin`)"
- "traefik.http.routers.private.entrypoints=web"
- "traefik.http.routers.private.middlewares=keycloak-auth"
You can also combine ForwardAuth with other Traefik middleware for layered security:
# Chain rate limiting with authentication
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
- "traefik.http.middlewares.rate-limit.ratelimit.period=1m"
- "traefik.http.middlewares.secured.chain.middlewares=rate-limit,keycloak-auth"
- "traefik.http.routers.myservice.middlewares=secured"
Kubernetes IngressRoute Configuration
For Kubernetes deployments using Traefik as the ingress controller, define the ForwardAuth middleware as a custom resource:
# middleware.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: keycloak-auth
namespace: default
spec:
forwardAuth:
address: http://forward-auth.default.svc.cluster.local:4181
trustForwardHeader: true
authResponseHeaders:
- X-Forwarded-User
- X-Auth-Roles
- X-Auth-Sub
---
# ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: my-app
namespace: default
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
middlewares:
- name: keycloak-auth
services:
- name: my-app-service
port: 80
tls:
certResolver: letsencrypt
---
# Forward auth service IngressRoute (for OAuth callback)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: forward-auth
namespace: default
spec:
entryPoints:
- websecure
routes:
- match: Host(`auth.example.com`)
kind: Rule
services:
- name: forward-auth
port: 4181
tls:
certResolver: letsencrypt
For Kubernetes deployments of Keycloak itself, see our guide on deploying Keycloak in Kubernetes with ArgoCD.
Production Hardening
HTTPS
In production, always use HTTPS. Configure Traefik with Let’s Encrypt:
traefik:
command:
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "[email protected]"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
volumes:
- letsencrypt_data:/letsencrypt
Session Storage
The default thomseddon/traefik-forward-auth stores sessions in cookies. For production, consider a custom ForwardAuth service with Redis-backed sessions for better scalability and session revocation.
Cookie Security
# Production cookie settings
COOKIE_DOMAIN: "example.com"
COOKIE_NAME: "__auth"
COOKIE_SECURE: "true" # Require HTTPS
COOKIE_HTTPONLY: "true" # Prevent JS access
COOKIE_SAMESITE: "lax" # CSRF protection
Monitoring and Audit
Enable audit logging in Keycloak to track all authentication events. Use Traefik’s access logs to correlate with the X-Forwarded-User header:
traefik:
command:
- "--accesslog=true"
- "--accesslog.fields.headers.names.X-Forwarded-User=keep"
For centralized monitoring, Skycloak’s insights dashboard provides real-time visibility into authentication patterns across your applications.
Security Headers
Add security headers alongside ForwardAuth:
- "traefik.http.middlewares.security-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.security-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.security-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.security-headers.headers.frameDeny=true"
- "traefik.http.middlewares.secured.chain.middlewares=security-headers,keycloak-auth"
Troubleshooting
Redirect loop after login: Ensure the COOKIE_DOMAIN matches your domain structure and that the AUTH_HOST is accessible from the browser.
502 Bad Gateway on ForwardAuth: The auth service is not reachable from Traefik. Check that both containers are on the same Docker network.
Token validation fails: If Keycloak’s internal URL differs from the browser-accessible URL, configure KC_HOSTNAME in Keycloak to match the external URL. The iss claim must match what the ForwardAuth service expects.
Session expires too quickly: Adjust both the ForwardAuth cookie TTL and Keycloak’s SSO session timeout (Realm Settings > Sessions > SSO Session Idle/Max).
Conclusion
Traefik ForwardAuth with Keycloak provides a clean pattern for adding authentication to any service without modifying application code. The approach works consistently across Docker Compose and Kubernetes environments, and because authentication is handled at the proxy layer, you can protect any HTTP service regardless of its technology stack.
Key takeaways:
- ForwardAuth delegates authentication decisions to an external service
thomseddon/traefik-forward-authprovides a ready-to-use OIDC bridge- Custom ForwardAuth services offer more control over headers and session management
- Kubernetes IngressRoute CRDs integrate the same middleware pattern
- Always use HTTPS and secure cookie settings in production
For more on Traefik middleware, see the Traefik ForwardAuth documentation. For Keycloak configuration, consult the Keycloak Server Administration Guide.
Looking for a managed Keycloak instance that works seamlessly with Traefik? Try Skycloak free and skip the infrastructure management. Check our hosting page for deployment options and SLA guarantees.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.