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.
- Database: Keycloak requires a database to store user data, sessions, and other information. You can use PostgreSQL, MySQL, or another supported database.
- Application Server: Keycloak runs on an application server such as WildFly or JBoss EAP.
- 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: