Keycloak Behind a Reverse Proxy

To authenticate on a Tailscale network overlay, I need an Identity Provider (IdP). Keycloak is an open source IdP written in Java that I can use for this purpose. I want to run Keycloak behind the Caddy reverse proxy, to benefit from its security, post-quantum cryptography, and especially its certificate automation. I used an Ubuntu Server 24.04 LTS VPS for this service.

Installing Caddy

For reasons relating to cryptography, I wanted to make sure I had the latest version of Caddy installed with the latest version of Go, so I installed Go from apt, used it to compile the latest version (Go 1.24.1), then removed the apt version. A little bit of symlink spaghetti later, I ran go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest. This allowed me to build Caddy with xcaddy: xcaddy build --with github.com/caddy-dns/cloudflare.

The importance of enabling the caddy-dns/cloudflare module is that I wanted to disable port 80 http traffic completely. This leaves DNS as the only way to do ACME server proof of ownership of my domain - I can’t have a .well-known file accessible with a flat HTTP.

Caddyfile

The resulting Caddyfile looked like this:

https://admin.auth.XXXXXXXX.com https://auth.XXXXXXXX.com {
	# Enable HTTPS (Caddy handles this automatically)
	tls {
		dns cloudflare {env.CLOUDFLARE_AUTH_TOKEN}
	}

	# Log settings
	log {
		output file /var/log/caddy/auth.XXXXXXXX.com.log {
			mode 600
			roll_size 100MiB
			roll_uncompressed
			roll_local_time
			roll_keep 10
		}
		format console
	}

	# Reverse proxy directive
	reverse_proxy https://localhost:8443 {
		# Add headers to the proxied request
		header_up Host {host}
		header_up X-Real-IP {remote}
		header_up X-Forwarded-For {remote}
		header_up X-Forwarded-Proto {scheme}

		# Timeouts
		transport http {
			tls_insecure_skip_verify
			dial_timeout 5s
			response_header_timeout 30s
		}
	}
}

Systemd

This service can be managed by systemctl enable caddy, to restart it if it goes down.

Postgres

I decided to use Postgres for the Keycloak installation. There were some permissions to grant to allow Keycloak to create the tables it wanted to, but the setup was fairly simple.

Keycloak

Keycloak HTTPS Passthrough

I created a folder /opt/keycloak/keycloak-26.1.3. I had originally been hoping that Caddy could be the TLS endpoint and Keycloak would accept flat http communication. It wouldn’t be directly accessible from outside the server (because of its own settings and a firewall), but could receive X-Forwarded-For headers and similar.

Unfortunately Keycloak’s settings are a little complex and sparsely documented, and I wasn’t able to get that working. It seems it was mostly intended to be directly exposed, so I decided to use HTTPS passthough instead. Caddy acts as a TLS endpoint, but then queries Keycloak at https://localhost:8443.

This meant Keycloak having TLS certificates that weren’t really of any security value, so I created a self-signed one with a 3,650 day time to live, and put the files at /opt/keycloak/keycloak-26.1.3/conf/server.crt and .../server.key.

Keycloak Config

The config file at /opt/keycloak/keycloak-26.1.3/conf/keycloak.conf ended up looking like this:

hostname-url=http://auth.XXXXXXXX.com
hostname-admin-url=http://admin.auth.XXXXXXXX.com
hostname-strict=true

hostname=https://auth.XXXXXXXX.com
hostname-admin=https://admin.auth.XXXXXXXX.com
http-relative-path=/

https-certificate-file=/opt/keycloak/keycloak-26.1.3/conf/server.crt
https-certificate-key-file=/opt/keycloak/keycloak-26.1.3/conf/server.key

http-enabled=true
http-port=8080
https-port=8443
health-enabled=true
proxy-headers=forwarded
proxy-trusted-addresses=127.0.0.0/8
#proxy-protocol-enabled=true

db=postgres
db-username=keycloak
db-password=XXXXXXXX
db-url=jdbc:postgresql://localhost:5432/keycloak

hostname-path=/

It’s not clear how much of this could safely be removed at this point, but the Keycloak configuration was not aided much by the docs, and I’m not keen to mess around with it any further now it’s working.

Managing Keycloak with Systemd

To use systemctl to manage Keycloak, it was necessary to put a config file at /etc/systemd/system/keycloak.service with the following contents:

[Unit]
Description=Keycloak Server
After=network.target

[Service]
Type=simple
User=keycloak
Group=keycloak
WorkingDirectory=/opt/keycloak/keycloak-26.1.3/
ExecStart=/usr/bin/keycloak start
Restart=always

[Install]
WantedBy=multi-user.target

Note that there is a symlink in this user’s path called keycloak: it is pointing to /opt/keycloak/keycloak-26.1.3/bin/kc.sh.

We can now use systemctl enable keycloak and systemctl start keycloak to keep the service alive.

Further Securing the Server

ufw firewall has been used on this server. I don’t need anything other than ports 22 and 443 exposed to the internet, so everything else was blocked coming in. The SSH settings were changed to disallow root access, and disallow logging in with a password. The SSH authorized key has a passphrase, and SSH brute-forcing is blocked by fail2ban.

The postgres, keycloak and caddy services, all run under systemctl, each have their own linux user. There is also a user account for maintenance.

I also rebuild Caddy using xcaddy again, this time with the Coraza web application firewall for improved appsec.