Keycloak SSO with docker compose and nginx

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


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.

version: '3'

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 $proxy_add_x_forwarded_for;
        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
version: '3'

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.

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 10.

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. keycloak theming with keycloakify and the keycloakify-starter theme