Skip to content

Keycloak single sign-on

Corporate accounts, no passwords in the container. The desktop sits behind an OIDC proxy that authenticates against Keycloak — or any other OIDC provider — and only lets through users holding the right role. Uses QGIS_DESKTOP_AUTH_MODE=oidc.

Why this scenario

basic and greeter mode both keep credentials inside the container: a password file, or Linux accounts materialised at boot. That does not scale past a handful of people, and it means offboarding someone is a container restart rather than a click in your directory.

With oidc the container holds no user credentials at all. Accounts, password policy, MFA and group membership stay in the identity provider. Revoke someone there and their next request to the desktop bounces.

Topology

flowchart LR
    Browser["Web Browser"] -->|"https :8443"| Proxy["oauth2-proxy<br/>only public listener"]
    Proxy <-->|"discovery + code exchange"| KC["Keycloak realm<br/>QGIS_DESKTOP_OIDC_ISSUER_URL"]
    Proxy -->|"127.0.0.1:6901"| Kasm["Xkasmvnc"]
    Kasm -->|":1"| XFCE["XFCE + QGIS"]
    Proxy -.->|"watchdog: proxy dies → container dies"| Init["PID 1"]

Three properties fall out of that shape:

  • KasmVNC is bound to loopback. There is no published port for it, so an unauthenticated request cannot reach the desktop even to be refused by it.
  • The proxy is the auth boundary and it is unprivileged (UID 1000, all capabilities cleared).
  • If the proxy exits, PID 1 is signalled and the container stops — the desktop is never left serving without it.

Requirements this covers

Requirement How
Users authenticate with corporate credentials OIDC authorization-code flow against the realm
No user passwords stored in the image The container only holds a client secret
Only entitled staff may open the desktop QGIS_DESKTOP_OIDC_ALLOWED_ROLES / _GROUPS
The client secret is not visible to docker inspect QGIS_DESKTOP_OIDC_CLIENT_SECRET_FILE + a 0400 mount
The desktop cannot phone home Egress lockdown on, with only the IdP allowlisted (automatically)

Run the demo

A complete, runnable version lives in examples/keycloak-oidc/: a throwaway Keycloak with a pre-imported realm plus the desktop wired to it.

nix run .#build-docker

# The browser and the container must reach the issuer under the same name.
echo '127.0.0.1 keycloak' | sudo tee -a /etc/hosts

nix run .#run-keycloak-demo

Open http://keycloak:8443.

Who Credentials Outcome
alice alice / hunter2 Has the qgis-user role → gets the desktop
mallory mallory / hunter2 Authenticates, has no role → Permission Denied
admin admin / admin Keycloak console on http://keycloak:8080/admin

Mallory is the interesting one: authentication and authorisation are separate decisions, and the proxy enforces the second.

Demo credentials are public

The realm, its client secret and both passwords are committed to this repository. The demo realm is for seeing the flow work, never for protecting anything.

Against your own Keycloak

Create a confidential client in your realm with standard flow enabled and a valid redirect URI of https://<your-host>/oauth2/callback, then:

docker run --rm -p 8443:8443 --cap-add=NET_ADMIN \
  -e QGIS_DESKTOP_AUTH_MODE=oidc \
  -e QGIS_DESKTOP_OIDC_ISSUER_URL=https://sso.example.com/realms/gis \
  -e QGIS_DESKTOP_OIDC_CLIENT_ID=qgis-desktop \
  -e QGIS_DESKTOP_OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc \
  -e QGIS_DESKTOP_OIDC_REDIRECT_URL=https://gis.example.com/oauth2/callback \
  -e QGIS_DESKTOP_OIDC_ALLOWED_ROLES=qgis-user \
  -e QGIS_DESKTOP_OIDC_COOKIE_SECRET_FILE=/run/secrets/cookie \
  -v /etc/qgis-desktop/client-secret:/run/secrets/oidc:ro \
  -v /etc/qgis-desktop/cookie-secret:/run/secrets/cookie:ro \
  ghcr.io/kartoza/qgis-desktop-docker:ltr

Every variable is documented under Authentication → OIDC variables.

Verification checklist

Check Expected
curl -sI http://localhost:8443/ 302 to the identity provider, never the desktop
docker exec <c> ss -ltn (or /proc/net/tcp) 8443 on 0.0.0.0, 6901 on 127.0.0.1 only
docker inspect <c> \| grep -i secret Shows the path, never the value, when using _FILE
docker exec <c> ls -l /run/qgis-desktop/oidc/secrets.cfg -r-------- 1 user user
Sign in as a user without the role oauth2-proxy shows Permission Denied
docker exec <c> curl https://example.com Blocked by the egress filter
docker kill --signal=TERM $(pgrep -f oauth2-proxy) inside the container Container stops within ~5 s

Choosing between the modes

basic greeter oidc
Where accounts live Container Container Identity provider
MFA No No Whatever the IdP enforces
Per-user Linux session No Yes With QGIS_DESKTOP_OIDC_INNER_MODE=greeter
Works offline Yes Yes No — needs the IdP
Clean re-prompt on failure No Yes Yes

oidc and greeter compose: set QGIS_DESKTOP_OIDC_INNER_MODE=greeter to require SSO at the edge and give each person their own home directory inside.