Reverse Proxy and Keycloak Configuration

When deploying a Keycloak behind a reverse proxy, it’s crucial to understand a relationship between Keycloak’s configuration arguments and an underlying reverse proxy concepts. Based on your security requirements, you must choose between SSL termination and TLS passthrough, and configure a session affinity accordingly in your reverse proxy. There is an article on Keycloak documentation that covers parts of topic, while I had have a lot of questions after reading it. Let’s explore the topic in more detail.

Brief Intro OR What is Keycloak?

Keycloak is an open source Identity and Access Management solution for modern applications and services. It provides out-of-the-box functionality for user registration, authentication, Single Sign-On (SSO), Identity Brokering, User Federation, Social Login, and more.

Infrastructure

To run Keycloak in production, you need to set up the necessary infrastructure.

  1. Database: Keycloak requires a database to store user data, sessions, and other information. You can use PostgreSQL, MySQL, or another supported database.
  2. Application Server: Keycloak runs on an application server such as WildFly or JBoss EAP.
  3. Reverse Proxy: We will use Nginx to simulate a load balancer. It will handle SSL termination, routing, and other tasks that is not a point of such article.

For a production environment, you could use an orchestration tool like Kubernetes or OpenShift to run Keycloak in a cluster. An official Kubernetes Operator and several Helm charts are available. I have personally used codecentric’s helm chart, which is well-maintained, not bloated, has good documentation, and is based on an old good StatefulSet, making it clear for any developers.

In this article, I will use Docker Compose to simulate production use Keycloak, but not overcomplicate sample.

version: '3.8'

services:
  postgres:
    image: postgres:16.2
    container_name: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password
    networks:
      - keycloak_network
    restart: always

  keycloak1:
    image: quay.io/keycloak/keycloak:26.1.1
    environment:
      # --- Database Configuration ---
      - KC_DB=postgres
      - KC_DB_URL_HOST=postgres
      - KC_DB_URL_DATABASE=keycloak
      - KC_DB_USERNAME=keycloak
      - KC_DB_PASSWORD=password
      - KC_DB_SCHEMA=public

      # --- Administrator Credentials ---
      - KC_BOOTSTRAP_ADMIN_PASSWORD=admin
      - KC_BOOTSTRAP_ADMIN_USERNAME=admin

      # --- Proxy and Hostname Configuration ---
      - KC_FEATURES=hostname:v2
      # - KC_PROXY_PROTOCOL_ENABLED=true
      - KC_PROXY_HEADERS=forwarded
      - KC_HOSTNAME=http://localhost:8080
      # - KC_HOSTNAME=https://localhost:8443
      - KC_HOSTNAME_STRICT=false
      - KC_HTTP_ENABLED=true

      # --- Logging and Metrics ---
      - KC_LOG_LEVEL=info
      - KC_METRICS_ENABLED=true
      - KC_HEALTH_ENABLED=true


    command: start
    depends_on:
      - postgres
    networks:
      - keycloak_network
    volumes:
      - ./ssl:/etc/keycloak/ssl
    restart: always

  keycloak2:
    # same as keycloak1
    # needs to represent a second instance for load balancing

  nginx:
    image: nginx:latest
    container_name: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      # - ./ssl:/etc/nginx/ssl
    ports:
      - "8080:8080"
      - "8443:8443"
    depends_on:
      - keycloak1
      - keycloak2
    networks:
      - keycloak_network
    restart: always

volumes:
  postgres_data:
    driver: local

networks:
  keycloak_network:
    driver: bridge

So we have a Posgre databse, two Keycloak instances, and an Nginx reverse proxy. Pay attention to environment variables in Keycloak services. We will use them to configure Keycloak to work behind a reverse proxy. There are few variable that you should provide for make Keycloak work behind a reverse proxy - KC_HOSTNAME and KC_PROXY_HEADERS.

Forwarded Header

If you do not have strict security requirements, you can simplify your infrastructure by using your reverse proxy to handle SSL termination and forward requests to Keycloak. In this case, you can use the Forwarded header and implement session affinity based on either the client’s IP address or cookies.

Note: Since Nginx only provides cookie-based affinity in it’s Plus version, we will use IP-based affinity. However, your load balancer might provide cookie-based affinity for free.

# /etc/nginx/nginx.conf with HTTP proxying
http {
   include       /etc/nginx/mime.types;
   default_type  application/octet-stream;
   access_log    /var/log/nginx/access.log;
   sendfile      on;
   keepalive_timeout  65;

   # --- Upstream for Keycloak ---
   upstream keycloak_upstream {
       ip_hash; # Simple IP-based session affinity
       server keycloak1:8080;
       server keycloak2:8080;
   }

   # --- HTTP Server ---
   # This block handles all incoming HTTP traffic on port 80.
   server {
       listen 8080;
       # Main proxy for all Keycloak endpoints under
       location / {
           # Forward the request to the Keycloak upstream group
           proxy_pass http://keycloak_upstream/;

           # --- Proxy Headers ---
           proxy_set_header Forwarded "for=$proxy_add_x_forwarded_for;host=$host;proto=$scheme";
       }
   }
}
# Keycloak configuration
keycloak1:
  # and keycloak2
  image: quay.io/keycloak/keycloak:26.1.1
  # ...
  environment:
    # ...
    # --- Proxy and Hostname Configuration ---
    - KC_PROXY_HEADERS=forwarded
    # ...

SSL Termination

It is way easier to handle SSL termination on your reverse proxy, than on Keycloak itself.

First, let’s generate self-signed certificates.

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl.key -out ssl.crt -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"

Add it to your nginx configuration:

nginx:
  # ...
  volumes:
    - ./ssl/ssl.crt:/etc/nginx/ssl.crt
    - ./ssl/ssl.key:/etc/nginx/ssl.key
http {
   # ... other configuration ...
   server {
       listen 8443 ssl;
       ssl_certificate /etc/nginx/ssl.crt;
       ssl_certificate_key /etc/nginx/ssl.key;
   }
}

And update Keycloak configuration to use HTTPS:


keycloak1:
  # and keycloak2
  image: quay.io/keycloak/keycloak:26.1.1
  # ...
  environment:
    # --- Proxy and Hostname Configuration ---
    - KC_HOSTNAME=https://localhost:8443
nginx:
  # ...
  volumes:
    - ./ssl/ssl.crt:/etc/nginx/ssl.crt
    - ./ssl/ssl.key:/etc/nginx/ssl.key

That’s it! No complexity at all.

SSL Termination and HTTPS in Keycloak

Potentially you can use ssl termination and HTTPS in keycloak, but it is questionable solution, it would be better to switch to TLS passthrough in this case!

X-Forwarded Headers

If you want to use old-fashioned x-forwarded headers, you can use it as well. No one judges you for that. I promise.

Proxy Protocol

The PROXY protocol works on top of TCP and allows a 3/4 layer (TCP-level) reverse proxy to pass client connection information to the backend server. Since you cannot modify HTTP headers at this level, your only option is to use TLS passthrough. This also means you have no choose but able to use only IP-based session affinity.

# /etc/nginx/nginx.conf with TCP stream proxying
stream {
    upstream keycloak {
        hash $remote_addr consistent;
        server keycloak1:8080;
        server keycloak2:8080;
    }

    server {
        listen 8080;
        proxy_pass keycloak;
        proxy_protocol on;
    }
}
# Keycloak docker-compose.yml configuration
# Apply to both keycloak1 and keycloak2 services
environment:    
    # --- Proxy and Hostname Configuration ---
    - KC_PROXY_PROTOCOL_ENABLED=true

TLS Passthrough

The main benefit of this method is end-to-end encryption. The encrypted connection is maintained from the user to the Keycloak server, meaning no one, not even your reverse proxy, can see decrypted traffic. If you have strict security requirements, you must use TLS passthrough and decrypt traffic only within the Keycloak process.

We will reuse the self-signed certificates we generated earlier. But transform them to PEM format, which is required by Keycloak.

openssl rsa -in ssl.key -text > private.pem
openssl x509 -inform PEM -in ssl.crt > public.pem

Next, add the certificate configuration to Keycloak by https-certificate-file and https-certificate-key-file parameters, disable HTTP:

keycloak1:
    # and keycloak2
    image: quay.io/keycloak/keycloak:26.1.1
    # ...
    environment:
      # ...
      # --- Proxy and Hostname Configuration ---
      - KC_HOSTNAME=https://localhost:8443
      - KC_HTTP_ENABLED=false # or remove it at all
      # --- TLS Configuration ---
      - KC_HTTPS_CERTIFICATE_FILE=/etc/keycloak/ssl/public.pem
      - KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/keycloak/ssl/private.pem

    volumes:
      - ./ssl:/etc/keycloak/ssl

Propagate connection throw the reverse proxy to Keycloak using the PROXY protocol:


```nginx
# ...
stream {
    upstream keycloak {
        hash $remote_addr consistent;
        server keycloak1:8443;
        server keycloak2:8443;
    }
    server {
        listen 8443;
        proxy_pass keycloak;
        proxy_protocol on;
    }
}

Summary of Reverse Proxy Configurations

Method SSL/TLS Handling Session Affinity
PROXY Protocol TLS Passthrough IP-based
Forwarded Header SSL Termination IP-based / Cookie-based
X-Forwarded Headers SSL Termination IP-based / Cookie-based

Since we have introduced session affinity on revers proxy that not rely on keycloak’s AUTH_SESSION_ID cookie, you can disable Keycloak’s sticky session cookie.

# Add to keycloak1 and keycloak2 services in docker-compose.yml
environment:
    # ...
    # --- Cache and Session ---
    - KC_SPI_STICKY_SESSION_ENCODER__INIFINISPAN__SHOULD_ATTACH_ROUTE=false
    # ...

Please note that if you cannot introduce session affinity, it is not critical while reduce performance due to distributed cache misses.

Sources: