Keycloak + Traefik: Reverse Proxy Authentication Setup

Guilliano Molaire Guilliano Molaire Updated June 9, 2026 9 min read

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:

  1. Client sends a request to app.example.com.
  2. Traefik intercepts the request and forwards it to the auth service.
  3. The auth service checks if the user has a valid session (cookie/token).
  4. If authenticated: the auth service returns 200 with user info headers.
  5. If not authenticated: the auth service returns 302 redirecting to Keycloak login.
  6. After login, Keycloak redirects back. The auth service creates a session and returns 200.
  7. 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 *.localhost which resolves to 127.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

  1. Open the Keycloak admin console at http://keycloak.localhost/admin (username: admin, password: admin).
  2. Create a new realm: traefik-demo.
  3. Create a client:
Setting Value
Client ID traefik-auth
Client Authentication ON
Valid Redirect URIs http://auth.localhost/_oauth
Web Origins *
  1. On the Credentials tab, set the client secret to traefik-auth-secret (matching the Docker Compose config).

Create a Test User

  1. Go to Users > Add user.
  2. Username: testuser, Email: [email protected].
  3. 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

  1. Log in with testuser and your password.
  2. After successful authentication, you are redirected back to http://app.localhost.
  3. The whoami service shows the request headers, including X-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-auth provides 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.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman