Configure Keycloak Behind a Reverse Proxy

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.7.3
nginx-1.26.3-6.el10_2.7.x86_64
haproxy-3.0.5-6.el10_2.2.x86_64
openssl-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
Short on time?Beta Get the gist of this 12-minute read in seconds, then ask follow-up questions

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, or KC_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_ADDRESSES

Choose 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:

bash
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 start

Confirm the flags before you wire the proxy:

bash
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
output
	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):

bash
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.pem

Run 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:

bash
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.conf

On 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:

bash
curl --noproxy '*' -sk -o /dev/null -w '%{http_code}\n' \
  --resolve auth.lab.local:18443:127.0.0.1 \
  https://auth.lab.local:18443/
output
302

Issuer 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

bash
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":"[^"]*"'
output
"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:

bash
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
EOF

Stop the lab Nginx process if it still holds 18443, then start HAProxy:

bash
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.pid

Re-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:

bash
# 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 start

Point 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:

bash
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;
        }
    }
}
EOF

Stop any earlier lab Nginx or HAProxy that still holds 18443, then start the re-encrypt config:

bash
nginx -c /tmp/kc-proxy-lab/nginx/nginx-reencrypt.conf -t
nginx -c /tmp/kc-proxy-lab/nginx/nginx-reencrypt.conf

After Keycloak logs show HTTPS listening on 8443, confirm discovery through the public proxy port:

bash
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":"[^"]*"'
output
"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 that proxy-protocol-enabled and proxy-headers cannot be used together, and the proxy cannot inject HTTP headers into encrypted traffic anyway

HAProxy TCP passthrough sketch:

text
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-v2

Use 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:

  1. Full hostname URL — set KC_HOSTNAME=https://example.com/auth so scheme, host, and path are fixed.
  2. X-Forwarded-Prefix — with KC_PROXY_HEADERS=xforwarded, have the proxy set X-Forwarded-Prefix: /auth when the external path differs from what Keycloak serves.
  3. 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)