This is an old revision of the document!
Keycloak
This tutorial is for Debian users desiring to spin up an OCI container for Keycloak. In my case, Keycloak was a compatible auth endpoint for Incus' Web GUI. The web GUI for Incus is open by default, which is not wise for a production container/vm server! You either needed to build a custom cert for the reverse proxy and server to everyone and/or lock down with </Location> directives, or you had to tunnel via a proxy and access it locally. Both are cumbersome and/or risky and so I kept everything local and behind the ssh tunnel while I tinkered. To me, it was mandatory that Incus' Web GUI be fully public-facing, secure w/ TLS, and absolutely no janky browser barf -based solutions. For that reason, I first spun up podman, i.e., to spin up a Keycloak instance to attach to Incus' web GUI and liberate it from the tunnel. To begin with, the most minimal setup is the keycloack container and a separate postgres database for it to use:
docker.io/library/postgres:16-alpinequay.io/keycloak/keycloak:26.7Keycloak requires its own network in docker so I created a project directory, the network, and pulled the images.
mkdir -p ~/keycloak/postgres-data podman pull docker.io/library/postgres:16-alpine podman pull quay.io/keycloak/keycloak:latest podman network exists keycloak_default || podman network create keycloak_default
Now that we have the images and project directories, we can create the required secrets.
umask 077 openssl rand -hex 32 > ~/keycloak/db.secret openssl rand -hex 32 > ~/keycloak/bootstrap-admin.secret chmod 600 ~/keycloak/db.secret ~/keycloak/bootstrap-admin.secret
We can now safely create the quadlet systemd service units that run/monitor the keycloak and postgres containers. Tweak these snippets to your needs and or liking:
cat > ~/.config/containers/systemd/container-keycloak-postgres.container << 'EOF' [Container] ContainerName=keycloak-postgres Image=docker.io/library/postgres:16-alpine Network=keycloak_default Volume=/home/worker/keycloak/postgres-data:/var/lib/postgresql/data:Z Environment=POSTGRES_DB=keycloak Environment=POSTGRES_USER=keycloak Environment=POSTGRES_PASSWORD=replace-with-your-db-password PodmanArgs=--cpus=2 --memory=4g [Service] Restart=always TimeoutStopSec=120 [Install] WantedBy=default.target EOF cat > ~/.config/containers/systemd/container-keycloak.container << 'EOF' [Unit] After=container-keycloak-postgres.service Wants=container-keycloak-postgres.service [Container] ContainerName=keycloak Image=quay.io/keycloak/keycloak:26.7 Exec=start Network=keycloak_default PublishPort=127.0.0.1:8080:8080 Environment=KC_DB=postgres Environment=KC_DB_URL=jdbc:postgresql://keycloak-postgres:5432/keycloak Environment=KC_DB_USERNAME=keycloak Environment=KC_DB_PASSWORD=replace-with-your-db-password Environment=KC_HOSTNAME=https://auth.haacksnetworking.org Environment=KC_HEALTH_ENABLED=true Environment=KC_HTTP_ENABLED=true Environment=KC_PROXY_HEADERS=xforwarded PodmanArgs=--cpus=2 --memory=6g [Service] Restart=always TimeoutStopSec=120 [Install] WantedBy=default.target EOF systemctl --user daemon-reload systemctl --user reset-failed container-keycloak-postgres.service container-keycloak.service systemctl --user start container-keycloak-postgres.service systemctl --user start container-keycloak.service
systemctl --user is-active container-keycloak-postgres.service container-keycloak.service
podman inspect keycloak-postgres keycloak --format '{{.Name}} cpus={{.HostConfig.NanoCpus}} memory={{.HostConfig.Memory}}'
podman exec keycloak-postgres pg_isready -U keycloak
curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health/ready || curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
Expect both active. Health may take a minute on first start while Keycloak builds. On 26.x the management health endpoint can be on port 9000 inside the container. If curl http://127.0.0.1:8080/ returns the welcome page, the proxy path is fine.
/usr/local/bin/upgrade-keycloak.sh. A tag change is an edit to Image= in the matching .container file before daemon-reload. The script does not recreate the units.
#!/bin/bash set -euo pipefail podman pull docker.io/library/postgres:16-alpine podman pull quay.io/keycloak/keycloak:26.7 podman network exists keycloak_default || podman network create keycloak_default systemctl --user stop container-keycloak.service systemctl --user stop container-keycloak-postgres.service systemctl --user daemon-reload systemctl --user reset-failed container-keycloak-postgres.service container-keycloak.service systemctl --user start container-keycloak-postgres.service systemctl --user start container-keycloak.service
Run it as worker:
su - worker -c '/bin/bash /usr/local/bin/upgrade-keycloak.sh'
Do not sudo -u worker. That drops the session bus.
a2enmod proxy proxy_http proxy_wstunnel headers rewrite ssl
/etc/apache2/sites-available/auth.haacksnetworking.org.conf:
<VirtualHost *:80>
ServerName auth.haacksnetworking.org
RewriteEngine On
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
<VirtualHost *:443>
ServerName auth.haacksnetworking.org
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/auth.haacksnetworking.org/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/auth.haacksnetworking.org/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Port "443"
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
ProxyPass / http://127.0.0.1:8080/ upgrade=websocket
ProxyPassReverse / http://127.0.0.1:8080/
ProxyTimeout 60
LimitRequestFieldSize 65535
ErrorLog ${APACHE_LOG_DIR}/auth.haacksnetworking.org-error.log
CustomLog ${APACHE_LOG_DIR}/auth.haacksnetworking.org-access.log combined
</VirtualHost>
a2ensite auth.haacksnetworking.org.conf apache2ctl configtest && systemctl reload apache2 curl -sI https://auth.haacksnetworking.org/
KC_PROXY_HEADERS=xforwarded matches the X-Forwarded-* headers. KC_HOSTNAME must be https://auth.haacksnetworking.org.
adminhttps://auth.haacksnetworking.orgDo not keep the bootstrap password.
Stay out of master for apps. Create a realm.
Realm
incusIssuer (must match Incus exactly):
https://auth.haacksnetworking.org/realms/incus
curl -sI https://auth.haacksnetworking.org/realms/incus/.well-known/openid-configuration
Must be 200. The issuer value in that JSON is what Incus uses.
Users (in realm incus)
oidc.claim=email
Create one user per human. Do not log into Incus as Keycloak admin unless that user also exists in the incus realm.
Client
incushttps://<incus-ui-host>/oidc/callback
Example: https://support.haacksnetworking.org:8443/oidc/callback
https://<incus-ui-host>No client secret. Incus does not send one.
Incus
incus config set oidc.issuer=https://auth.haacksnetworking.org/realms/incus/ incus config set oidc.client.id=incus incus config set oidc.scopes=openid,email,profile incus config set oidc.claim=preferred_username
oidc.claim can be email instead.
incus config show | grep oidc
Open the Incus UI → Login with SSO → Keycloak incus realm user.
Then grant that identity. SSO alone is not admin:
incus auth identity list
From the unix socket as root, grant Admin (or project access) to the new oidc/ identity. Until you do, the UI logs in and then shows nothing / 403.
— oemb1905 2026/10/10 04:18