Keycloak SSO with docker compose and nginx
Published: 2024-02-11, Revised: 2024-04-20
TL;DR I always hesitated to deploy an extra tool for user management and SSO, but the current state of the web makes it very difficult to keep up with security, CVEs etc. Why not trust one of the longest standing solutions for identity and access management? Keycloak is open source, interoperable with major SSO protocols (OpenID Connect (OIDC), OAuth 2.0, SAML
), and robust. The setup with docker compose is not complicated, but it was not straight forward either. This is why I provide a summary of the process below.
Info
This is currently a stub. I thought I would share my docker-compose.yml
and nginx.conf
quickly and update the post later to add steps for theming and integration.
Concept#
You may have seen the concept below already in my previous post about Mastodon. We will use a standard setup of nginx as a central reverse proxy that forwards traffic through localhost to individual services, all running in their own rootless docker namespaces. I consider this the typical economical setup, by sharing resources of a single host but with maximally isolated environments. Adapt where this does not fit your usecase.
Web | | 0.0.0.0:80 0.0.0.0:443 +------------------------------------------------------+-----------------------------------------------------+ | | | | v | | +------------------------------- nginx/acme -----------------------------+ | | | | | | | http://127.0.0.1:3000 | | | | http://127.0.0.1:4000 http://127.0.0.1:8080 http://127.0.0.1:9999 | | +--------------+---------------+ +--------------+---------------+ +-------------+----------------+ | | | | | | | | | | | | | | Rootless Docker Service | | Rootless Docker Service | | Rootless Docker Service | | | | +---------+----------+ | | +---------+----------+ | | +--------+-----------+ | | | | | | | | | | | | | | | | | | | | | | v | | | | v | | | | v | | | | | | Mastdon Docker | | | | Keycloak Docker | | | | Nextcloud Docker | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +--------------------+ | | +--------------------+ | | +--------------------+ | | | | | | | | | | | +------------------------------+ +------------------------------+ +------------------------------+ | | | +------------------------------------------------------------------------------------------------------------+
Preparations#
You will need some basic tools:
- SSH
- A VM with Linux (Ubuntu; Debian etc.)
- A domain or subdomain where you can add an
A
(and optionallyAAAA
) record for your service
Follow the Mastodon post for basic setup of docker rootless to:
- create a new non-root user named
keycloak
, without password, with its home directory set to/srv/keycloak
- update
/etc/subuid
and/etc/subgid
ranges with userkeycloak
(because e.g. the postgres container will need these to create a nested non-root user itself) - install docker rootless through
dockerd-rootless-setuptool.sh
and configure automatic service start for thekeycloak
user
Keycloak Setup#
Login to the newly created keycloak
user.
machinectl shell keycloak@
Warning
We need to use machinectl
to login, otherwise
XDG_RUNTIME_DIR
environment variables will not be available.
Do not use (e.g.) sudo -u keycloak -H bash
.
Create directories for persistent data (data/postgres16
) and the docker files.
cd ~ \
&& mkdir -p data/postgres16 \
&& mkdir docker && cd docker
nano docker-compose.yml
docker-compose.yml#
The official docs provide some information here 1. But they use docker run
, which would be unusual in production.
Going to a compose file is not complicated and allows us to have a more reproducable setup. There are some example docker-compose.yml
s available, such as 2, 3 or 4.
We will start with a docker-compose.yml
that directly uses the official keycloak image. This can be changed later.
services:
postgres_db:
image: postgres:16
volumes:
- /srv/keycloak/data/postgres16:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: ${POSTGRES_USER:-keycloak}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-eX4mP13p455w0Rd}
keycloak:
# build: .
image: quay.io/keycloak/keycloak:23.0.6
environment:
KC_LOG_LEVEL: debug
KC_DB: postgres
KC_DB_URL: 'jdbc:postgresql://postgres_db/keycloak'
KC_DB_USERNAME: ${POSTGRES_USER:-keycloak}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-eX4mP13p455w0Rd}
KC_DB_SCHEMA: public
KC_HOSTNAME: ${KC_HOSTNAME:-your.tld.com}
KC_HOSTNAME_STRICT_HTTPS: true
KC_HOSTNAME_STRICT: true
KC_PROXY: edge
HTTP_ADDRESS_FORWARDING: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-eX4mP13p455w0Rd}
# command: start --optimized
ports:
- '127.0.0.1:8080:8080'
depends_on:
- postgres_db
Create an .env
file with your sensitive and variable information:
nano .env
Contents (change passwords):
# DB Password
POSTGRES_PASSWORD=eX4mP13p455w0Rd
# admin password
KEYCLOAK_ADMIN_PASSWORD=eX4mP13p455w0Rd
# domain
KC_HOSTNAME=your.tld.com
The relevant documentation of all these variables can be found here 5.
Note
Optionally initialize a .git
repository in ~/docker
now, create a .gitignore
and add .env
to it, and commit the docker-compose.yml
, for tracking changes.
Some explanations for the docker-compose.yml
above
'127.0.0.1:8080:8080'
andKC_PROXY: edge
will make keycloak listen on localhost- the syntax for env variables (
${KC_HOSTNAME:-your.tld.com}
) means:
1. useKC_HOSTNAME
from.env
, if available;
2. otherwise substitute a default value (your.tld.com
) - note (e.g.) that we do not set
KC_DB_USERNAME
, which means: use the default image: quay.io/keycloak/keycloak:23.0.6
references a specific image tag, as is suggested for production (only use:latest
for development).KC_LOG_LEVEL: debug
can be commented out once we are done with development- bind-mount the persistent postgres data from the subfolder created earlier in the user's home directory:
services: postgres_db: image: postgres:16 volumes: - /srv/keycloak/data/postgres16:/var/lib/postgresql/data
- if you want to start over from scratch, simply delete this postgres folder and it will be re-initialized on next startup:
CTRL+D sudo rm -rf /srv/keycloak/data/postgres16 sudo machinectl shell keycloak@ cd ~ && mkdir -p data/postgres16
/srv/keycloak/data/postgres16
holds the persistent data that would need periodic backups
Test locally#
At this stage, we can test the docker compose stack:
docker compose up -d && docker compose logs --follow
Afterwards, create a reverse SSH tunnel to your VM and the keycloak local port.
ssh you@111.11.11.11 -L :8080:127.0.0.1:8080 -p 22 -N -v
Open 127.0.0.1:8080
in your browser and you should be greeted with the keycloak welcome screen:
nginx#
Logout from the keycloak user with CTRL+D.
Follow the Mastodon post for setup of nginx as a system reverse proxy.
Create a new nginx .conf
for the keycloak service.
Note
From here on, wherever you see your.tld.com
: replace with your actual domain.
Note
At this stage, you should head to your domain registrar and add an A
record to forward DNS queries to your VM's IP.
nano /etc/nginx/sites-available/your.tld.com.conf
ln -s /etc/nginx/sites-available/your.tld.com.conf /etc/nginx/sites-enabled/
We can find some relevant information in the keycloak docs 6.
Info
I recommend to use the Mozilla SSL configurator to generate best practice defaults for nginx 7. Make sure to update with your nginx version (nginx -v
).
server {
if ($host = your.tld.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name your.tld.com;
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your.tld.com;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /etc/nginx/dhparam/dhparam;
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# verify chain of trust of OCSP response using Root CA and Intermediate certs
# ssl_trusted_certificate /etc/letsencrypt/live/your.tld.com/chain.pem;
access_log /var/log/nginx/your.tld.com-access.log;
error_log /var/log/nginx/your.tld.com-error.log;
location / {
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 $scheme;
proxy_pass http://127.0.0.1:8080;
# the following headers are needed, if your application uses redirection flow to authenticate with Keycloak.
# replace http://127.0.0.1:8080 with the application server url
# add_header Content-Security-Policy "frame-src *; frame-ancestors *; object-src *;";
# add_header Access-Control-Allow-Origin 'http://127.0.0.1:8080';
# add_header Access-Control-Allow-Credentials true;
}
# ssl_certificate /etc/letsencrypt/live/your.tld.com/fullchain.pem; # managed by Certbot
# ssl_certificate_key /etc/letsencrypt/live/your.tld.com/privkey.pem; # managed by Certbot
}
Test the configuration and reload nginx.
nginx -t
systemctl reload nginx
Use certbot
to request SSL certificates for your service.
certbot --nginx -d your.tld.com
This will automatically update necessary lines in your.tld.com.conf
.
Edit your.tld.com.conf
and uncomment ssl_trusted_certificate
.
# verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/letsencrypt/live/your.tld.com/chain.pem;
Reload nginx
systemctl reload nginx
Debug#
You can now open your.tld.com
and login to keycloak using the admin
user with the password from the .env
file.
For debugging, the first stop are the docker compose logs.
docker compose logs --follow
For nginx, follow the access and error logs.
tail -f /var/log/nginx/your.tld.com-access.log;
tail -f /var/log/nginx/your.tld.com-error.log;
If you need to have a look at the keycloak database.
machinectl shell keycloak@
cd ~/docker
docker compose exec postgres_db /bin/bash
psql -h localhost -p 5432 -U keycloak keycloak
SELECT ...;
Custom build with Dockerfile#
So far, we are using the prebuild image from quay.io.
For any customization, e.g. in order to use themes and run the keycloak container in --optimized
mode 8, we need to build our own image.
We will utilize a multi-stage docker build 9 starting with the official quay.io image.
Add a Dockerfile
cd ~/docker
nano Dockerfile
.. with the following content.
FROM quay.io/keycloak/keycloak:23.0.6 as builder
# Configure a database vendor
ENV KC_DB=postgres
WORKDIR /opt/keycloak
# COPY --from=keycloakify_jar_builder /opt/app/build_keycloak/target/ keycloakify-starter-keycloak-theme-5.1.3.jar /opt/keycloak/providers/
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:23.0.6
COPY --from=builder /opt/keycloak/ /opt/keycloak/
# Add ENTRYPOINT
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
Afterwards, edit the docker-compose.yml
:
- remove or comment out the
image:
line - uncomment
build: .
- add
command: start --optimized
Final docker-compose.yml
services:
postgres_db:
image: postgres:16
volumes:
- /srv/keycloak/data/postgres16:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: ${POSTGRES_USER:-keycloak}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-eX4mP13p455w0Rd}
keycloak:
build: .
environment:
KC_LOG_LEVEL: debug
KC_DB: postgres
KC_DB_URL: 'jdbc:postgresql://postgres_db/keycloak'
KC_DB_USERNAME: ${POSTGRES_USER:-keycloak}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-eX4mP13p455w0Rd}
KC_DB_SCHEMA: public
KC_HOSTNAME: ${KC_HOSTNAME:-your.tld.com}
KC_HOSTNAME_STRICT_HTTPS: true
KC_HOSTNAME_STRICT: true
KC_PROXY: edge
HTTP_ADDRESS_FORWARDING: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-eX4mP13p455w0Rd}
command: start --optimized
ports:
- '127.0.0.1:8080:8080'
depends_on:
- postgres_db
Restart the docker stack afterwards with
docker compose down
# optional explicit build step
docker compose build
docker compose up -d && docker compose logs --follow
This will build the image and start the service.
Production#
For production, some additional things may need to be changed.
Buffer size for MulticastSocket
I noticed the following warning regarding buffer sizes
:
JGRP000015: the receive buffer of socket MulticastSocket was set to 25MB, but the OS only allocated 212.99KB
I resolved this by editing /etc/sysctl.conf
on the host.10
# Allow a 25MB UDP receive buffer for JGroups
net.core.rmem_max = 26214400
# Allow a 1MB UDP send buffer for JGroups
net.core.wmem_max = 1048576
and then running sysctl -p
so that changes take effect.
Enable Quarkus recovery
Another warning I received 11:
WARN [io.quarkus.agroal.runtime.DataSources] (main) Datasource
enables XA but transaction recovery is not enabled. Please enable transaction recovery by setting quarkus.transaction-manager.enable-recovery=true, otherwise data may be lost if the application is terminated abruptly
This can be fixed by adding a volume mount to the keycloak service and setting QUARKUS_TRANSACTION_MANAGER_ENABLE_RECOVERY
:
services:
keycloak:
build: .
volumes:
- /srv/keycloak/data/keycloak/ObjectStore/:/ObjectStore/
environment:
QUARKUS_TRANSACTION_MANAGER_ENABLE_RECOVERY: true
Before starting, create the directory on the host with:
machinectl shell keycloak@
mkdir -p /srv/keycloak/data/keycloak/ObjectStore
Then bash as user root
into the keycloak container and chown
the newly created folder to the user with id 1000
:
docker compose exec -u root keycloak /bin/bash
chown 1000:1000 /ObjectStore
Conclusions#
We are now running a keycloak service in rootless docker behind our system nginx reverse proxy, which does the SSL termination for us.
For automatic updates of the docker container, see the Mastodon post.
The next step would be logging in to the keycloak services and adding
an email under https://your.tld.com/admin/master/console/#/master/realm-settings/email
.
Next comes (e.g.) adding a realm. Then theming, which can be done with keycloakify 12.
You can see that I already added the necessary lines to the Dockerfile:
# COPY --from=keycloakify_jar_builder /opt/app/build_keycloak/target/keycloakify-starter-keycloak-theme-5.1.3.jar /opt/keycloak/providers/
If you find improvements to the instructions above, please add in the comments section!
-
keycloak.org, Running Keycloak in a container ↩
-
Otmane Fettal on medium.com, Use Keycloak 18 with Docker and Nginx ↩
-
docker-compose.yml
from keycloak-config-cli ↩ -
Mark Wolfe's
docker-compose.yml
↩ -
keycloak docs, All configuration ↩
-
keycloak docs, Using a reverse proxy ↩
-
Mozilla SSL configurator for nginx, version=1.17.7 ↩
-
keycloak docs, Postgres & the --optimized flag ↩
-
docs.docker.com, Multi-stage builds ↩
-
Buffer sizes warning, https://developer.jboss.org/thread/272212 ↩
-
keycloak theming with keycloakify and the keycloakify-starter theme ↩