| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64quay.io/keycloak/keycloak:26.7.3nginx-1.26.3-6.el10_2.7.x86_64haproxy-3.0.5-6.el10_2.2.x86_64openssl-3.5.5-6.el10_2.x86_64 |
| Applies to | Any Linux host with Podman or Docker plus Nginx or HAProxy — Keycloak proxy-headers options match the official Quay image and ZIP distribution |
| Privilege | Normal user for rootless Podman; root or sudo when binding privileged ports or managing system Nginx/HAProxy |
| Lab environment | Keycloak on Podman plus host Nginx (primary) or HAProxy for edge and re-encrypt TLS — see Install Keycloak with Podman for the base container layout |
| Scope | Edge, re-encrypt, and passthrough layouts; KC_PROXY_HEADERS, trusted proxy addresses, PROXY protocol; /auth context paths; which paths to expose; working Nginx and HAProxy examples — not PEM/PKCS12 certificate setup on Keycloak itself. |
| Related guides | Configure Keycloak HTTPS/TLS with SSL certificates Keycloak environment variables Keycloak hostname |
In this article we will configure Keycloak behind a reverse proxy on Keycloak 26.7.3, using Nginx as the primary worked example and HAProxy as the equivalent.
A common Keycloak deployment places a reverse proxy or load balancer in front of Keycloak. Keycloak must trust forwarded headers so redirects, cookies, and the OIDC issuer stay on https:// with the public hostname. Certificate setup for Keycloak itself (re-encrypt or passthrough) lives in the HTTPS/TLS sibling listed under Related guides.
What this reverse-proxy lab teaches
At the end you will understand:
- Which termination pattern you are using: edge, re-encrypt, or passthrough
- Which Keycloak flags each pattern needs (
KC_HTTP_ENABLED,KC_PROXY_HEADERS,KC_PROXY_TRUSTED_ADDRESSES, orKC_PROXY_PROTOCOL_ENABLED) - How to overwrite
X-Forwarded-*headers in Nginx and HAProxy so clients cannot inject false values - Which application paths to expose publicly
- How to fix
403 Forbidden,http://redirects, and wrong issuer URLs behind a proxy
Edge (proxy terminates TLS, plain HTTP to Keycloak)
┌────────┐ TLS ┌───────┐ HTTP ┌───────────────┐
│ Client │ ─────► │ Proxy │ ─────► │ Keycloak :8080│
└────────┘ └───────┘ └───────────────┘
overwrite KC_HTTP_ENABLED
X-Forwarded-* KC_PROXY_HEADERS
KC_PROXY_TRUSTED_ADDRESSESChoose a TLS termination pattern
Pick the pattern before you write proxy configs.
| Pattern | TLS ends at | Keycloak backend | Typical Keycloak settings |
|---|---|---|---|
| Edge | Reverse proxy | HTTP 8080 |
KC_HTTP_ENABLED=true, KC_PROXY_HEADERS, KC_PROXY_TRUSTED_ADDRESSES, KC_HOSTNAME = public URL |
| Re-encrypt | Proxy (client side); Keycloak (back side) | HTTPS 8443 |
Keycloak KC_HTTPS_* + KC_PROXY_HEADERS + trusted addresses; restrict backend access to proxy |
| Passthrough | Keycloak (proxy does not decrypt) | HTTPS 8443; raw TLS tunnel |
Keycloak KC_HTTPS_*, KC_PROXY_PROTOCOL_ENABLED=true; do not set KC_PROXY_HEADERS |
Keycloak 26.x no longer uses the legacy proxy=edge single flag from WildFly-era tutorials. Edge and re-encrypt layouts use proxy-headers (forwarded or xforwarded) plus http-enabled when the hop from proxy to Keycloak is plain HTTP.
Never trust client-supplied forwarding identity. The last trusted proxy should overwrite the Forwarded / X-Forwarded-* values Keycloak is configured to consume. Advanced header filtering lists live in the official reverse-proxy guide linked under References.
Which Keycloak paths should the proxy expose?
Do not reverse-proxy the management port 9000. For the application listener (8080 or 8443), expose only what clients need:
| Path prefix | Typical exposure |
|---|---|
/realms/ |
Public — application realms need protocol endpoints |
/realms/master/ |
Restrict — administrative realm; preferably internal only |
/resources/ |
Public — theme and static assets for the login UI |
/.well-known/ |
Public — OIDC discovery helpers |
/admin/ |
Restrict — Admin Console; limit to operators or a private network |
/metrics, /health |
Do not expose publicly — these belong on management port 9000 |
Exact path policies depend on your threat model. The table follows Keycloak's reverse-proxy path guidance: keep application-realm front doors reachable, and keep the master realm, Admin Console, and ops endpoints off the public hostname when you can.
Configure the Keycloak backend for edge TLS
The proxy decrypts HTTPS from clients and forwards plain HTTP to Keycloak on 8080. Keycloak must trust forwarded headers so redirects use https:// externally:
| Variable | Edge value |
|---|---|
KC_HTTP_ENABLED |
true |
KC_PROXY_HEADERS |
xforwarded or forwarded |
KC_PROXY_TRUSTED_ADDRESSES |
TCP source address Keycloak sees for the proxy (see note below) |
KC_HOSTNAME |
Full public URL clients use (https://auth.example.com or https://auth.lab.local:18443) |
Your proxy must overwrite X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Port, and related headers — not append or pass through client-supplied values.
Set KC_PROXY_TRUSTED_ADDRESSES to the source address Keycloak sees for your reverse proxy. Keycloak evaluates the connecting proxy IP, not the value inside X-Forwarded-For. On this lab host (Podman bridge/netavark with a published port), host Nginx appears inside the container as 10.88.0.1 (the podman0 gateway) — not 127.0.0.1, even though Nginx listens on loopback. Docker, Kubernetes, rootless pasta, or a proxy on another host will show a different peer address.
Start Keycloak for the edge lab. Publish HTTP on a loopback port that only the proxy will call:
podman run -d --name kc-proxy-backend --replace \
-p 127.0.0.1:18080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-e KC_HTTP_ENABLED=true \
-e KC_HOSTNAME=https://auth.lab.local:18443 \
-e KC_PROXY_HEADERS=xforwarded \
-e KC_PROXY_TRUSTED_ADDRESSES=10.88.0.1 \
quay.io/keycloak/keycloak:26.7.3 startConfirm the flags before you wire the proxy:
podman run --rm \
-e KC_HOSTNAME=https://auth.lab.local:18443 \
-e KC_HTTP_ENABLED=true \
-e KC_PROXY_HEADERS=xforwarded \
-e KC_PROXY_TRUSTED_ADDRESSES=10.88.0.1 \
quay.io/keycloak/keycloak:26.7.3 show-config kc.hostname = https://auth.lab.local:18443 (ENV)
kc.proxy-headers = xforwarded (ENV)
kc.proxy-trusted-addresses = 10.88.0.1 (ENV)
kc.http-enabled = true (ENV)start disables HTTP by default, so edge mode must set KC_HTTP_ENABLED=true. See Keycloak start vs start-dev for why production mode expects this deliberate HTTP-or-HTTPS choice.
Nginx edge example
Create a lab certificate for the proxy front door (SAN must match the name clients use):
mkdir -p /tmp/kc-proxy-lab/certs /tmp/kc-proxy-lab/nginx
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout /tmp/kc-proxy-lab/certs/proxy.key.pem \
-out /tmp/kc-proxy-lab/certs/proxy.crt.pem \
-days 365 \
-subj '/CN=auth.lab.local' \
-addext 'subjectAltName=DNS:auth.lab.local,DNS:localhost'
chmod 644 /tmp/kc-proxy-lab/certs/proxy.crt.pem /tmp/kc-proxy-lab/certs/proxy.key.pemRun a standalone Nginx config that listens on 127.0.0.1:18443 and proxies to the Keycloak HTTP port. This avoids colliding with anything already bound on port 80. Use $remote_addr for X-Forwarded-For so Nginx replaces any client-supplied value instead of appending with $proxy_add_x_forwarded_for:
cat > /tmp/kc-proxy-lab/nginx/nginx-lab.conf <<'EOF'
worker_processes 1;
error_log /tmp/kc-proxy-lab/nginx/error.log;
pid /tmp/kc-proxy-lab/nginx/nginx.pid;
events { worker_connections 1024; }
http {
access_log /tmp/kc-proxy-lab/nginx/access.log;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 127.0.0.1:18443 ssl;
server_name auth.lab.local;
ssl_certificate /tmp/kc-proxy-lab/certs/proxy.crt.pem;
ssl_certificate_key /tmp/kc-proxy-lab/certs/proxy.key.pem;
location / {
proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 18443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
}
EOF
nginx -c /tmp/kc-proxy-lab/nginx/nginx-lab.conf -t
nginx -c /tmp/kc-proxy-lab/nginx/nginx-lab.confOn a production host you would usually drop a server { ... } block into /etc/nginx/conf.d/ and listen on 443, with the same overwrite headers and proxy_pass aimed at Keycloak.
After Keycloak logs show Listening on: http://0.0.0.0:8080, confirm basic connectivity through Nginx:
curl --noproxy '*' -sk -o /dev/null -w '%{http_code}\n' \
--resolve auth.lab.local:18443:127.0.0.1 \
https://auth.lab.local:18443/302Issuer and redirect checks are in Verify through the proxy below.
Verify through the proxy
Walk through these checks after the edge Nginx path works.
Redirect and scheme
Open the Admin Console URL users will bookmark. The location bar should stay on https:// after login redirects. If it drops to http://, KC_PROXY_HEADERS or the proxy’s X-Forwarded-Proto is wrong.
OIDC discovery issuer
curl --noproxy '*' -sk --resolve auth.lab.local:18443:127.0.0.1 \
https://auth.lab.local:18443/realms/master/.well-known/openid-configuration \
| grep -o '"issuer":"[^"]*"'"issuer":"https://auth.lab.local:18443/realms/master"The issuer must match the public URL — scheme, host, and port included.
HAProxy edge equivalent
HAProxy wants a combined certificate and key PEM. Build that file, then run an edge frontend on the same lab port. Explicitly replace forwarded headers instead of relying on option forwardfor alone:
cat /tmp/kc-proxy-lab/certs/proxy.crt.pem /tmp/kc-proxy-lab/certs/proxy.key.pem \
> /tmp/kc-proxy-lab/certs/proxy.pem
chmod 644 /tmp/kc-proxy-lab/certs/proxy.pem
mkdir -p /tmp/kc-proxy-lab/haproxy
cat > /tmp/kc-proxy-lab/haproxy/haproxy.cfg <<'EOF'
global
maxconn 256
defaults
mode http
timeout connect 5s
timeout client 50s
timeout server 50s
frontend fe_https
bind 127.0.0.1:18443 ssl crt /tmp/kc-proxy-lab/certs/proxy.pem
http-request set-header X-Forwarded-For %[src]
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
http-request set-header X-Forwarded-Port 18443
default_backend be_keycloak
backend be_keycloak
server kc1 127.0.0.1:18080 check
EOFStop the lab Nginx process if it still holds 18443, then start HAProxy:
haproxy -c -f /tmp/kc-proxy-lab/haproxy/haproxy.cfg
haproxy -D -f /tmp/kc-proxy-lab/haproxy/haproxy.cfg \
-p /tmp/kc-proxy-lab/haproxy/haproxy.pidRe-run the discovery check from the verification section. The issuer should still be https://auth.lab.local:18443/realms/master.
Re-encrypt: HTTPS + proxy headers
Same header overwrite rules as edge, but the proxy opens a second TLS connection to Keycloak. Use separate host ports so the public frontend and Keycloak backend do not collide:
| Role | Lab address |
|---|---|
| Nginx public HTTPS | 127.0.0.1:18443 |
| Keycloak backend HTTPS | 127.0.0.1:18444 → container 8443 |
Generate Keycloak backend certificates (or reuse the PEM steps from the HTTPS/TLS guide), then start Keycloak with HTTPS and proxy headers:
# Backend certs for Keycloak (SAN auth.lab.local)
mkdir -p /tmp/kc-proxy-lab/kc-certs
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout /tmp/kc-proxy-lab/kc-certs/server.key.pem \
-out /tmp/kc-proxy-lab/kc-certs/server.crt.pem \
-days 365 \
-subj '/CN=auth.lab.local' \
-addext 'subjectAltName=DNS:auth.lab.local,DNS:localhost'
chmod 644 /tmp/kc-proxy-lab/kc-certs/server.crt.pem \
/tmp/kc-proxy-lab/kc-certs/server.key.pem
podman run -d --name kc-proxy-reencrypt --replace \
-v /tmp/kc-proxy-lab/kc-certs:/opt/keycloak/conf:Z \
-p 127.0.0.1:18444:8443 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-e KC_HTTP_ENABLED=false \
-e KC_HOSTNAME=https://auth.lab.local:18443 \
-e KC_PROXY_HEADERS=xforwarded \
-e KC_PROXY_TRUSTED_ADDRESSES=10.88.0.1 \
-e KC_HTTPS_CERTIFICATE_FILE=/opt/keycloak/conf/server.crt.pem \
-e KC_HTTPS_CERTIFICATE_KEY_FILE=/opt/keycloak/conf/server.key.pem \
quay.io/keycloak/keycloak:26.7.3 startPoint Nginx at the backend HTTPS port. Enable SNI and trust the Keycloak certificate so verification works against auth.lab.local even though the upstream IP is 127.0.0.1:
cat > /tmp/kc-proxy-lab/nginx/nginx-reencrypt.conf <<'EOF'
worker_processes 1;
error_log /tmp/kc-proxy-lab/nginx/error-reencrypt.log;
pid /tmp/kc-proxy-lab/nginx/nginx-reencrypt.pid;
events { worker_connections 1024; }
http {
access_log /tmp/kc-proxy-lab/nginx/access-reencrypt.log;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 127.0.0.1:18443 ssl;
server_name auth.lab.local;
ssl_certificate /tmp/kc-proxy-lab/certs/proxy.crt.pem;
ssl_certificate_key /tmp/kc-proxy-lab/certs/proxy.key.pem;
location / {
proxy_pass https://127.0.0.1:18444;
proxy_http_version 1.1;
proxy_ssl_server_name on;
proxy_ssl_name auth.lab.local;
proxy_ssl_trusted_certificate /tmp/kc-proxy-lab/kc-certs/server.crt.pem;
proxy_ssl_verify on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 18443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
}
EOFStop any earlier lab Nginx or HAProxy that still holds 18443, then start the re-encrypt config:
nginx -c /tmp/kc-proxy-lab/nginx/nginx-reencrypt.conf -t
nginx -c /tmp/kc-proxy-lab/nginx/nginx-reencrypt.confAfter Keycloak logs show HTTPS listening on 8443, confirm discovery through the public proxy port:
curl --noproxy '*' -sk \
--resolve auth.lab.local:18443:127.0.0.1 \
https://auth.lab.local:18443/realms/master/.well-known/openid-configuration \
| grep -o '"issuer":"[^"]*"'"issuer":"https://auth.lab.local:18443/realms/master"Restrict network access so only the proxy can reach Keycloak on 18444 — otherwise clients could connect directly and forge headers.
Passthrough: HTTPS + PROXY protocol
The proxy forwards the raw TLS stream. Keycloak terminates TLS and must hold a certificate valid for the public hostname. In passthrough mode, enable PROXY protocol on both the load balancer and Keycloak if Keycloak needs the original client IP:
- On Keycloak:
KC_PROXY_PROTOCOL_ENABLED=true - On the load balancer: enable
send-proxy(HAProxy) or the equivalent PROXY protocol option - Do not set
KC_PROXY_HEADERS— Keycloak documents thatproxy-protocol-enabledandproxy-headerscannot be used together, and the proxy cannot inject HTTP headers into encrypted traffic anyway
HAProxy TCP passthrough sketch:
frontend fe_tls
mode tcp
bind *:443
default_backend be_keycloak_tls
backend be_keycloak_tls
mode tcp
server kc1 10.0.0.20:8443 send-proxy-v2Use Keycloak HTTPS certificates from the HTTPS/TLS guide for the backend listener.
Run Keycloak under /auth behind a reverse proxy
A common public URL is https://example.com/auth instead of a dedicated hostname such as https://auth.example.com/. Keycloak supports that layout when the proxy and Keycloak agree on the context path. Three supported approaches:
- Full hostname URL — set
KC_HOSTNAME=https://example.com/authso scheme, host, and path are fixed. X-Forwarded-Prefix— withKC_PROXY_HEADERS=xforwarded, have the proxy setX-Forwarded-Prefix: /authwhen the external path differs from what Keycloak serves.KC_HTTP_RELATIVE_PATH=/auth— Keycloak itself serves under/auth, and the proxy forwards that path without rewriting it away.
Avoid arbitrary URL rewriting that strips or invents /auth without aligning one of those options. Mismatched path settings produce broken Admin Console assets, wrong redirect URIs, and discovery documents that clients cannot use.
Hostname v2 details for split admin URLs and strict mode are covered in the hostname sibling under Related guides.
Common problems
| Symptom | Likely cause | Fix |
|---|---|---|
403 Forbidden via proxy only |
Origin check sees wrong scheme/host, or headers from an untrusted address | Enable KC_PROXY_HEADERS; set KC_PROXY_TRUSTED_ADDRESSES; overwrite forwarded headers at the proxy |
Redirects use http:// behind proxy |
Missing or wrong X-Forwarded-Proto |
Set X-Forwarded-Proto: https at the proxy; confirm KC_PROXY_HEADERS |
| OIDC issuer shows internal host:port | KC_HOSTNAME wrong or headers not trusted |
Set full public KC_HOSTNAME; fix forwarded host/port; confirm trusted addresses |
| Admin Console redirect loop | Hostname does not match public URL | Align KC_HOSTNAME with the URL clients reach through the proxy |
Production start exits with no HTTP listener |
Edge layout forgot KC_HTTP_ENABLED=true |
Enable HTTP for edge, or configure HTTPS on Keycloak for re-encrypt/passthrough |
| Passthrough but wrong client IP | PROXY protocol off on Keycloak or load balancer | KC_PROXY_PROTOCOL_ENABLED=true on Keycloak; enable send-proxy on the load balancer |
proxy-headers and PROXY protocol both set |
Mutually exclusive options | Use one mode: headers for edge/re-encrypt, PROXY protocol for passthrough |
Broken UI under /auth |
Path rewrite without Keycloak alignment | Use full KC_HOSTNAME with /auth, X-Forwarded-Prefix, or KC_HTTP_RELATIVE_PATH |
Summary
Keycloak behind a reverse proxy starts with choosing where TLS terminates. Edge mode terminates TLS at Nginx or HAProxy, enables HTTP on Keycloak, and relies on KC_PROXY_HEADERS plus KC_PROXY_TRUSTED_ADDRESSES so redirects and the OIDC issuer stay on the public https:// URL. Re-encrypt adds a second TLS hop to Keycloak on a separate backend port and still needs proxy headers. Passthrough keeps TLS on Keycloak and uses the PROXY protocol instead of header injection.
Overwrite forwarded headers at the proxy. Expose login and discovery paths carefully, and keep management port 9000 off the public proxy. After every change, confirm the Admin Console stays on https:// and that discovery returns the expected issuer.
References
- Configuring a reverse proxy — edge, re-encrypt, passthrough,
proxy-headers, trusted addresses, exposed paths, PROXY protocol - Configuring the hostname (v2) — full URL hostname with proxies and context paths
- Configuring TLS — Keycloak-side certificates for re-encrypt and passthrough
- NGINX Reverse Proxy —
proxy_passand upstream TLS options - HAProxy Configuration Manual — header replacement, SSL bind, PROXY protocol
Frequently Asked Questions
Do I still set proxy=edge in Keycloak 26.x?
No. Current Keycloak uses proxy-headers (forwarded or xforwarded) plus http-enabled when the proxy speaks HTTP to the backend. The old proxy=edge flag from pre-Quarkus guides is gone.
Can I use the same certificate on the proxy and Keycloak?
In re-encrypt mode the proxy and Keycloak each need TLS material — they can be different certificates from the same CA. In edge mode only the proxy needs the public certificate; Keycloak runs HTTP behind the proxy.
Why do I get 403 only through the proxy?
Keycloak is checking the request origin using scheme and host. Without trusted forwarded headers it sees plain HTTP from the proxy hop. Set KC_PROXY_HEADERS and KC_PROXY_TRUSTED_ADDRESSES, and make the proxy overwrite X-Forwarded-Proto and related headers.
Should management health go through the same proxy?
No. Keep port 9000 on an internal network. Proxy only the application port that serves the Admin Console and OIDC endpoints.

