Keycloak in Production: Realm Design, Federation, Kubernetes HA, and the Operational Reality of Running Your Own Identity Provider
Running your own identity provider is one of those decisions that looks straightforward until the first time a misconfigured redirect URI causes an OAuth2 callback loop at 2 AM, or your LDAP sync job silently stops working and nobody notices until payroll can’t log in. Keycloak is remarkably capable — it handles OIDC, OAuth2, SAML 2.0, user federation, social login, MFA, passkeys, and multi-tenancy Organizations all in one open-source package backed by Red Hat. That capability comes with a configuration surface area that can swallow teams whole.
This guide is the field manual you wish you’d had before standing up Keycloak at scale. We’ll cover realm architecture, the client scopes and protocol mapper model, LDAP federation, authentication flows including WebAuthn passkeys, Kubernetes HA deployment with the official Operator, theme customization, production operations, and — most importantly — the pitfalls that aren’t in the official docs.
What Keycloak Is (and Isn’t)
Keycloak is an open-source Identity and Access Management (IAM) server. It speaks OIDC/OAuth2, SAML 2.0, and various legacy protocols. It ships with an admin console, a REST/Admin API, a user account self-service console, and a rich SPI (Service Provider Interface) model that lets you extend almost everything.
Current version: As of May 2026, the active release line is Keycloak 26.6.x, with 26.6.2 released May 19, 2026. The project follows a cadence of four minor releases per year within a major release cycle. Keycloak 26 introduced Organizations (multi-tenancy) as GA, DPoP as fully supported, FAPI 2 Final support, and Zero-Downtime patch releases via the Operator. Keycloak 26.4 promoted passkeys from preview to supported. Keycloak 26.6 added JWT Authorization Grant (RFC 7523), Federated Client Authentication, and Workflows for realm administration.
Quarkus-based since 17.x: The WildFly distribution was retired. The current distribution runs on Quarkus 3.31+ (Keycloak 26.6). This matters because the build model changed: Keycloak now has a build step that compiles configuration into the server binary, and a start step that runs it. The --optimized flag tells start to skip the build check and run from the pre-compiled artifact — critical for container image performance.
Red Hat’s downstream: Red Hat build of Keycloak (RHBK) is the supported enterprise product. It ships with the same major version numbers as community Keycloak but on a defined lifecycle with longer support windows, and appears in the OpenShift OperatorHub as a first-class citizen.
Docker image: quay.io/keycloak/keycloak:<version> (community) or registry.redhat.io/rhbk/keycloak-rhel9:<version> (RHBK).
The Build Step Model
Before diving into deployment, understand the two-phase execution model:
|
|
Build-time options (database vendor, feature flags, metrics/health) get compiled in. Runtime options (hostname, credentials, proxy settings) are passed at startup. Mixing them up causes confusing errors. The start-dev command skips the build step and uses insecure defaults — never use start-dev in production.
Realm Design
The realm is Keycloak’s fundamental isolation boundary. Every user, client, role, and identity provider lives inside a realm. Realms are completely isolated — a user in realm A cannot authenticate to a client in realm B without explicit federation.
The Master Realm: Admin Only
The master realm exists to administer other realms. The admin superuser account lives here. Never create application clients in the master realm. The master realm admin has full access to the entire Keycloak installation; an OIDC client in master inherits that blast radius. All application workloads belong in dedicated realms.
Realm Strategies
Per-environment realms (dev, staging, prod) in the same Keycloak instance give you clean separation of users and configs between environments. This is common for smaller organizations. The tradeoff is that realm configurations must be kept in sync across environments, which pushes you toward GitOps.
Per-application realms (one realm per business domain or product) give the strongest isolation and let separate teams manage their own clients and users. It scales cleanly to a platform model where each team owns their realm. The downsides: users don’t get SSO across realms, realm count can grow large, and management overhead multiplies.
Single realm with many clients works well when all your applications share the same user population and you want true SSO across them. Protocol mappers and client-specific scopes let you customize token content per-application. With Keycloak 26’s Organizations feature now GA, you can implement B2B multi-tenancy (multiple companies/tenants) within a single realm — each Organization gets its own identity providers, invitation flows, and organization-level roles without needing separate realms.
Rule of thumb: start with per-environment realms within a single Keycloak installation. Split into per-application realms only when a team genuinely needs isolation from other teams’ users or clients.
Realm Settings That Matter
Token lifetimes (Realm Settings → Tokens):
- Access Token Lifespan: default 5 minutes — keep it short; refresh tokens do the heavy lifting
- Refresh Token Lifespan: controlled by SSO session max (default 30 days) and SSO session idle (default 30 min)
- Do not conflate “access token” lifetime with “user session” lifetime
Brute Force Protection (Realm Settings → Security Defenses): Enable this. Keycloak 26.6 adds separate brute force counters for passwords and OTP codes. Configure:
- Max login failures: 5
- Wait increment: 30 seconds
- Max wait: 15 minutes
- Failure reset time: 12 hours
SSL Required: Set to all requests in production. external requests (the default) allows HTTP on private IP ranges — acceptable only for internal dev environments.
Password Policy: At minimum enforce: minimum length (12+), not username, not email, digits required, special characters required, and a password history of 5.
Realm Import/Export for GitOps
Realm export is the foundation of infrastructure-as-code for Keycloak. Export produces JSON that can be committed to Git and applied via CI/CD.
|
|
The exported JSON supports environment variable substitution with ${ENV_VAR} syntax — use this to parameterize secrets and hostnames across environments:
|
|
Important: CLI exports include hashed passwords and LDAP bind credentials. Treat export files as secrets. Admin Console exports mask credentials with asterisks — they are useful for diffing configuration, not for backup or server transfer.
Clients: The Authorization Surface Area
A client represents an application that uses Keycloak for authentication. Misconfigured clients are the most common source of security incidents in Keycloak deployments.
Confidential vs Public Clients
Confidential clients have a server-side component that can hold a secret. They authenticate to Keycloak’s token endpoint using client_secret_basic (HTTP Basic Auth with client ID and secret), client_secret_jwt (a JWT signed with the shared secret), or private_key_jwt (a JWT signed with an asymmetric private key). Use confidential clients for backend APIs, server-rendered apps, and service accounts.
Public clients cannot hold a secret (SPAs, mobile apps). They rely on PKCE (Proof Key for Code Exchange) to prevent authorization code interception. Always enable PKCE for public clients:
- Set
Proof Key for Code Exchange Code Challenge MethodtoS256 - Never rely on
plainPKCE — it provides no real security benefit
Redirect URI Wildcard Gotcha
This is the most common Keycloak misconfiguration leading to open redirector vulnerabilities. Keycloak allows wildcard redirect URIs: https://app.example.com/*. The problem: https://app.example.com.evil.com/callback does NOT match this pattern, but https://app.example.com/anything/at/all does — including paths that could be used to exfiltrate authorization codes.
Production rule: use exact redirect URIs wherever possible. If you need multiple, list them explicitly. Never use * as the entire URI. The pattern https://app.example.com/* is acceptable; * alone as a valid redirect URI is an immediate CVE-class issue.
Service Accounts (Client Credentials Grant)
Enable the Service accounts roles toggle on a confidential client to enable the OAuth2 client credentials grant. The service account user gets a system-generated username (service-account-<clientId>) and can be assigned realm roles and client roles. Use this for machine-to-machine authentication:
|
|
Client Roles vs Realm Roles
Realm roles are available across all clients in a realm. Use them for coarse-grained permissions (admin, user, moderator) that span multiple applications.
Client roles are scoped to a specific client. Use them for application-specific permissions (e.g., my-app:read, my-app:write). They appear in the resource_access claim in tokens.
Both are included in the JWT via the built-in roles scope, but the mapper behavior differs. The realm_access.roles claim contains realm roles; resource_access.<clientId>.roles contains client roles.
Client Scopes and Protocol Mappers
This is where Keycloak’s token content model lives, and it is — frankly — over-engineered compared to simpler OIDC providers. Spend time here; it pays dividends.
The Scope Model
A client scope is a named container for protocol mappers and role scope mappings. Scopes can be assigned to clients as default (always included) or optional (included only when explicitly requested via the scope parameter in the authorization request).
Built-in scopes you will encounter:
openid— always required for OIDC; triggers ID token issuanceprofile— mapsname,given_name,family_name,preferred_username,picture,website,gender,birthdate,locale,zoneinfo,updated_atemail— mapsemailandemail_verifiedroles— maps realm and client roles intorealm_accessandresource_accessclaimsaddress— maps the OpenID Connect address claimphone— mapsphone_numberandphone_number_verifiedoffline_access— enables refresh tokens that survive server restarts (offline sessions stored in DB)microprofile-jwt— addsgroupsandupnclaims for Jakarta EE/MicroProfile compatibility
Scope parameter in authorization requests: The scope parameter in the OAuth2 authorization URL controls which optional scopes are included. scope=openid email profile groups would include the groups scope (if configured as optional).
Protocol Mappers
Protocol mappers are the functions that produce individual claims in tokens. Each scope contains zero or more mappers. Common mapper types:
User Attribute Mapper — maps a custom user attribute to a token claim:
Mapper Type: User Attribute
User Attribute: department
Token Claim Name: department
Claim JSON Type: String
Add to ID token: on
Add to access token: on
Add to userinfo: on
Group Membership Mapper — adds user group memberships as a claim:
Mapper Type: Group Membership
Token Claim Name: groups
Full group path: off (produces "engineering", not "/org/engineering")
Audience Mapper — adds a service’s client ID to the aud claim of access tokens. This is how backend APIs validate that a token was issued for them:
Mapper Type: Audience
Included Client Audience: my-api-service
Add to access token: on
Hardcoded Claim Mapper — injects a static value into every token:
Mapper Type: Hardcoded claim
Token Claim Name: tenant
Claim Value: acme-corp
User Realm Role Mapper — maps realm roles into a custom claim (useful if you want roles as a flat array instead of nested under realm_access):
Mapper Type: User Realm Role
Token Claim Name: roles
Multivalued: on
ID Token vs Access Token vs Userinfo
This distinction trips up almost every team:
- ID Token: A JWT issued alongside the access token in OIDC. Contains user identity claims (sub, name, email). Consumed by the client application to learn who the user is. Should be short-lived and not used for API authorization.
- Access Token: The credential presented to APIs. May be a JWT (opaque tokens are also supported). APIs validate this token’s signature and check the
audclaim. Do not add sensitive PII here — this token gets logged in reverse proxies. - UserInfo Endpoint (
/protocol/openid-connect/userinfo): Returns user claims on demand. Useful when you want to keep access tokens small (no embedded claims) and fetch fresh user data per request.
The Add to access token / Add to ID token / Add to userinfo toggles on each mapper let you route claims to the right token type.
User Federation: LDAP and Active Directory
User federation lets Keycloak authenticate against an existing directory without migrating users into its database. LDAP/AD federation is the most common enterprise integration.
Provider Configuration
Navigate to User Federation → Add LDAP provider in the Admin Console. Critical settings:
| Setting | Active Directory | OpenLDAP |
|---|---|---|
| Vendor | Active Directory | Other |
| Connection URL | ldap://dc01.corp.example.com:389 |
ldap://ldap.example.com:389 |
| Enable StartTLS | recommended | recommended |
| Bind DN | CN=keycloak-svc,OU=ServiceAccounts,DC=corp,DC=example,DC=com |
cn=keycloak,dc=example,dc=com |
| Bind Credential | (service account password) | (bind password) |
| Users DN | OU=Users,DC=corp,DC=example,DC=com |
ou=people,dc=example,dc=com |
| User object classes | person, organizationalPerson, user |
inetOrgPerson, organizationalPerson |
| Username LDAP attribute | sAMAccountName |
uid |
| RDN LDAP attribute | cn |
uid |
| UUID LDAP attribute | objectGUID |
entryUUID |
| User DN attribute | distinguishedName |
entryDN |
Edit Modes
- READ_ONLY: Passwords authenticated against LDAP. User data read from LDAP; cannot be modified in Keycloak. Use this for AD where you don’t want Keycloak writing back.
- WRITABLE: Keycloak writes attribute changes back to LDAP. Requires write permissions on the bind account.
- UNSYNCED: Users imported from LDAP but stored locally; Keycloak data and LDAP diverge. Useful during migrations.
Sync Configuration
- Periodic Full Sync: Scans all LDAP users on a schedule. Set to every 24 hours for large directories.
- Periodic Changed Users Sync: Syncs only users modified since last sync (requires
modifyTimestampsupport). Set to every 15 minutes. - Import Users toggle: When enabled, Keycloak creates local user records mapped to LDAP. When disabled, no local records — each auth goes directly to LDAP.
LDAP Attribute Mappers
Add attribute mappers to import LDAP attributes into user profile fields:
- User Attribute Mapper:
ldap.attribute=mail → user.model.attribute=email - Full Name Mapper:
cn → first + last name - MSAD Account Controls Mapper (AD-specific): maps
pwdLastSet,userAccountControlfor account enabled/disabled and password expiry
Group Mapper
The Group Mapper syncs LDAP groups into Keycloak groups:
LDAP Groups DN: OU=Groups,DC=corp,DC=example,DC=com
Group Name LDAP Attribute: cn
Group Object Classes: group
Membership LDAP Attribute: member
Membership Attribute Type: DN
Drop non-existing groups during sync: true
Kerberos Integration
For true Kerberos SSO (SPNEGO/negotiate):
- Enable Allow Kerberos Authentication on the LDAP federation provider
- Set Kerberos Realm (e.g.,
CORP.EXAMPLE.COM) - Set Server Principal (
HTTP/keycloak.corp.example.com@CORP.EXAMPLE.COM) - Set KeyTab path (
/etc/krb5.keytab— mount this as a Secret in Kubernetes) - Enable
Use Kerberos For Password Authenticationto use Kerberos credentials for initial LDAP bind
Debugging LDAP Connectivity
|
|
The Admin Console also has a Test connection and Test authentication button on the LDAP federation config page — use them before saving.
Identity Providers: Social Login and External Federation
Keycloak can broker external identity providers (IdPs), letting users log in via GitHub, Google, corporate SAML IdPs, or other OIDC providers.
Adding an External OIDC IdP
Navigate to Identity Providers → OpenID Connect v1.0 (or Social → GitHub, Google, etc.). Keycloak discovers endpoint URLs via the .well-known/openid-configuration URL automatically.
For GitHub:
- Create an OAuth App at github.com/settings/developers
- Set callback URL to
https://auth.example.com/realms/myrealm/broker/github/endpoint - In Keycloak: Client ID and Secret from GitHub, Scopes:
user:email
First Login Flow
When a federated user authenticates for the first time, Keycloak runs the First Broker Login flow. The default flow checks if an existing local user matches by email or username. Options:
- Create new user: always create a local account linked to the IdP identity
- Link to existing account: merge with an existing local account if email matches (requires email verification)
- Confirm link with existing account by re-authentication: prompts the user to authenticate their existing account before linking
Customize this flow per-IdP. The default is usually fine for OIDC social providers but needs thought for enterprise SAML federation.
Passing Through IdP Tokens
Enable Store Tokens and Stored Tokens Readable on the IdP config to preserve the upstream access token. Clients can then retrieve it via the account service or REST API for calling upstream APIs on behalf of the user.
Authentication Flows
Authentication flows define the execution steps a user goes through during login, registration, or credential reset. They’re one of Keycloak’s most powerful features and one of its most complex configuration surfaces.
Built-in Flows
- browser: The standard login form (username/password), with optional OTP as a conditional step
- direct grant: Resource Owner Password Credentials (ROPC) flow — avoid in new applications
- registration: Sign-up flow
- reset credentials: Forgot-password flow
- first broker login: As above
- docker auth: Docker Registry v2 authentication
Execution Requirements
Each step in a flow has a requirement:
- Required: Must succeed; failure blocks the flow
- Alternative: Success of any one alternative step satisfies this level
- Conditional: Runs only if a condition evaluates to true
- Disabled: Skipped entirely
Adding OTP (TOTP)
To require TOTP as a second factor:
- Duplicate the
browserflow - After the
Username Password Formstep, add a subflow withAlternativerequirement - Add
OTP Formas aRequiredstep within the subflow - Set the flow as the binding for
Browser Flowin Realm Settings
Keycloak supports both TOTP (time-based, Google Authenticator, FreeOTP, Authy) and HOTP (counter-based). The QR code registration is handled by the OTP Setup Required Action.
WebAuthn and Passkeys
As of Keycloak 26.4, passkeys are fully supported (promoted from preview). WebAuthn enables both second-factor hardware tokens and passwordless authentication.
WebAuthn as Second Factor:
- Register the
WebAuthn Authenticatorexecution in the browser flow at the same level asOTP Form - Users register a security key or platform authenticator via Required Action
- The
WebAuthn Authenticatormapper in the account console handles device management
Passkeys (Passwordless):
- Keycloak 26.4 introduced conditional UI passkeys — the login form checks for available passkeys before showing the password field
- No browser flow modification required; passkeys are automatically offered
- The new
Conditional - credentialauthenticator checks whether a specific credential type was used during authentication
Conditional Executions
Conditional subflows let you apply MFA selectively:
Condition - User Role: required_role=mfa-required → Required
OTP Form: Required
Available conditions:
- Condition - User Role: checks realm or client role membership
- Condition - User Configured: checks if the user has configured a specific credential
- Condition - User Attribute: checks user attribute value
- Condition - Sub-Flow Result: checks result of a prior subflow
Recovery Codes
Keycloak 26.3 promoted Recovery Codes from preview to supported. Enable the generate-recovery-codes Required Action to let users generate backup codes for when their OTP device is unavailable. Configure in the authentication flow alongside OTP/WebAuthn.
Events and Audit Logging
Event Types
Keycloak distinguishes two event categories:
Login (User) Events: LOGIN, LOGIN_ERROR, REGISTER, REGISTER_ERROR, LOGOUT, CODE_TO_TOKEN, CODE_TO_TOKEN_ERROR, REFRESH_TOKEN, REFRESH_TOKEN_ERROR, CLIENT_LOGIN, CLIENT_LOGIN_ERROR, TOKEN_EXCHANGE, IDENTITY_PROVIDER_LOGIN, RESET_PASSWORD, UPDATE_PROFILE, GRANT_CONSENT, REVOKE_GRANT
Admin Events: All Admin REST API calls: realm creation, user updates, client modifications, role assignments, etc. Admin events have resourceType (USER, CLIENT, REALM, etc.), operationType (CREATE, UPDATE, DELETE, ACTION), resourcePath, and a representation field containing the changed object.
Built-in Event Listeners
jboss-logging(always active): writes events to the Keycloak log. ERROR events are logged at WARN level; DEBUG-level events require enabling specific event types.email: sends email notifications to users on login errors. Requires SMTP config.
Enabling Event Storage
Go to Realm Settings → Events:
- Enable Save Events for user events
- Enable Save Admin Events and optionally Include Representation
- Set Expiration: events older than this are purged (default: never — set this to 30-90 days or your database will grow unbounded)
- Select specific event types to store (default: all error types only)
Custom Event Listener SPI for SIEM Integration
To ship events to a SIEM (Splunk, Elasticsearch, Datadog, etc.), implement a custom EventListenerProvider:
|
|
Register by creating META-INF/services/org.keycloak.events.EventListenerProviderFactory with your factory class name. Package as a JAR, drop in providers/, and register in the realm’s Event Listeners list.
Kubernetes Deployment with High Availability
This section covers both the Operator-based deployment (recommended) and raw Deployment YAML (for environments without the Operator).
The Keycloak Operator
The official Keycloak Operator is installed from OperatorHub or directly from the Keycloak GitHub releases. The API version is k8s.keycloak.org/v2beta1 (not v2alpha1 — that was superseded). The Operator manages:
- Keycloak cluster lifecycle
- TLS certificate integration
- Database secret management
- Rolling updates with zero downtime (enabled by default in 26.6)
- Automatic ServiceMonitor creation for Prometheus
Install the Operator:
|
|
Database Secret:
|
|
PostgreSQL StatefulSet (production-grade — use a managed database or CloudNativePG in real deployments):
|
|
Keycloak CR — Production HA Deployment:
|
|
PodDisruptionBudget (create separately):
|
|
KeycloakRealmImport CRD (for GitOps realm management):
|
|
Raw Deployment YAML (Without Operator)
For environments where you can’t install the Operator:
|
|
Cache Configuration: jdbc-ping vs kubernetes Stack
Since Keycloak 26.1, jdbc-ping is the default cache stack for non-Operator deployments. It uses the Keycloak database to store cluster membership information — no UDP multicast, no DNS configuration, no headless service required (though a headless service is still good practice for network policies).
The kubernetes (DNS_PING) stack still works but is deprecated. If you’re starting fresh, let jdbc-ping be the default. If you’re on the Operator, it continues to configure kubernetes stack automatically but the Operator team is working on migrating to jdbc-ping.
Probes and Health Endpoints
All health and metrics endpoints are served on port 9000 (the management interface), isolated from application traffic on port 8080/8443.
| Endpoint | Purpose |
|---|---|
GET :9000/health |
Overall health (combines ready + live) |
GET :9000/health/ready |
Readiness — Keycloak has initialized and can serve requests |
GET :9000/health/live |
Liveness — process is alive |
GET :9000/health/started |
Started — use this for startupProbe |
GET :9000/metrics |
Prometheus metrics (Micrometer format) |
Enable them at build time: kc.sh build --health-enabled=true --metrics-enabled=true, or via KC_HEALTH_ENABLED=true and KC_METRICS_ENABLED=true environment variables.
Prometheus ServiceMonitor
|
|
The Keycloak Operator creates this ServiceMonitor automatically when metrics-enabled: "true" is set in additionalOptions. For raw deployments, apply the above manually.
Key metrics to alert on:
keycloak_logins_total— successful logins by realm and providerkeycloak_failed_login_attempts_total— failed logins by realm and errorkeycloak_active_sessions— active SSO sessions by realmjvm_memory_used_bytes— heap/non-heap usageagroal_connections_active_count— database connection pool activevendor_statistics_entries— Infinispan cache entry counts
Local Development: Docker Compose
|
|
Access: http://localhost:8080/admin — Username: admin, Password: admin
Theme Customization
Keycloak supports five theme types: login, account, admin, email, and welcome. Login and email themes are the most commonly customized.
Directory Structure
themes/
└── my-brand/
├── login/
│ ├── theme.properties # parent=keycloak OR parent=base
│ ├── login.ftl # Override login page template
│ ├── register.ftl
│ ├── error.ftl
│ ├── resources/
│ │ ├── css/
│ │ │ └── login.css
│ │ ├── js/
│ │ └── img/
│ │ └── logo.png
│ └── messages/
│ ├── messages_en.properties
│ └── messages_de.properties
├── email/
│ ├── theme.properties # parent=base
│ ├── messages/
│ │ └── messages_en.properties
│ └── html/
│ ├── email-verification.ftl
│ └── password-reset.ftl
└── account/
└── theme.properties # parent=keycloak.v3
theme.properties
|
|
For a minimal CSS-only customization, extend parent=keycloak and override CSS variables. The Keycloak login theme uses CSS custom properties for colors, fonts, and spacing, making a CSS-only theme straightforward.
Freemarker Templates
Override specific .ftl files to change HTML structure. Copy the template from the base theme and modify. The base theme is embedded in keycloak-themes-<version>.jar — extract it to see all available templates:
|
|
Keycloakify: React-based Login Themes
For teams who want to build login UIs in React (with TypeScript, component testing, Storybook integration), Keycloakify is the de facto standard. It compiles a React application into a Keycloak JAR theme using keycloakify build. The resulting JAR drops into providers/.
Account Console (v3)
The Account Console was rewritten in React (Keycloak Account Console v3). Custom account themes using the v3 base can extend parent=keycloak.v3. The React source is available in the Keycloak repository if you need to fork and customize at the component level.
JAR Deployment
For production, package themes as JARs:
META-INF/
keycloak-themes.json
theme/
my-brand/
login/
...
META-INF/keycloak-themes.json:
|
|
Build: jar cf my-brand-theme.jar -C src/ .
Drop the JAR into providers/ and rebuild: kc.sh build. No restart needed if using the dynamic provider loader (enabled by default in Keycloak 26).
Kubernetes Theme Deployment
Via init container (preferred — keeps the main image immutable):
|
|
Via ConfigMap (for small CSS/resource-only themes, not JAR):
ConfigMaps have a 1MB limit. A full theme JAR typically exceeds this. Use an init container with a container image for JAR-based themes.
Production Operations
The kc.sh Build Pipeline
The production deployment workflow:
|
|
The --optimized flag tells kc.sh start to skip the build phase. Without it, Keycloak runs a build step every time it starts — slow and risky in production containers.
Database Schema Migration
Keycloak handles schema migrations automatically on startup (via Liquibase). There is no manual migration step. When upgrading Keycloak versions:
- Ensure the new Keycloak version’s migration scripts are compatible with your current schema version
- Check the Upgrading Guide for each version
- Run one pod at a time on initial upgrade (reduce replicas to 1 while migration runs, then scale back up)
- Back up your database before any upgrade
Admin CLI (kcadm.sh)
kcadm.sh is the command-line admin client. Essential for scripting and CI/CD pipelines:
|
|
Realm Export Without Secrets
|
|
Cache Invalidation
When you modify realm configuration through the Admin Console or REST API, Keycloak handles cache invalidation automatically via the distributed Infinispan cache — all cluster nodes see the change via the replication channel. You do not need to restart pods for configuration changes.
The exception: changes to the build configuration (feature flags, database vendor, enabled providers) require a rebuild and rolling restart.
Realm JSON Export Snippet (GitOps)
|
|
Common Pitfalls and Operational Reality
After running Keycloak at scale the same failure modes appear repeatedly. Know these before they happen to you.
1. Wildcard Redirect URI
Already covered, but worth repeating: https://app.example.com/* is reasonable. * by itself as a valid redirect is a textbook open redirector. Even https://app.example.com/* can be dangerous if your app has user-controlled path segments. Validate URI patterns in code reviews.
2. H2 in Production
The default embedded H2 database survives exactly one Keycloak pod restart. Data is written to data/h2/keycloak.mv.db inside the container filesystem. When the pod is replaced, that file is gone. Users disappear, sessions disappear, client secrets disappear. Always configure PostgreSQL (or MySQL/MariaDB) from day one. There is no migration path from H2 to PostgreSQL without a full realm export/reimport.
3. Running Apps in the Master Realm
The master realm admin account is essentially root for the entire Keycloak installation. An OIDC client in master that gets compromised can manage all other realms. Create a dedicated realm for every workload.
4. Token Expiry Confusion
Teams regularly confuse these four lifetimes:
- Access token lifespan (default: 5 min): how long the JWT is valid for API calls
- SSO session idle timeout (default: 30 min): inactivity window before the user session expires
- SSO session max lifespan (default: 10 hours): absolute maximum session length
- Refresh token lifespan: governed by SSO session settings, not a separate token setting
A user can be “logged in” (active SSO session) while their current access token is expired. The client is responsible for using the refresh token to get a new access token. Most OAuth2 client libraries handle this automatically.
Setting the access token lifespan to 30 minutes “to avoid frequent refreshes” is a mistake — it means a stolen access token is valid for 30 minutes with no way to revoke it (since JWTs are stateless).
5. No Brute Force Protection
Off by default. Every Keycloak instance that is publicly accessible needs brute force protection enabled. Without it, an attacker can attempt unlimited password guesses. Enable it in Realm Settings → Security Defenses.
6. Not Setting KC_HTTP_ENABLED=true With KC_PROXY_HEADERS
Since Keycloak 26 removed the old KC_PROXY=edge option, the replacement is KC_PROXY_HEADERS=xforwarded. But KC_PROXY_HEADERS alone doesn’t work — you must also set KC_HTTP_ENABLED=true to allow HTTP connections from the reverse proxy. Omitting this causes HTTPS required errors even when the user is connecting via HTTPS through the load balancer.
7. Clock Skew
JWTs use iat (issued at) and exp (expiration) Unix timestamps. If the Keycloak server clock is ahead of the client clock, tokens will appear expired immediately. If the client clock is ahead, tokens may pass expiry checks they should fail. Use NTP sync everywhere. Keycloak has a configurable Not Before Policy that can help with replay attacks but won’t fix clock skew.
8. CORS Misconfiguration
Set Web Origins on your client to the exact origins that should make cross-origin requests. + as web origin copies the list from Valid Redirect URIs (common shortcut). * allows all origins — acceptable for fully public APIs, a security risk for APIs with user-specific data.
9. Admin Console Export Includes Hashed Passwords
A realm export from kc.sh export includes Bcrypt/SHA-512 hashed passwords for local users. These cannot be reversed but they can be imported directly into another Keycloak instance, effectively copying all user credentials. Treat export files as secrets and don’t commit them unencrypted to Git.
10. Database Connection Pool Sizing
The default pool size is too small for production traffic. Set poolMinSize, poolInitialSize, and poolMaxSize to the same value (this matters for JDBC statement caching — varying min/max forces pool drain and refill, which flushes the statement cache). Start at 30 connections per pod for moderate traffic; monitor agroal_connections_active_count and adjust.
11. Ignoring the aud Claim in API Validation
Access tokens should contain the aud (audience) claim identifying which services should accept them. Without an Audience mapper on your API’s client scope, access tokens won’t contain your API’s client ID as an audience. If your API doesn’t validate aud, any valid Keycloak access token from any client in the realm can call it.
12. Session Limits Under Load
Keycloak stores session data in Infinispan (in-memory distributed cache). Under high concurrency with many concurrent sessions, memory pressure grows. Monitor keycloak_active_sessions per realm and jvm_memory_used_bytes. Tune SSO Session Max to balance user experience against memory consumption.
Key Architectural Decisions Checklist
Before going to production, verify:
- PostgreSQL (not H2) configured and backed up
- Master realm used only for Keycloak administration
- Brute force protection enabled on all realms
- SSL required set to
all requests - Strong password policy configured
- Exact redirect URIs for all clients (no bare
*) - PKCE enabled for all public clients
- Audience mappers configured for all backend APIs
- Event storage enabled with expiration policy
- Access token lifespan ≤ 5 minutes
- Health and metrics endpoints enabled
- PodDisruptionBudget in place (minAvailable: replicas-1)
- Pod anti-affinity rules across nodes and zones
- KC_PROXY_HEADERS=xforwarded + KC_HTTP_ENABLED=true (for proxy deployments)
- LDAP bind credential rotated from initial setup
- Theme customization tested across all locales
- Realm export committed to Git (without user passwords if GitOps)
- Upgrade runbook documented for database migration procedure
- Grafana dashboards for keycloak_logins_total and keycloak_failed_login_attempts_total
What’s New in Keycloak 26.x
Running Keycloak 26.6 means access to:
- Organizations (GA since 26.0): First-class B2B multi-tenancy within a single realm. Each organization gets its own IdPs, invitation workflows, and organization-scoped roles.
- Passkeys (Supported since 26.4): Passwordless authentication with conditional UI. No browser flow modification required.
- DPoP (Fully supported since 26.4): OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer for sender-constrained tokens.
- FAPI 2 Final (26.4): Full support for the OpenID Foundation’s Financial-grade API 2.0 Security Profile and Message Signing.
- Recovery Codes (Supported since 26.3): Backup codes for MFA recovery.
- Zero-Downtime Patch Updates (26.6): Rolling updates within a minor release stream enabled by default in the Operator.
- JWT Authorization Grant (26.6): RFC 7523 external-to-internal token exchange.
- Federated Client Authentication (26.6): Clients authenticate using Kubernetes Service Account tokens or OIDC IdP credentials — no shared secret management.
- Workflows (26.6): YAML-defined automation for realm administrative tasks.
- jdbc-ping default (26.1): No more DNS_PING setup for clustering in most environments.
- Token Exchange V1 deprecated: Migrate to the V2 Identity Brokering APIs.
The operational reality of running your own identity provider is that you’re taking on responsibility for the security critical path of every application in your organization. Keycloak makes that tractable — but only if the configuration is correct, the database is reliable, the clustering is healthy, and someone is watching the metrics. The reward is full control: custom authentication flows, your own branding, deep LDAP integration, and no per-MAU pricing. For organizations with the platform maturity to operate it, it’s an excellent choice. For organizations that need to focus engineering capacity elsewhere, managed Keycloak services (cloud-iam.com, Phase Two, Red Hat SSO on OpenShift) are worth the trade-off.
Sources:
- Keycloak 26.6.0 Released
- Keycloak 26.5.6 Released
- Keycloak Passkeys Support in 26.4
- Keycloak 26.4.0 Released
- Keycloak 26.1.0 Released — jdbc-ping default
- Keycloak Operator Basic Deployment
- Keycloak Operator Advanced Configuration
- Deploying Keycloak for HA with the Operator
- Keycloak Reverse Proxy Configuration
- KC_PROXY deprecation and KC_PROXY_HEADERS clarification
- Keycloak Server Caching Documentation
- Keycloak Management Interface
- Keycloak Metrics Configuration
- Keycloak Import/Export Guide
- Keycloak Production Configuration
- Keycloak Container Guide
- Keycloak Theme Documentation
- Keycloak Organizations Feature (CNCF)
- Keycloak Multi-Tenancy Guide
- Red Hat Build of Keycloak 26.0 Release Notes
- Keycloak Releases on GitHub
- jdbc-ping Kubernetes Discovery Issue
- Keycloak LDAP/AD Federation Configuration
- Keycloak Kerberos SSO with Active Directory
- Keycloak Event Listener SPI
- Keycloak SIEM SPI Example
- kcadm.sh Command Examples
- Keycloak EndOfLife Dates
- Keycloak Service Level Indicators
Comments