Keycloak SSO with docker compose and nginx

Published: 2024-02-11, Revised: 2024-04-20


nextcloud_bg


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:

Follow the Mastodon post for basic setup of docker rootless to:

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.ymls 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' and KC_PROXY: edge will make keycloak listen on localhost
  • the syntax for env variables (${KC_HOSTNAME:-your.tld.com}) means:
    1. use KC_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: keycloak_local

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:

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!


  1. keycloak.org, Running Keycloak in a container 

  2. Otmane Fettal on medium.com, Use Keycloak 18 with Docker and Nginx 

  3. docker-compose.yml from keycloak-config-cli  

  4. Mark Wolfe's docker-compose.yml 

  5. keycloak docs, All configuration 

  6. keycloak docs, Using a reverse proxy 

  7. Mozilla SSL configurator for nginx, version=1.17.7 

  8. keycloak docs, Postgres & the --optimized flag 

  9. docs.docker.com, Multi-stage builds 

  10. Buffer sizes warning, https://developer.jboss.org/thread/272212 

  11. Enable Quarkus recovery #15255 

  12. keycloak theming with keycloakify and the keycloakify-starter theme