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.