GuidesBusiness and monitoringSingle sign-on with Keycloak

Run Keycloak single sign-on with rootless Podman

Keycloak on Debian 13, one login for all your company's apps over OpenID Connect or SAML, in rootless Podman with PostgreSQL under a user of its own behind Caddy, with two-factor authentication required for everyone and the admin console kept off the internet.

Tested on Keycloak 26.8.1 on Debian 13 (trixie) on a Melonslab server Updated October 14, 2026

Recommended server for this guide

SSD RAID 1 · ECC DDR4 · 10 Gbit

VC-P Alloy · 2 vCPU · 8 GB Memory · 60 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€8.99/mo

Deploy now
On this page

What you will set up

Keycloak is an identity provider: it holds your company's user accounts, their passwords and their second factors, and your apps let Keycloak check who is logging in. Each person has one account for everything, logs in once a day, and when someone leaves you turn off one account instead of one in every app. Apps connect to it with OpenID Connect or SAML 2.0, which most business apps support.

Keycloak was started at Red Hat, which still sells a supported version of it, and is now a project of the Cloud Native Computing Foundation, part of the Linux Foundation in the United States. It is developed in the open on GitHub and is open source under the Apache 2.0 licence. In our tests it made no outside connections: a capture of the server's traffic while Keycloak started and a user logged in showed none.

Here Keycloak runs as a pod under a user of its own called keycloak, with PostgreSQL for its data, behind Caddy from the Podman guide. Only the login pages are on the internet. The admin console is reached through an SSH tunnel. As an example, Keycloak becomes the login for the wiki from the Outline guide.

Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:

  • Keycloak 26.7.5 started in production mode with PostgreSQL 18, and its temporary admin was replaced by a permanent one before Caddy was set up. From the internet, the admin console and the admin realm answered 404.
  • The admin console worked through an SSH tunnel. A company realm, a group and users were created there.
  • A new user had to change their temporary password and set up an authenticator app at their first login. After that, every login asked for a one-time code, and a wrong code was refused. A password that was too short was refused.
  • After five failed logins, the account was locked, and even the right password was refused until the lock ran out.
  • A password reset arrived by email, asked for the one-time code before the new password, and kept the authenticator app.
  • Users logged in to Outline 1.11.0 through Keycloak, with their code. An existing Outline admin was linked to their Keycloak account by email address. Logging out of Outline also ended the Keycloak session, and a disabled user could not log in.
  • The login pages and emails were in Swedish for a browser set to Swedish.
  • An update from Keycloak 26.7.5 to 26.8.1 kept all data. Keycloak was restored from a database backup into an empty volume, the realm was deleted and imported again from an export, and everything came back by itself after a reboot.

Keycloak used about 470 MB of memory when idle and up to 700 MB in use, and PostgreSQL about 15 MB. Keycloak is a Java application, and it needs most of that from the start, even for a handful of users. During a realm export, which starts a second copy of Keycloak for a moment, it used about 1 GB.

Before you start

You need:

  • a server set up as in the Podman guide, with Caddy running, and at least 2 GB of memory;
  • an A record and an AAAA record for sso.example.com pointing at your server;
  • an email account that can send through your email provider's SMTP server, such as sso@example.com, for password resets;
  • to connect Outline, a wiki set up as in the Outline guide.

The examples use sso.example.com for Keycloak, wiki.example.com for Outline, 203.0.113.10 for your server's IPv4 address, which ip -brief address show eth0 shows, and example as the name of your company's realm. Replace them throughout.

1. Create the user

As root:

useradd -m -s /bin/bash keycloak
loginctl enable-linger keycloak
machinectl shell keycloak@

Everything up to step 5 runs as keycloak.

2. Create the secrets

openssl rand -hex 24 | tr -d '\n' | podman secret create keycloak-db-password -
openssl rand -base64 18 | tr -d '\n' | podman secret create keycloak-temp-admin-password -

The first is the database password. The second is the password of a temporary admin account that Keycloak creates when it first starts, which step 4 replaces. Both are stored by Podman, not in the files below.

3. Define the pod

mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd

Create keycloak.pod:

[Pod]
PodName=keycloak
# Only Caddy, on this server, can reach Keycloak: the port is not open to the internet.
PublishPort=127.0.0.1:8166:8080

[Install]
WantedBy=default.target

Create keycloak-db.container:

[Container]
ContainerName=keycloak-db
Image=docker.io/library/postgres:18-alpine
Pod=keycloak.pod
Volume=keycloak-db:/var/lib/postgresql
Environment=POSTGRES_USER=keycloak POSTGRES_DB=keycloak
Secret=keycloak-db-password,type=env,target=POSTGRES_PASSWORD
HealthCmd=pg_isready -U keycloak
HealthInterval=10s
Notify=healthy
AutoUpdate=registry

[Service]
Restart=always

And keycloak-app.container:

[Unit]
Requires=keycloak-db.service
After=keycloak-db.service

[Container]
ContainerName=keycloak-app
Image=quay.io/keycloak/keycloak:26.8
Pod=keycloak.pod
Exec=start
Environment=KC_DB=postgres KC_DB_URL=jdbc:postgresql://127.0.0.1:5432/keycloak KC_DB_USERNAME=keycloak
Environment=KC_HOSTNAME=https://sso.example.com KC_HOSTNAME_ADMIN=http://localhost:8166
Environment=KC_HTTP_ENABLED=true KC_PROXY_HEADERS=xforwarded KC_PROXY_TRUSTED_ADDRESSES=203.0.113.10
Environment=KC_CACHE=local
Environment=KC_BOOTSTRAP_ADMIN_USERNAME=temp-admin
Secret=keycloak-db-password,type=env,target=KC_DB_PASSWORD
Secret=keycloak-temp-admin-password,type=env,target=KC_BOOTSTRAP_ADMIN_PASSWORD
PodmanArgs=--memory=1536m
AutoUpdate=registry

[Service]
Restart=always

What the settings do:

  • Exec=start runs Keycloak in production mode. Keycloak's start-dev mode, which many examples use, keeps its data in the container and relaxes its security checks; it is for trying Keycloak out, not for real users.
  • KC_HOSTNAME is the public address Keycloak puts in every link, token and email. KC_HOSTNAME_ADMIN moves the admin console to http://localhost:8166, which you reach through an SSH tunnel in step 6.
  • KC_HTTP_ENABLED lets Keycloak speak plain HTTP to Caddy, which handles HTTPS. KC_PROXY_HEADERS=xforwarded makes it read the visitor's address and protocol from Caddy's headers, and KC_PROXY_TRUSTED_ADDRESSES accepts those headers only from Caddy. Caddy's connections arrive in the pod from the server's own IPv4 address, as the Podman guide explains, so that is the one to name. Without it, Keycloak's logs show 203.0.113.10 for everyone.
  • KC_CACHE=local runs Keycloak as a single server, instead of looking for others to form a cluster with.
  • PodmanArgs=--memory=1536m caps Keycloak's memory. Java sizes itself after the memory it is given, and without a cap it would plan for half of the server's memory as a start.
  • Keycloak's tag 26.8 follows the patch releases of Keycloak 26.8, which AutoUpdate=registry installs by itself. Step 14 covers moving to the next version.

Start it:

systemctl --user daemon-reload
systemctl --user start keycloak-pod
systemctl --user enable --now podman-auto-update.timer

The first start downloads about 800 MB of images, and Keycloak takes a minute or two to set up its database. It is ready when this prints 200:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8166/realms/master

4. Replace the temporary admin

Keycloak keeps its own admins in a realm called master, and the account it created at the start is meant to be replaced. Keycloak's command-line admin tool, kcadm, runs inside the container. Make a short name for it:

alias kcadm='podman exec -it keycloak-app /opt/keycloak/bin/kcadm.sh'

Show the temporary password, and log in with it when kcadm asks:

podman secret inspect --showsecret --format '{{.SecretData}}' keycloak-temp-admin-password
kcadm config credentials --server http://localhost:8080 --realm master --user temp-admin

Create your own admin account, here called admin, with a password you keep in your password manager:

kcadm create users -r master -s username=admin -s enabled=true
kcadm set-password -r master --username admin
kcadm add-roles -r master --uusername admin --rolename admin

Then set up the master realm for the SSH tunnel, and turn on its protection against password guessing:

kcadm update realms/master -s attributes.frontendUrl=http://localhost:8166 -s sslRequired=none -s bruteForceProtected=true

frontendUrl makes the admin login use the tunnel's address as well. sslRequired=none allows it without HTTPS: the tunnel is encrypted by SSH, and step 5 keeps the master realm off the internet. Log in again as your new admin, find the temporary account's ID, and delete it:

kcadm config credentials --server http://localhost:8080 --realm master --user admin
kcadm get users -r master -q username=temp-admin --fields id
kcadm delete users/ID -r master

Use the id the second command prints in place of ID. Finally, remove the temporary password from the container and from Podman:

sed -i '/BOOTSTRAP_ADMIN\|temp-admin-password/d' ~/.config/containers/systemd/keycloak-app.container
podman secret rm keycloak-temp-admin-password
systemctl --user daemon-reload
systemctl --user restart keycloak-app

Keycloak creates the temporary account only on the very first start, so it does not come back.

5. Put Caddy in front

Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:

sso.example.com {
    # Only the login pages are public. The admin console and the master realm stay behind SSH.
    @public {
        path /realms/* /resources/*
        not path /realms/master/*
    }
    handle @public {
        reverse_proxy 127.0.0.1:8166
    }
    handle {
        respond 404
    }
}

Restart Caddy with systemctl --user restart caddy. This is the setup Keycloak's documentation recommends behind a proxy: only /realms/ and /resources/ are passed on, so the admin console at /admin/ is not on the internet, and /realms/master/ is not either, so nobody can try passwords against your admin account. Check from any computer:

curl -s -o /dev/null -w '%{http_code}\n' https://sso.example.com/admin/
curl -s -o /dev/null -w '%{http_code}\n' https://sso.example.com/realms/master/

Both print 404.

6. Open the admin console

On your own computer, open a tunnel to the server and leave it open while you work:

ssh -L 8166:127.0.0.1:8166 root@203.0.113.10

Open http://localhost:8166 in your browser and log in as admin. The tunnel carries the connection to Keycloak's port on the server, encrypted by SSH. When you close it, the admin console is gone again.

The console opens in the master realm. Manage realms at the top left lists your realms and switches between them.

7. Create the company realm

A realm is a separate set of users, settings and connected apps. Keep your staff out of master, which is only for Keycloak's own admins, and create a realm for the company. On the server, as keycloak, log in to kcadm again, since the restart in step 4 logged it out:

alias kcadm='podman exec -it keycloak-app /opt/keycloak/bin/kcadm.sh'
kcadm config credentials --server http://localhost:8080 --realm master --user admin

Then create the realm with all its settings at once:

kcadm create realms -s realm=example -s 'displayName=Example AB' -s enabled=true \
  -s 'passwordPolicy=length(12) and notUsername and notEmail and passwordHistory(3)' \
  -s bruteForceProtected=true -s failureFactor=5 \
  -s internationalizationEnabled=true -s 'supportedLocales=["sv","en"]' -s defaultLocale=sv \
  -s resetPasswordAllowed=true

example is the realm's name, which appears in every address, such as https://sso.example.com/realms/example/account, and Example AB is the name shown on the login page. The settings, which you find in the admin console after switching to the realm:

  • Password policy, under Authentication > Policies: at least 12 characters, not the username or email address, and not one of the last three passwords. Length protects best; rules about digits and special characters mostly make people write passwords down.
  • Brute force detection, under Realm settings > Security defenses: after 5 failed logins, the account is locked for a minute, and for longer at each new round of failures, up to 15 minutes. Wrong one-time codes count too. The lock is per account, so someone who guesses at an account can also keep its owner locked out for a while.
  • Localization in Realm settings: Swedish and English, with Swedish as the default. Step 12 has the details.
  • Forgot password under Realm settings > Login, which needs email from step 10. User registration is off by default, so nobody can create an account themselves.

8. Require two-factor authentication

Out of the box, Keycloak asks for a one-time code only from users who have chosen to set up an authenticator app. To require it from everyone, change a copy of the login flow in the admin console. Switch to the example realm under Manage realms, and open Authentication:

  1. Open the browser flow, choose Action > Duplicate, name the copy browser-2fa, and choose Duplicate.
  2. Go back to Authentication and open browser-2fa. The console sometimes stays on the original flow after duplicating it, and changes there would go to the original.
  3. In the Requirement column, set browser-2fa Browser - Conditional 2FA to Required, the two conditions below it, Condition - user configured and Condition - credential, to Disabled, and OTP Form to Required.
  4. Choose Action > Bind flow, keep Browser flow, and Save.

Now everyone has to give a one-time code after their password. A user who has no authenticator app yet is asked to set one up first, with a QR code to scan and a list of apps that work, such as FreeOTP, Google Authenticator and Microsoft Authenticator.

The password reset flow needs the same change. By default, a reset link by email also removes the user's authenticator app, so whoever can read the mailbox gets in without the second factor:

  1. Open the reset credentials flow, choose Action > Duplicate, and name the copy reset-2fa.
  2. Go back to Authentication and open reset-2fa.
  3. Set reset-2fa Reset - Conditional OTP to Disabled.
  4. Choose Add execution, search for OTP Form, select OTP Form and choose Add. Set its Requirement to Required.
  5. Choose Action > Bind flow, pick Reset credentials flow, and Save.

A reset now asks for the one-time code before the new password, and keeps the authenticator app. When someone loses their phone, delete the Otp row under Users > the user > Credentials, and they set up a new app at their next login.

Your own admin account in master keeps the default: it asks for a code once you set up an app at http://localhost:8166/realms/master/account, through the tunnel.

9. Add users and groups

Groups sort your people, such as by department, and let you give many users the same roles at once. To create one, open Groups, choose Create group, give it a Name, such as Staff, and Create.

To add a person, open Users and choose Add user:

  • Fill in Username, Email, First name and Last name, and turn on Email verified, since you know the address is right.
  • Choose Join Groups, tick the group, and Join. Then choose Create.
  • On the user's Credentials tab, choose Set password, enter a starting password, keep Temporary on, and Save. Give them the password in person or by phone.

At their first login, the user sets up an authenticator app and picks a new password of their own. Once email works (step 10), Credential Reset on the same tab, with the action Update Password, emails them a link to set the password themselves instead. We tested that email with Keycloak's command-line tool, not from the console.

When someone leaves, switch off Enabled at the top of their user page. They can no longer log in anywhere. The Sessions tab on the same page shows where they are logged in, from which address, and Logout all sessions ends those sessions in Keycloak. Apps keep their own sessions, so a person already logged in to an app can keep working there until that session ends.

Every user can see and change their own details and authenticator apps at https://sso.example.com/realms/example/account.

Failed logins appear in Keycloak's log with the visitor's real address. As keycloak:

journalctl --user -u keycloak-app | grep LOGIN_ERROR

A locked user gets the same "Invalid username or password" as for a wrong password, so the message does not tell an attacker the account exists. To unlock someone at once, switch off Temporarily locked on their user page.

10. Email

Keycloak sends email for forgotten passwords, links to set a password, and email address checks. Without email, Forgot Password? on the login page still says "You should receive an email shortly with further instructions", but nothing is sent: Keycloak's log has Failed to send password reset email. You then set every password yourself as in step 9.

Keycloak can read the SMTP password from a file, which it calls a vault, instead of keeping it in its database. As keycloak, store the password of your email account as a secret, in place of the-smtp-password:

printf %s 'the-smtp-password' | podman secret create keycloak-smtp-password -

Add these lines to keycloak-app.container, after the Secret=keycloak-db-password line:

Environment=KC_VAULT=file KC_VAULT_DIR=/run/secrets
Secret=keycloak-smtp-password,type=mount,target=example_smtp-password

The file name is the realm's name, an underscore and the key, which is how Keycloak finds it. Restart:

systemctl --user daemon-reload
systemctl --user restart keycloak-app

Then, in the admin console, in the example realm, open Realm settings > Email:

  • From: sso@example.com, and From display name: Example AB.
  • Host: your provider's SMTP server, such as smtp.example.com, and Port: 587. Tick Enable StartTLS. For port 465, which uses TLS from the start, tick Enable SSL instead.
  • Turn on Authentication, with your email account as Username, and ${vault.smtp-password} as Password, exactly so. Then Save.

To test it, choose Forgot Password? on the login page and enter a user's address. An email with a link arrives, with the subject "Återställ lösenord" for a user who sees Keycloak in Swedish. Test connection on the same page needs an email address on your own admin account; we did not use it. We tested the emails with a test mail server on the same machine, not through a real provider.

11. Connect Outline

Keycloak needs to know each app that uses it. In the admin console, in the example realm, open Clients and choose Create client:

  1. Keep Client type as OpenID Connect, enter outline as Client ID and Outline as Name, and choose Next.
  2. Turn on Client authentication, keep Standard flow ticked, and choose Next.
  3. Enter https://wiki.example.com as Root URL, https://wiki.example.com/auth/oidc.callback under Valid redirect URIs, and https://wiki.example.com under Valid post logout redirect URIs, without a slash at the end. Choose Save.
  4. Open the Credentials tab and copy the Client Secret.

On the server, as root, switch to machinectl shell outline@, and store the secret, in place of the-client-secret:

printf %s 'the-client-secret' | podman secret create outline-oidc-secret -

Outline fetches Keycloak's settings from its public address, which inside Outline's pod belongs to the pod itself. Add this line to outline.pod, under [Pod], so the address goes to the server instead, as the Podman guide explains:

AddHost=sso.example.com:host-gateway

And these lines to outline-app.container, after the other Secret= lines:

Environment=OIDC_ISSUER_URL=https://sso.example.com/realms/example OIDC_CLIENT_ID=outline OIDC_DISPLAY_NAME=Keycloak
Environment=OIDC_LOGOUT_URI=https://sso.example.com/realms/example/protocol/openid-connect/logout
Secret=outline-oidc-secret,type=env,target=OIDC_CLIENT_SECRET

Restart the whole pod:

systemctl --user daemon-reload
systemctl --user restart outline-pod

Outline's login page now has Continue with Keycloak, which takes people to Keycloak's login and back, logged in. A person who logs in for the first time joins the Outline workspace as an editor. If Outline already has an account with the same email address, such as the admin from the Outline guide, they get that account. Log out in Outline also ends the Keycloak session, thanks to OIDC_LOGOUT_URI.

Last, make Keycloak the only way in. Log in to Outline as an admin, open Preferences > Authentication, and switch off Email and Passkeys. Otherwise a person you have disabled in Keycloak could still log in to Outline with an email link or a passkey, without your second factor. With Keycloak as the only choice, Outline sends visitors straight to Keycloak's login page.

Other apps connect the same way: a client in Keycloak with the app's redirect address, and the realm's address, https://sso.example.com/realms/example, with the client ID and secret in the app. We tested only Outline. Mattermost is an exception: its documentation lists OpenID Connect and GitLab login only for its paid plans, so Team Edition, the free one in our guide, cannot use Keycloak.

12. Use Keycloak in Swedish

Keycloak comes translated into Swedish: the login pages, the account page and the emails. With the realm settings from step 7, people whose browser is set to Swedish get Swedish, and everyone else gets English, as English is also on the list. A menu on the login page switches between the two. To show Swedish to everyone, remove English under Realm settings > Localization. We used the admin console in English.

13. Back up

As keycloak:

umask 077
mkdir -p ~/backup
podman exec keycloak-db pg_dump -U keycloak -d keycloak > ~/backup/keycloak-db.sql
podman exec keycloak-app /opt/keycloak/bin/kc.sh export --realm example --file /tmp/example-realm.json
podman exec keycloak-app cat /tmp/example-realm.json > ~/backup/example-realm.json
podman exec keycloak-app rm /tmp/example-realm.json

The database dump is the full backup: both realms, users, passwords, authenticator apps, clients and the keys Keycloak signs its tokens with. The export is the example realm alone, as a JSON file, with its users and their credentials, which you can import into another Keycloak server. Both run while Keycloak keeps working. The export is read out with cat rather than podman cp, so that umask 077 applies to it: podman cp would leave it readable by every user on the server. Copy ~/backup to another machine, for example with restic, and store it safely: anyone with it can make tokens your apps trust.

To restore the database into an empty pod, as keycloak, with the backup in ~/backup:

systemctl --user stop keycloak-pod
podman volume rm keycloak-db
systemctl --user start keycloak-db
systemctl --user stop keycloak-app
podman exec -i keycloak-db psql -q -U keycloak -d keycloak < ~/backup/keycloak-db.sql
systemctl --user start keycloak-app

Starting the database starts the whole pod, Keycloak too, so stop Keycloak before loading the backup. Otherwise Keycloak sets up an empty database first, and the backup no longer fits into it. On a new server, create the user, the database secret and the files from steps 1 to 3, without the two temporary admin lines and without starting the pod, and then restore: your admin account comes back with the database. We restored on the same server.

To import the realm export instead, into a Keycloak that has no example realm, log in to kcadm as in step 7, and run:

podman exec -i keycloak-app /opt/keycloak/bin/kcadm.sh create realms -f - < ~/backup/example-realm.json

The SMTP password is not in either backup, only the reference to the vault. Create keycloak-smtp-password again as in step 10.

14. Update

podman auto-update checks once a day for new images of Keycloak and PostgreSQL, and restarts the pod when one has changed. To update now:

podman auto-update

Keycloak publishes a new version, such as 26.9, every few months. Read the release notes and the upgrading guide on keycloak.org, make a backup, then change the tag in keycloak-app.container, such as 26.8 to 26.9, and restart:

systemctl --user daemon-reload
systemctl --user restart keycloak-app

Keycloak updates its database as it starts. Each start takes a minute or so, as Keycloak first prepares itself for the settings it is given, and the login pages are unavailable until it is ready.

Troubleshooting

The admin console shows only a spinning circle. The master realm has no frontendUrl, so the console tries to reach https://sso.example.com/realms/master, which Caddy blocks. Run the kcadm update realms/master command from step 4.

The admin login says "HTTPS required". sslRequired=none is missing on the master realm. Run the same command.

A user is sure the password is right, but gets "Invalid username or password". The account is locked after too many failed attempts. Wait up to 15 minutes, or switch off Temporarily locked on the user's page.

Outline does not start, or has no Continue with Keycloak. As outline, run journalctl --user -u outline-app | grep -i oidc. If fetching the configuration fails, the AddHost line is missing from outline.pod, or the pod was not restarted after it was added.

Keycloak shows "Invalid redirect uri" after logging in to an app. The app's address under Valid redirect URIs in the client does not match exactly. For logging out, check Valid post logout redirect URIs: Outline sends https://wiki.example.com without a slash.

No email arrives. As keycloak, run journalctl --user -u keycloak-app | grep -i mail for the email server's answer. If it is about the login, check the username and the password secret, and that Password says ${vault.smtp-password}.

Run it on your own server

SSD RAID 1 · ECC DDR4 · 10 Gbit

VC-P Alloy

€8.99/mo

vCPU
2
Memory
8 GB
Storage
60 GB
Transfer
10 TB
Performance
SSD · RAID 1
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides