https://du.nkel.dev/blog/2024-02-10_keycloak-docker-compose-nginx/ du.nkel.dev * Concept * Preparations * Keycloak Setup * docker-compose.yml * Test locally * nginx * Debug * Custom build with Dockerfile * Conclusions 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: * SSH * A VM with Linux (Ubuntu; Debian etc.) * A domain or subdomain where you can add an A (and optionally AAAA) 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 user keycloak (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 the keycloak 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.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: * remove or comment out the image: line * uncomment build: . * add command: start --optimized 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 - --------------------------------------------------------------------- --------------------------------------------------------------------- This is a personal code notes blog. CC BY-SA 4.0 Alexander Dunkel, Mail: du@nkel.dev Built with MkDocs. Keyboard Shortcuts xClose Keys Action ? Open this help n Next page p Previous page s Search