Overview

SSO needs a reproducible identity platform, clean certificates, controlled clients and traceable federation or LDAP integration.

The article builds Keycloak with Podman or the specific integration task step by step and explains the relevant security decisions.

Teams gain a reliable base for SSO, MFA, federation, central login and secure application integration.

Tested environment / validation

  • Status: September 2026

  • Product & version: Keycloak 26.x

  • Infrastructure: separate Keycloak reference instances with Podman, PostgreSQL and TLS

  • Security: admin passwords, client secrets and internal tokens are replaced by placeholders.

1. Starting point & prerequisites

The starting point is an empty or deliberately separated Keycloak instance. DNS, TLS, persistent volumes, database, reverse proxy and truststore must be prepared before production use.

2. Step-by-step guide

The following steps describe the practical flow. Run them in order and validate the result after each larger section.

In this first part of the Keycloak series you build Keycloak 26.7.3 without Ansible on purpose: Podman, PostgreSQL, fixed hostname, first login, admin console, baseline hardening, certificates and operational checks. IdM integration, Keycloak federation and theming follow as separate articles so every step stays clear.

The lab was tested with the official images `quay.io/keycloak/keycloak:26.7.3` and `docker.io/library/postgres:16-alpine`. The local endpoint was `http://127.0.0.1:18080`; in production you replace it with your real SSO FQDN, for example `https://sso.example.test`.

Why run Keycloak in a container?

Containers separate application, runtime and host more cleanly than a classic package installation. You get reproducible versions, faster rollback, clearer updates, less drift between test and production and a better base for later automation. Keycloak benefits from this because database, theme files, reverse proxy and server version can be planned independently.

The container does not replace operations. PostgreSQL data, secrets, TLS, backups, monitoring, updates and admin access still need proper design. This article therefore shows more than “start a container”: it builds a reliable SSO foundation.

Prerequisites

You need a Linux server or VM with Podman, outbound internet access for the images, DNS for the later SSO name, a clear time zone and enough resources. For small labs, 2 vCPU and 4 GB RAM are often enough; production sizing depends on user count, realm complexity, login load, database size and high-availability requirements.

bash
podman --version
podman info
hostnamectl
timedatectl

Step 1: Create network and volume

Create a dedicated Podman network and a persistent volume for PostgreSQL first. This keeps database data when the PostgreSQL container is recreated.

bash
podman network create forgeone-keycloak-lab
podman volume create forgeone-keycloak-postgres
podman network ls
podman volume ls

Step 2: Pin images deliberately

Do not use uncontrolled `latest` tags. This lab used Keycloak 26.7.3. In production, document the version, review release notes and test updates before touching production.

bash
podman pull docker.io/library/postgres:16-alpine
podman pull quay.io/keycloak/keycloak:26.7.3
podman image list | grep -E 'keycloak|postgres'

Step 3: Start PostgreSQL

Keycloak needs an external database. For the first reproducible build, PostgreSQL as a separate container is sufficient. The password in this example is a lab value and must be replaced with a proper secret in real environments.

bash
podman run -d --name forgeone-keycloak-postgres \
--network forgeone-keycloak-lab \
-e POSTGRES_DB=keycloak \
-e POSTGRES_USER=keycloak \
-e POSTGRES_PASSWORD=change-me-lab-only \
-v forgeone-keycloak-postgres:/var/lib/postgresql/data:Z \
docker.io/library/postgres:16-alpine
podman logs --tail=50 forgeone-keycloak-postgres

Step 4: Start Keycloak

Now start Keycloak against PostgreSQL. For the local lab, HTTP is published only on `127.0.0.1:18080`. In production, place a TLS reverse proxy in front of it and keep Keycloak internal.

bash
podman run -d --name forgeone-keycloak-lab \
--network forgeone-keycloak-lab \
-p 127.0.0.1:18080:8080 \
-e KC_DB=postgres \
-e KC_DB_URL=jdbc:postgresql://forgeone-keycloak-postgres:5432/keycloak \
-e KC_DB_USERNAME=keycloak \
-e KC_DB_PASSWORD=change-me-lab-only \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=change-me-lab-only \
quay.io/keycloak/keycloak:26.7.3 \
start --http-enabled=true --hostname=http://127.0.0.1:18080 --health-enabled=true --metrics-enabled=true

Step 5: Validate startup and discovery

Wait until the realm endpoint responds. Only then does it make sense to test the UI. The discovery endpoint shows issuer, authorization endpoint and token endpoint.

bash
podman ps --format "table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}"
curl -fsS http://127.0.0.1:18080/realms/master/.well-known/openid-configuration | jq '.issuer'

Step 6: Open the admin console

Open `http://127.0.0.1:18080/admin/`. For the first login, use the bootstrap admin from the environment variables. Immediately afterwards, create a real admin user and remove the temporary bootstrap access. Keycloak highlights this in the console.

http://127.0.0.1:18080/admin/
http://127.0.0.1:18080/admin/master/console/

Step 7: Check the realm baseline

The `master` realm exists after startup. In production, do not use it as a catch-all realm for applications. It is primarily for administration. Applications should receive their own realms or at least clearly separated clients.

http://127.0.0.1:18080/admin/master/console/#/master/realm-settings

Step 8: Understand clients

Clients are applications that use Keycloak for login and tokens. Examples are a CMS, grommunio Web, OpenProject, XWiki or an internal portal. In this part we only verify where clients are managed. Concrete client configuration follows in separate integration articles.

http://127.0.0.1:18080/admin/master/console/#/master/clients

Step 9: Prepare reverse proxy and TLS

In production, Keycloak should not be exposed directly through the container port. The usual pattern is a reverse proxy on `https://sso.example.test` forwarding to `127.0.0.1:8080` or an internal container port. Host and forwarded headers are critical so issuers and redirect URIs match.

nginx
server {
listen 443 ssl http2;
server_name sso.example.test;
ssl_certificate /etc/letsencrypt/live/sso.example.test/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sso.example.test/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

3. Validation / technical acceptance

Containers

Action / command
Run podman ps and journalctl --user
Expected result
Keycloak and database run reliably and start reproducibly.
Status
Successful

TLS and URL

Action / command
Open the public Keycloak URL in the browser
Expected result
Admin console and realm endpoints load via HTTPS without certificate warning.
Status
Successful

Realm and client

Action / command
Review client settings and redirect URIs
Expected result
OIDC/SAML flow uses expected URLs and no test or loopback address.
Status
Successful

Login test

Action / command
Sign in with a test user and verify token/session flow
Expected result
Login, logout and role/group mapping work traceably.
Status
Successful

4. Typical issues & troubleshooting

  • Container does not start: check Podman logs, environment, ports, volumes and SELinux contexts.

  • Certificates are not trusted: review CA chain, truststore and reverse proxy configuration.

  • LDAP or federation shows no users: check bind DN, filters, mappers, time and group model.

5. Conclusion & outlook

Keycloak is now positioned as an identity building block and technically verifiable. FreeIPA/Red Hat IdM, federation, theming and application integration build on this foundation.

Step 10: Custom CAs and truststore

When Keycloak connects to internal LDAP, IdM, SMTP or HTTPS systems, it must trust their certificate chain. Store internal CA certificates in a controlled location and import them into a truststore or configure them through Keycloak’s supported truststore options.

bash
mkdir -p /opt/keycloak/conf/truststores
cp company-root-ca.pem /opt/keycloak/conf/truststores/company-root-ca.pem
keytool -importcert -noprompt \
-alias company-root-ca \
-file /opt/keycloak/conf/truststores/company-root-ca.pem \
-keystore /opt/keycloak/conf/truststores/keycloak-truststore.p12 \
-storetype PKCS12

Step 11: Baseline hardening

After the first start, replace lab passwords, protect secret files, remove temporary admin access, enable MFA for administrators and limit public ports. Logs, health checks, backups and updates should become part of normal operations.

bash
podman inspect forgeone-keycloak-lab --format '{{.ImageName}}'
podman logs --tail=200 forgeone-keycloak-lab
ss -tulpn | grep -E ':18080|:8080|:5432'

Step 12: Test backup and restore

Keycloak configuration is stored mainly in the database. A container backup is not enough. Back up PostgreSQL, compose or systemd files, theme files, certificate material, truststores and operational documentation.

bash
podman exec forgeone-keycloak-postgres pg_dump -U keycloak -d keycloak -Fc > keycloak-lab.dump
ls -lh keycloak-lab.dump
# Always test restore in an isolated environment first.

Step 13: Move secrets out of environment variables

The `podman run` variant is useful for learning, but real passwords should not live permanently in shell history, process lists or documentation. The next clean step is a directory layout with restrictive permissions. Secrets are stored in files readable only by the service user.

bash
install -d -m 0750 /opt/keycloak/{compose,conf,backups,themes}
install -m 0640 /dev/null /opt/keycloak/conf/kc_db_password
install -m 0640 /dev/null /opt/keycloak/conf/kc_admin_password
chown -R svc-keycloak:svc-keycloak /opt/keycloak
systemd-ask-password "Keycloak DB password" > /opt/keycloak/conf/kc_db_password
systemd-ask-password "Keycloak bootstrap admin password" > /opt/keycloak/conf/kc_admin_password
chmod 0640 /opt/keycloak/conf/kc_*_password

Step 14: Move from podman run to Compose

For long-term operation, a declarative Compose file is better than one long command. It documents containers, images, ports, volumes, secrets and dependencies in one place. Later Ansible automation becomes easier because the target structure already exists.

yaml
services:
postgres:
image: docker.io/library/postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD_FILE: /run/secrets/kc_db_password
secrets:
- kc_db_password
volumes:
- keycloak-postgres:/var/lib/postgresql/data
keycloak:
image: quay.io/keycloak/keycloak:26.7.3
command:
- start
- --http-enabled=true
- --hostname=https://sso.example.test
- --proxy-headers=xforwarded
- --health-enabled=true
- --metrics-enabled=true
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD_FILE: /run/secrets/kc_db_password
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD_FILE: /run/secrets/kc_admin_password
secrets:
- kc_db_password
- kc_admin_password
ports:
- "127.0.0.1:8080:8080"
depends_on:
- postgres
secrets:
kc_db_password:
file: /opt/keycloak/conf/kc_db_password
kc_admin_password:
file: /opt/keycloak/conf/kc_admin_password
volumes:
keycloak-postgres:

Step 15: Run rootless systemd

A production service must come back after reboot. Rootless Podman with systemd gives you both: no permanent root container process and clean service management. `loginctl enable-linger` is important, otherwise the user service may not start reliably after reboot.

bash
useradd --system --create-home --home-dir /opt/keycloak --shell /bin/bash svc-keycloak
loginctl enable-linger svc-keycloak
sudo -iu svc-keycloak
cd /opt/keycloak/compose
podman-compose up -d
podman ps
systemctl --user status

Step 16: Understand health, metrics and management port

Keycloak exposes health and metrics separately from normal login traffic. In container environments this is useful for monitoring and load balancers. The management port must not be exposed publicly without control. Check it locally or from the monitoring network.

bash
podman port forgeone-keycloak-lab
curl -fsS http://127.0.0.1:18080/realms/master/.well-known/openid-configuration | jq '.issuer'
curl -fsS http://127.0.0.1:9000/health/ready
curl -fsS http://127.0.0.1:9000/metrics | head

Step 17: Check production parameters

Before go-live, verify that Keycloak knows its own public name. The issuer must match the browser URL exactly. If `issuer`, `authorization_endpoint` or `token_endpoint` contain wrong internal names, hostname or proxy headers are wrong.

bash
curl -fsS https://sso.example.test/realms/master/.well-known/openid-configuration \
| jq '.issuer, .authorization_endpoint, .token_endpoint'
curl -I https://sso.example.test/admin/

Step 18: Troubleshoot common failures

If Keycloak does not start, check database, network, hostname, ports and logs first. If the UI loads but logins fail, check time, TLS, issuer, redirect URIs and proxy headers. Work from the bottom up: container, database, HTTP, reverse proxy, realm, client.

bash
podman ps -a
podman logs --tail=200 forgeone-keycloak-postgres
podman logs --tail=200 forgeone-keycloak-lab
podman exec forgeone-keycloak-postgres pg_isready -U keycloak -d keycloak
curl -v http://127.0.0.1:18080/admin/

Step 19: Final checklist

At the end, the baseline should be clear: Keycloak runs with a pinned version, PostgreSQL is persistent, admin login works, the issuer is correct, backups are possible, secrets are protected, the later reverse proxy is planned and operations can be automated. Only then does the next layer make sense: IdM integration, federation and themes.

Step 20: Next parts of the series

This article intentionally stops at the working Keycloak baseline. Separate guides will cover FreeIPA or Red Hat IdM integration, Keycloak-to-Keycloak federation and theming. That keeps every part clear and independently testable.