Skip to content

Latest commit

 

History

History

README.md

Keycloak HA with Traefik TLS Passthrough

This quickstart is for educational purposes only and should not be used in production. It demonstrates how to configure Traefik as a TLS passthrough load balancer in front of a clustered Keycloak deployment.

What is TLS passthrough?

In TLS passthrough mode, the load balancer forwards encrypted TLS traffic directly to the backend servers without decrypting it. Traefik operates at the TCP layer (Layer 4) and has no visibility into the HTTP content. The TLS connection is terminated by Keycloak itself, which means:

  • Keycloak holds the TLS certificate and private key, not the proxy.
  • Traefik cannot inspect, modify, or cache HTTP headers or the request body.
  • End-to-end encryption is preserved between the client and Keycloak.

Architecture

Architecture diagram

  • Traefik listens on port 8443 and forwards raw TCP traffic to both Keycloak instances using round-robin. It uses PROXY protocol v2 to pass the original client IP address to Keycloak. It is attached to both the frontend and backend networks, making it the single entry point.
  • Keycloak 1 & 2 are clustered via embedded Infinispan. They terminate TLS and share the same PostgreSQL database. They live exclusively on the backend network, which is marked as internal and unreachable from the host.
  • PostgreSQL provides the shared database for Keycloak on the backend network.

Prerequisites

  • Docker and Docker Compose
  • openssl (for certificate generation)

Quick start

1. Generate a TLS certificate

./generate-certs.sh <hostname>

This example uses nip.io, a DNS service that maps 127.0.0.1.nip.io to 127.0.0.1, avoiding the need to edit /etc/hosts:

./generate-certs.sh 127.0.0.1.nip.io

2. Start the services

KC_HOST=<hostname> docker compose up -d

For example:

KC_HOST=127.0.0.1.nip.io docker compose up -d

3. Access Keycloak

Once the services are up, Keycloak is available at https://<hostname>:8443. Log in to the admin console using credentials admin / admin.

The browser will show a certificate warning because the certificate is self-signed. This is expected and can be safely accepted for local testing.

4. Check Traefik dashboard

Open http://127.0.0.1:8080/dashboard/ in a browser to verify that Traefik is running and both Keycloak backends are registered as TCP services.

5. Showcase graceful shutdown

This is a walkthrough through a graceful shutdown of one of the Keycloak instances:

  1. Open http://127.0.0.1:8080/dashboard/ in a browser to verify that Traefik is running and both Keycloak backends are healthy.

  2. Send a TERM signal to one of the Keycloak containers for a graceful shutdown (takes 30 seconds). Container exits with code 143.

    docker stop passthrough-keycloak1-1 -t 60
  3. Observe that Traefik detects the backend is unavailable and stops routing traffic to it. Requests are still served by the remaining node.

  4. Start the Keycloak container again:

    docker start passthrough-keycloak1-1
  5. Observe that Traefik automatically re-registers the backend and resumes routing traffic to it.

6. Stop the services

docker compose down

Traefik configuration

Traefik separates its configuration into two files:

  • traefik.yaml — static configuration loaded at startup (entry points, dashboard, and providers).
  • keycloak.yaml — dynamic configuration for runtime routing rules, services, and health checks.

Both files are required for this setup to work correctly.

traefik.yaml — static configuration:

Entry point for TLS traffic:

entryPoints:
  websecure:
    address: ":8443"

Defines the entry point on port 8443 where Traefik accepts incoming TCP/TLS connections.

Dashboard:

api:
  insecure: true

Enables the Traefik dashboard on port 8080 without authentication. The dashboard is accessible at http://127.0.0.1:8080/dashboard/. This must never be used in production.

keycloak.yaml — dynamic configuration:

TCP router with TLS passthrough:

tcp:
  routers:
    keycloak-router:
      rule: "HostSNI(`*`)"
      service: "keycloak-service"
      priority: 100
      entryPoints:
        - "websecure"
      tls:
        passthrough: true
  • HostSNI(*) matches all TLS connections regardless of the SNI hostname.
  • priority: 100 ensures this router takes precedence over any other TCP routers that may be defined.
  • tls.passthrough: true instructs Traefik to forward the raw TLS stream without terminating it. Keycloak handles TLS termination.

Servers transport with PROXY protocol:

serversTransports:
  kc-passthrough:
    proxyProtocol:
      version: 2
  • Defines a named transport kc-passthrough that enables PROXY protocol v2 on all backend connections. This allows Keycloak to see the original client IP address and requires Keycloak to be configured with KC_PROXY_PROTOCOL_ENABLED=true.

Load balancer with health check:

services:
  keycloak-service:
    loadBalancer:
      serversTransport: kc-passthrough
      # Use the JSON syntax to have explicit CR/LF encoding for send/expect.
      healthCheck: {
        interval: "5s",
        timeout: "3s",
        port: 9000,
        send: "HEAD /health/ready HTTP/1.0\r\n\r\n",
        expect: "HTTP/1.0 200 OK\r\ncontent-type: application/json; charset=UTF-8\r\ncache-control: no-store\r\n\r\n"
      }
      servers:
        - address: "keycloak1:8443"
        - address: "keycloak2:8443"
  • serversTransport: kc-passthrough links the load balancer to the transport defined above, enabling PROXY protocol v2 on all backend connections. This requires Keycloak to be configured with KC_PROXY_PROTOCOL_ENABLED=true.
  • Traffic is distributed across both Keycloak instances using round-robin.
  • Traefik checks each backend every 5s, marking a server as unhealthy if it does not respond within 3s. Unhealthy backends are automatically removed from the load balancer rotation and re-added once they recover.

Traefik health check:

When Traefik is set up for TLS passthrough and TCP services, it currently does not support an HTTPS or HTTP based health check for those services.

Making use of the available features, this setup uses a TCP-level send/expect check against Keycloak's management port (9000). As the Traefik's TCP check does not support TLS, Keycloak's management endpoint is configured to use plain HTTP (via KC_HTTP_MANAGEMENT_SCHEME=http) so that Traefik can parse the response.

The check sends a raw HEAD /health/ready HTTP/1.0 request and matches the expected HTTP response headers. This approach has known trade-offs:

  • Fragile: The expect string must match the full response headers exactly, including content-type charset. Any change to the Keycloak response format (e.g. a different charset) will break the check.
  • No TLS: The management port runs plain HTTP for this to work, which is acceptable since port 9000 is only accessible from the internal backend network.
  • Alternatives considered: A simple TCP connect check (no send/expect) would be less fragile but would not detect split-brain, database connectivity failures, or an overloaded node — it would only detect a dead process. Without a readiness probe, KC_SERVER_ASYNC_BOOTSTRAP would also need to be disabled, and graceful shutdown behaviour would need a different approach (manually removing the node before stopping it).

Note: Traefik does not yet support native HTTP health checks for TCP services. Track upstream support at traefik/traefik#12606. Once that feature is available, the send/expect workaround can be replaced with a cleaner HTTP check.

Resources