Keycloak
- A realm is a security boundary, not an app folder. Most internal estates want a small number of realms, not one per service.
- Clients represent applications, groups represent humans, and roles represent permissions. Mixing those concepts early makes later automation miserable.
- User federation keeps LDAP as the source of truth; identity brokering delegates login to another IdP. They solve different problems.
- Most "OIDC is broken" incidents are actually
hostname, reverse-proxy, or cookie issues. - A database backup is your real backup. Realm export is useful for migration, review, and smoke tests, but it is not disaster recovery by itself.
- Clock skew between browser, reverse proxy, Keycloak, and upstream IdP produces weird, intermittent failures. Keep NTP boring.
Mental model
Keycloak is the control plane for authentication, tokens, and authorization metadata. It is not your user database unless you explicitly decide it should be. In most enterprise installs it fronts a directory like FreeIPA or another LDAP source, and it publishes OIDC or SAML to applications.
| Object | What it is | Common mistake |
|---|---|---|
| Realm | Isolation boundary for users, sessions, keys, flows, and clients | Creating one realm per app when one shared workforce realm would do |
| Client | An application or service that trusts Keycloak | Treating a client like a user group |
| Client scope | A reusable bundle of protocol mappers and token settings | Copy-pasting mappers into every client instead of using scopes |
| Authentication flow | The step-by-step login pipeline | Editing the built-in browser flow directly instead of copying it first |
| Realm role | Permission that makes sense across many apps | Using realm roles for app-specific privileges |
| Client role | Permission meaningful only to one client | Reusing the same app-specific role names across unrelated clients |
Production topology
The default production shape is simple: TLS terminates at a reverse proxy or load balancer, one or more Keycloak nodes serve HTTP on the inside, and a shared Postgres database stores persistent state. Put all nodes behind the same external hostname and keep the reverse proxy headers correct.
# /etc/keycloak/conf/keycloak.conf
db=postgres
db-url=jdbc:postgresql://pgbouncer.internal:6432/keycloak
db-username=keycloak
db-password=${KEYCLOAK_DB_PASSWORD}
hostname=https://sso.example.internal
hostname-strict=true
proxy-headers=xforwarded
http-enabled=true
http-port=8080
health-enabled=true
metrics-enabled=true
log-level=info,org.keycloak.events:debug
This configuration assumes TLS offload at the proxy. Keycloak still serves HTTP internally on :8080, but it generates links and cookies for the external HTTPS hostname. If that external URL does not exactly match what users hit in the browser, redirect loops start.
upstream keycloak_pool {
server kc01.internal:8080;
server kc02.internal:8080;
}
server {
listen 443 ssl http2;
server_name sso.example.internal;
ssl_certificate /etc/pki/tls/certs/sso.example.internal.crt;
ssl_certificate_key /etc/pki/tls/private/sso.example.internal.key;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_http_version 1.1;
proxy_pass http://keycloak_pool;
}
}
- Database: use Postgres, not the dev-file DB. Back it up like any other production service.
- Proxy: terminate TLS once, then pass trusted
X-Forwarded-*headers. - Sessions: clusters work best when all nodes run the same version and the load balancer is not rewriting paths or hosts.
- Monitoring: scrape health and metrics endpoints; correlate login spikes with dashboards in Grafana Basics.
Realms, clients, and flows
Most day-to-day Keycloak work is one of three things: create a realm, onboard a client, or customize a login flow. Keep them separate in your head and in automation.
Realm layout
A common pattern is one workforce realm for human users and one machine realm only if you truly need separation of keys, tokens, and policies. Inside the realm, standardize client scopes for profile, email, groups, and team metadata so every app does not reinvent token mapping.
Client example
{
"clientId": "grafana",
"name": "Grafana",
"protocol": "openid-connect",
"publicClient": false,
"secret": "replace-me",
"redirectUris": [
"https://grafana.example.internal/login/generic_oauth"
],
"webOrigins": [
"https://grafana.example.internal"
],
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"attributes": {
"post.logout.redirect.uris": "https://grafana.example.internal/*"
}
}
For browser apps, leave standardFlowEnabled=true and lock down redirect URIs. Avoid wildcards broader than the exact host and path prefixes you really need.
Flows
- Browser flow: interactive login with cookies, MFA, and redirects.
- Direct grant: password grant style flows for legacy tools; disable unless a real client still needs it.
- Client credentials: service-to-service auth via service accounts, not a human username.
- Reset, registration, first broker login: copy built-ins before editing so upgrades remain sane.
Roles, groups, and service accounts
Authorization usually gets messy faster than authentication. The clean model is:
- Use groups for human organization and broad assignment.
- Use realm roles for privileges shared across many apps, like
employeeorplatform-admin. - Use client roles for app-local permissions like
grafana:editororargocd:sync. - Use service accounts for automation. Machines should not log in with a shared human account.
export KC_URL="https://sso.example.internal"
# Log in once as an admin
kcadm.sh config credentials --server "$KC_URL" --realm master --user admin
# Create a realm role
kcadm.sh create roles -r platform -s name=platform-admin
# Create a client role
CLIENT_ID=$(kcadm.sh get clients -r platform -q clientId=grafana --fields id --format csv --noquotes)
kcadm.sh create clients/$CLIENT_ID/roles -r platform -s name=editor
# Grant a realm role to a user
kcadm.sh add-roles -r platform --uusername alice --rolename platform-admin
When multiple apps need the same entitlement, do not stamp the same client role into each client. Create one realm role, then map it into client roles or token claims as needed.
Federation and identity brokering
User federation means Keycloak reads users from LDAP or another external store. Identity brokering means Keycloak delegates login to some other IdP over OIDC or SAML. In practice you often use one or the other, not both, for the same workforce.
| If the source of truth is... | Use... | Typical example |
|---|---|---|
| An LDAP directory you operate | User federation | FreeIPA, OpenLDAP & 389DS, Active Directory |
| An external OIDC/SAML provider | Identity brokering | Azure AD, Entra ID, Okta, another Keycloak |
Example LDAP federation component for a FreeIPA-style directory:
{
"name": "freeipa-ldap",
"providerId": "ldap",
"providerType": "org.keycloak.storage.UserStorageProvider",
"parentId": "platform",
"config": {
"enabled": ["true"],
"priority": ["0"],
"editMode": ["READ_ONLY"],
"cachePolicy": ["DEFAULT"],
"importEnabled": ["true"],
"connectionUrl": ["ldaps://ipa.example.internal:636"],
"usersDn": ["cn=users,cn=accounts,dc=example,dc=internal"],
"bindDn": ["uid=keycloak-bind,cn=users,cn=accounts,dc=example,dc=internal"],
"bindCredential": ["replace-me"],
"usernameLDAPAttribute": ["uid"],
"rdnLDAPAttribute": ["uid"],
"uuidLDAPAttribute": ["ipaUniqueID"],
"userObjectClasses": ["inetOrgPerson, organizationalPerson, person, ipaObject"]
}
}
kcadm.sh create components -r platform -f ldap-freeipa.json
For a Windows-hosted install and LDAPS truststore setup, see Keycloak on Windows + LDAPS. For directory layout and replication trade-offs, see OpenLDAP & 389DS.
Admin CLI examples
kcadm.sh is the repeatable way to manage Keycloak. Use the UI for discovery, then convert anything worth keeping into CLI or JSON so it can be recreated. On Windows the equivalent binary is kcadm.bat.
export KC_URL="https://sso.example.internal"
export KC_REALM="platform"
# Authenticate
kcadm.sh config credentials --server "$KC_URL" --realm master --user admin
# Create the realm if it does not already exist
kcadm.sh create realms -s realm="$KC_REALM" -s enabled=true
# Create a client from JSON
kcadm.sh create clients -r "$KC_REALM" -f grafana-client.json
# Look up the UUID and fetch the client secret
CLIENT_ID=$(kcadm.sh get clients -r "$KC_REALM" -q clientId=grafana --fields id --format csv --noquotes)
kcadm.sh get clients/$CLIENT_ID/client-secret -r "$KC_REALM"
# Create a user and set an initial password
kcadm.sh create users -r "$KC_REALM" -s username=bob -s enabled=true -s email=bob@example.internal
USER_ID=$(kcadm.sh get users -r "$KC_REALM" -q username=bob --fields id --format csv --noquotes)
kcadm.sh set-password -r "$KC_REALM" --userid "$USER_ID" --new-password 'ChangeMeNow!'
Backups and upgrades
Treat Keycloak as three things you must preserve together: the database, local customizations, and the exact release/version you are running.
# 1. Database backup (this is the real backup)
pg_dump -Fc -h pgbouncer.internal -U keycloak keycloak \
-f /srv/backups/keycloak-$(date +%F).dump
# 2. Realm export for review or migration tests
/opt/keycloak/bin/kc.sh export --dir /srv/exports/keycloak-$(date +%F) \
--realm platform --users same_file
# 3. Preserve local additions
tar czf /srv/backups/keycloak-customizations-$(date +%F).tgz \
/opt/keycloak/conf /opt/keycloak/themes /opt/keycloak/providers
Upgrade sequence that fails the least:
- Read release notes for the target version and confirm supported DB and Java versions.
- Back up the DB and customizations.
- Stand up the new version alongside the old one if possible.
- Run
kc.sh buildwith the new config and providers. - Smoke-test one browser login, one client-credentials flow, one admin CLI operation, and one LDAP-federated login before calling it done.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser bounces between the app and Keycloak, never logs in | hostname, reverse proxy, or redirect URI mismatch | Set Keycloak's external hostname exactly, pass X-Forwarded-Proto=https, and make the client redirect URI match what the browser actually uses. |
Invalid parameter: redirect_uri | Client redirect URIs are too strict or simply wrong | Inspect the exact URL sent by the app and register only that hostname/path prefix. |
| Login page loads but session cookies never stick | Mixed HTTP/HTTPS, wrong host, or proxy headers missing so cookies are marked for the wrong origin | Use one canonical HTTPS URL, confirm the browser sees Secure cookies for the expected domain, and verify proxy headers. |
invalid_code, intermittent login failures, or random token exchange errors | Clock skew between Keycloak, proxy, app, or upstream IdP | Fix NTP everywhere. Start with Chrony and confirm time on all nodes before changing flows. |
Admin CLI gets 401 Unauthorized | Wrong admin realm, expired token, or bad server URL | Re-run kcadm.sh config credentials against the right server and realm, then retry. |
| LDAP users can search but cannot log in | Bind account, user filter, password policy, or mapper issue in federation | Check the provider tests, then use Keycloak on Windows + LDAPS or OpenLDAP & 389DS to debug the directory side. |
| Post-upgrade themes or providers disappear | Custom files were not copied into the new install or the build step was skipped | Restore themes/ and providers/, then rerun kc.sh build. |