From the WildFly legacy to the lean Quarkus distribution: what has changed since the big architectural migration, how a productive cluster with Infinispan and external PostgreSQL is built, how themes for login and email can be customised, and how realm exports open the path to GitOps-ready IAM configuration. A ten-page deep dive on the tool we encounter most often in our client projects in regulated industries.
Positioning — the IAM for regulated industries
Keycloak is an open-source identity and access management platform under Apache 2.0 licence, originating from a JBoss project at Red Hat. Its commercial sibling under the name Red Hat build of Keycloak (formerly RH-SSO) is deployed in most regulated setups in Germany — banks, insurers, energy utilities, and the public administration have converged on the tool because it can be operated self-hosted and covers all common standards (OIDC, OAuth 2.0, SAML 2.0).
Three properties make Keycloak the de-facto standard in our projects. First: self-hosted and EU-compliant. Where SaaS solutions such as Auth0 or Okta fail data-protection or KRITIS requirements, Keycloak runs in your own data centre or in the EU cloud. Second: standards-compliant and protocol-rich. OIDC with all flows (Authorization Code with PKCE, Client Credentials, Device Code, CIBA), SAML as identity provider and service provider, plus identity brokering to external IdPs and user federation against LDAP or Active Directory. Third: mature and maintained. In production since 2014, financially backed by Red Hat, with a large open-source community and monthly patch releases.
One of the most important turning points in the project's history was version 17 (April 2022), with which the distribution was switched from WildFly to Quarkus. The Quarkus distribution starts in under two seconds, has a significantly smaller RAM footprint, and is configured via a single file (keycloak.conf) or environment variables — a noticeable simplification compared to the old WildFly setup with its XML subsystem files. Anyone setting up new today should use only the Quarkus distribution; the WildFly variant has been unsupported since version 20. Many of our consulting engagements start exactly here: an existing client still runs the old WildFly install and needs a clean migration path.
What Keycloak is not should also be stated clearly. It is not a customer identity platform with ready-made marketing tooling — anyone looking for an Auth0 with drag-and-drop login flows, A/B tests, and prefabricated social-login buttons for a consumer product is better served there. Keycloak demands a baseline of engineering: realm design, theme work, cluster operations, upgrade discipline. In return you get a tool that stays in your own hands and creates no vendor lock-in.
Core concepts — realm, client, user, role
Five concepts hold Keycloak together. Distinguish them, and you have the mental model — the rest is configuration and theme work.
Realm — the tenant container
A realm is the top-level logical container. Inside a realm exist users, clients, roles, groups, and themes — fully isolated from other realms. Multi-tenant setups typically model tenants as their own realms: a "staff" realm for the internal workforce, a "customer portal" realm for external end users, possibly another realm per business unit or brand. The master realm has a special role: it manages server configuration and realm administrators — end users have no business there.
Client — the registered application
A client is an application registered with Keycloak that uses authentication. OIDC clients receive a client ID and (for trusted back-end clients) a client secret. Four important distinctions: public clients (browser SPAs, mobile apps — no secret, use PKCE), confidential clients (server-to-server, with secret or mTLS), bearer-only clients (resource servers that only validate tokens), and service-account clients (machine-to-machine without an end user). SAML clients exist in addition for legacy or B2B integrations where SAML still dominates.
User, role, group
A user is an identity within a realm. Roles are the permission units — they exist on two levels: realm roles are realm-wide (such as "realm-admin", "auditor"), client roles are tied to a specific client (such as "order-service.viewer", "order-service.editor"). Groups are collections of users that inherit shared attributes and roles. A group "department-orders" with two assigned client roles automatically passes both on to its members — the default path for scalable permission management.
Composite roles are a frequently overlooked feature: a role can include other roles. "order-service.admin" as a composite role can implicitly include "order-service.viewer" and "order-service.editor" — the user's tokens then carry all three roles. This is how permission hierarchies are modelled cleanly without rebuilding role logic in every application.
Authentication flow
The authentication flow is the configurable chain of steps a login traverses: username + password → optional MFA → optional risk evaluation → token issuance. Keycloak ships default flows that cover 95 percent of setups — customisation is possible at any time, for instance to enforce a two-factor requirement for administrative accounts without burdening other users.
Authentication flows in practice
Choosing the right flow for each application has lasting consequences for security and maintainability. It is one of the few truly impactful architectural decisions around Keycloak.
Authorization Code with PKCE — the standard for interactive apps
Whether it is a classical server-rendered web app or a single-page application, the Authorization Code Flow with PKCE (Proof Key for Code Exchange) is today's right choice. The older Implicit Flow is obsolete under OAuth 2.1 — finding it in legacy applications is a migration case waiting to happen. PKCE protects against authorization-code interception and is recommended even for confidential clients; in public clients (browser, mobile) it is mandatory.
Client Credentials — service-to-service
When two backend services talk to each other and no end user is involved, the Client Credentials Flow is right. The service authenticates with its client secret (or mTLS certificate) and receives an access token with the permissions of its service account. A bread-and-butter pattern in microservice architectures.
Devices without a browser or with limited input use the Device Authorization Flow: the user is shown a short code and types it on another device (smartphone), which carries out the browser login. Also commonly used for CLI tools — the az and gh CLIs are well-known examples outside the Keycloak world.
A variant that is becoming increasingly relevant in banking: the endpoint is not the user's browser but a notification to their smartphone (push notification with a confirmation dialog). CIBA is part of the FAPI 2.0 profile and will become the standard in PSD2/PSD3-compliant banking APIs.
SAML 2.0 — legacy and B2B
Despite OIDC's dominance, SAML lives on — especially in B2B federations, the public sector, and enterprise SaaS integrations. Keycloak acts as a SAML identity provider (for external service providers) and as a SAML service provider (for an external IdP). The configuration is more extensive than OIDC; once set up, it runs stably.
Cluster architecture
A productive Keycloak setup consists of several components that together carry high availability, shared cache state, and persistent storage. The figure below shows the typical topology for a single-site installation.
Figure 1 — A three-node cluster with reverse proxy, Infinispan-based cache replication, and external PostgreSQL as the configuration and identity store. An optional LDAP or Active Directory server serves as a user-federation source. Every node holds a synchronised cache; on node failure, the reverse proxy and Infinispan automatically route sessions to the remaining nodes.
What Infinispan actually stores
The cache layer in Keycloak holds three kinds of data: realm cache (realm definitions, client configurations — rarely changed, frequently read), session cache (active user sessions, refresh tokens, authorization codes — central for stateful behaviour), and login failures (for brute-force protection). The first two are spread across nodes in a distributed cache mode; each piece of information lives on multiple nodes and survives a node failure.
Stickiness at the reverse proxy
Even though the Infinispan cluster makes every session available on every node, session affinity has proven valuable in practice — the reverse proxy routes a user to the same node where possible. Performance (cache-hit ratios) is one reason; the other is stability during longer OIDC flows with multiple steps (login, MFA, consent), which run more smoothly on a consistent node.
Cross-data centre
For multi-site setups, Keycloak offers a multi-site mode with active-active replication across two data centres. Setting it up is more demanding — Infinispan over RPC, Postgres replication, coordinated failover behaviour. In most Tenvias projects, a single-site HA setup with three nodes is sufficient; cross-DC becomes concretely relevant for a banking client with a real disaster-recovery requirement.
Installing and configuring the Quarkus distribution
A near-production configuration consists of three building blocks: the Quarkus distribution, an external PostgreSQL database, and a thoughtful configuration file. The steps below show a runnable single-node setup that can be scaled directly into an HA cluster.
Prerequisites
Java 17 or newer, an external PostgreSQL server (at least 13, ideally 15+), an FQDN with a valid TLS certificate behind a reverse proxy. The bundled kc.sh file is the standard tool for start, build, and migration.
Configuration file keycloak.conf
All settings live in conf/keycloak.conf. The most important blocks:
Sensitive values (passwords, client secrets) do not belong in the file but in environment variables with the KC_ prefix. db-password is set via KC_DB_PASSWORD. In Kubernetes, this is typically wired through a secret.
Build optimisation
A Quarkus-distribution quirk: before a productive start, a kc.sh build step belongs in the workflow, compiling static settings (database driver, enabled features) into an optimised distribution. Forgetting this yields a warning at startup and a slower boot. In container images, this is done once at image build:
FROM quay.io/keycloak/keycloak:26.0 as builder
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:26.0
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
CMD ["start", "--optimized"]
Initial admin user
On the very first start, Keycloak looks for a bootstrap admin in the environment variables KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD. Directly after the first login, a regular admin user should be created and the bootstrap path disabled. In production, the initial password should be rotated after exactly that step.
Kubernetes with the operator
For Kubernetes setups, the official Keycloak operator is the established path. It handles cluster scaling, the Postgres connection, and the realm configuration via CRDs. A minimal definition:
The operator starts three Keycloak pods with Infinispan discovery over the Kubernetes stack, loads the DB password from the secret, and ties it all to an ingress definition. Realm configurations can be maintained declaratively through the additional CRD KeycloakRealmImport — the path to GitOps-ready IAM.
Themes and custom branding
An underrated topic in Keycloak: the bundled login front end looks like Keycloak — and in most client projects that is not what the business side accepts. Themes allow a full re-branding without touching the server code.
Theme types
Keycloak knows four theme types that can all be customised separately: login (login pages, password reset, consent), account (the account console where end users manage their own profile), email (email templates for confirmation and reset mails), and admin (the admin console — practically never customised in productive setups). For a customer branding, login and email are usually enough.
Theme directory structure
A theme lives in themes/<theme-name>/<type>/ with a clear substructure:
The theme.properties of each theme type allows inheriting from a parent theme — typically parent=keycloak. The own theme then only overrides files that actually differ, falling back to the standard for the rest. Anyone planning to build a login theme "from scratch" should schedule a couple of hours and then still switch to parent inheritance — the number of FTL templates to override is larger than expected.
FreeMarker as the template engine
Templates are written in FreeMarker (.ftl). For those who do not yet know FreeMarker: the syntax sits between JSP and Thymeleaf; ${...} for variables, <#if ...> for conditionals, <#list ...> for loops. The most important file of a login theme is template.ftl, which defines the scaffold (header, footer, logo, CSS includes); the individual pages (login.ftl, register.ftl, login-update-password.ftl) inherit from it.
Theme deployment
In the classical model, the theme directory is placed under themes/ and Keycloak restarts. For GitOps setups, the theme is packaged as a JAR with a META-INF/keycloak-themes.json and copied into the providers/ directory — kc.sh build registers it. That is also the path that works with the Keycloak operator, because the theme becomes a container layer in the image.
Practical note
Keep theme customisations minimal. Every overridden FTL template binds you to a specific Keycloak version — at major upgrades, the underlying template structure can change, and your theme has to catch up. Anyone customising only CSS and a few text strings comes through upgrades almost unscathed. Anyone who has rewritten entire HTML structures will regret it at every major step.
Application integration
Integrating with Keycloak is surprisingly simple on the application side once the right libraries are in place. The harder topics lie in reverse-proxy headers and token validation.
Spring Boot — resource server
A Spring Boot backend that validates tokens needs the OAuth2 resource-server starter and one line of configuration:
The JWKS endpoint is automatically discovered through OIDC discovery (.well-known/openid-configuration). Token signatures are validated against Keycloak's public keys; on key rotation, Spring reloads the new keys automatically. In the Java configuration, roles are then mapped from the token (typically from realm_access.roles or resource_access.<client>.roles).
Front end — single-page application
For modern front ends (React, Vue, Angular), the library oidc-client-ts is the standard. It handles the Authorization Code Flow with PKCE, token refreshing, and logout logic:
Important: PKCE is enabled by default in oidc-client-ts — no client secret in the browser. That is exactly the right default; any attempt to use a client secret from a browser is security-theatre.
Reverse proxy and X-Forwarded headers
One of the most common stumbling blocks: Keycloak behind a reverse proxy "sees" itself at the internal HTTP URL, while browsers see it at the external HTTPS URL. Without a fix, it builds OIDC redirect URLs that the browser rejects. Three configurations have to align:
Keycloak:proxy-headers=xforwarded, hostname=auth.intern.example.com, hostname-strict-https=true in keycloak.conf.
Reverse proxy (Nginx): set proxy_set_header X-Forwarded-Proto https; as well as X-Forwarded-Host and X-Forwarded-For.
Container network: the internal addresses must be in Keycloak's trusted-proxy list, or it will reject the headers.
Once those three are in agreement, the rest is friction-free. When they are not, hours of redirect errors follow with no visible root cause.
Federation, brokering, and FAPI
Beyond the standard use cases, Keycloak offers three extension areas that regularly come into play in regulated industries.
Identity brokering — external IdPs as a login source
Identity brokering means Keycloak delegates the actual authentication to another IdP but takes over identity handling and token issuance under its own rules. Classical examples: login via Google, GitHub, or Microsoft Entra ID for external staff; BundID or ELSTER login for applications in the public sector; Microsoft Entra ID as a federation for M365 accounts. Keycloak is the service provider and translates the external IdP's response into its own tokens — the downstream application sees uniform tokens regardless of where the user originally came from.
User federation — external identity stores
User federation is a different concept: the user database lives in an external system (LDAP or Active Directory), and Keycloak queries it to perform authentication. Three modes: read-only (Keycloak reads, never writes — the LDAP server remains the single source of truth), writable (changes to a user in Keycloak are written back to LDAP), import (users are imported once and managed locally from then on). In most Tenvias projects, read-only is the right choice — AD remains the authoritative identity source, Keycloak only adds the OIDC/SAML view on top.
FAPI — financial-grade API
FAPI 1.0 Advanced and FAPI 2.0 are security profiles from the OpenID consortium that banking APIs under PSD2 and PSD3 must meet. Keycloak supports FAPI through preconfigured client profiles that enforce strict minimum requirements: PAR (Pushed Authorization Requests), DPoP or mTLS for sender-constrained tokens, asymmetric client authentication via private-key JWT instead of a client secret. Anyone offering a banking API cannot avoid FAPI — and Keycloak is one of the few open-source IdPs that covers the requirements completely.
Customising in practice — step by step
Two customisations show up in practically every Keycloak project we work on: reshaping the login page to match a client's branding, and integrating an external user source that is neither LDAP nor Active Directory. Both can be done cleanly once the toolbox is understood. The two walkthroughs below proceed step by step — the first with FreeMarker and CSS, the second with Java code for a custom federation provider.
(a) Reshaping the login page
The bundled Keycloak login mask looks unmistakably like Keycloak. For client projects, that is almost never acceptable — the following seven steps lead to a fully branded variant with its own logo, colours, copy, and optionally an extra notice block inside the form.
Step 1 — create the theme directory. Inside the Keycloak server (or as a Maven module, if the theme is shipped as a JAR), the following structure is laid down:
parent=keycloak means all non-overridden templates and resources are inherited from the bundled Keycloak theme. The styles line lists the CSS files in the desired order — it is important to load the bundled login.css first and the custom custom.css after it, so the custom rules override the defaults.
Step 3 — brand-specific CSS. The file resources/css/custom.css contains the branding adjustments. A typical example:
Step 4 — custom copy and translations. The standard labels (e.g. "Sign In") should be replaced with your own wording. In messages/messages_en.properties:
loginAccountTitle=Sign in to Tenvias
doLogIn=Sign in
usernameOrEmail=Username or email
password=Password
doForgotPassword=Forgot password?
noticeTitle=Login notice
noticeBody=This application is only for authorised staff. \
For questions, contact servicedesk@tenvias.com.
Key names such as loginAccountTitle, doLogIn, and so on are predefined in Keycloak; a full list can be found in the source of the bundled theme. Custom keys (here noticeTitle and noticeBody) can be used in your own FreeMarker templates via ${msg("noticeTitle")}.
Step 5 — selectively overriding a FreeMarker file. When the standard layout is not enough, any .ftl file from the parent theme can be overridden by placing it under the same name in the custom theme. A customised login.ftl that inserts an extra notice block above the login form:
Only the sections that really need overriding have to be written out — everything else lives on in the parent template.
Step 6 — deploy the theme. In a classical setup, the directory is copied to ${KEYCLOAK_HOME}/themes/ and Keycloak is restarted. In a container or Kubernetes setup, the theme is packaged as a JAR — directory structure inside the JAR:
Drop the JAR into the providers/ directory, run kc.sh build, and restart Keycloak.
Step 7 — activate the theme in the realm. In the admin console: Realm Settings → Themes → Login Theme, set to tenvias-login. Alternatively declaratively in the realm JSON export:
The next login call shows the customised mask. If changes are not visible, a browser cache reload almost always helps — or, during theme development, setting spi-theme-cache-themes=false.
(b) Connecting an external source through a federation provider
If neither LDAP nor Active Directory covers an existing user database — typical for legacy applications with their own user table or REST API — you write a custom User Storage Provider in Java. The example below integrates an external REST API as a federation source: Keycloak asks the legacy API at every login, validates the password there, and presents the identity as a "normal" Keycloak user. We walk through the seven required steps.
Step 1 — set up the Maven project. A pom.xml with the Keycloak SPI dependencies declared as provided:
scope=provided is essential: the SPI libraries are shipped by Keycloak itself and must not be bundled into the produced JAR, or class-loader conflicts will appear at runtime.
Step 2 — implement the provider class. The central class implements UserStorageProvider and the interfaces that cover the relevant features. For username lookup and password validation, those are UserLookupProvider and CredentialInputValidator:
package com.tenvias.keycloak;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.user.UserLookupProvider;
public class LegacyRestStorageProvider
implements UserStorageProvider,
UserLookupProvider,
CredentialInputValidator {
private final KeycloakSession session;
private final ComponentModel model;
private final LegacyApiClient apiClient;
public LegacyRestStorageProvider(KeycloakSession session,
ComponentModel model,
LegacyApiClient apiClient) {
this.session = session;
this.model = model;
this.apiClient = apiClient;
}
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
LegacyApiClient.User legacyUser = apiClient.findByUsername(username);
if (legacyUser == null) return null;
return new LegacyUserAdapter(session, realm, model, legacyUser);
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
LegacyApiClient.User legacyUser = apiClient.findByEmail(email);
if (legacyUser == null) return null;
return new LegacyUserAdapter(session, realm, model, legacyUser);
}
@Override
public UserModel getUserById(RealmModel realm, String id) {
String externalId = StorageId.externalId(id);
return getUserByUsername(realm, externalId);
}
@Override
public boolean supportsCredentialType(String credentialType) {
return PasswordCredentialModel.TYPE.equals(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user,
String credentialType) {
return supportsCredentialType(credentialType);
}
@Override
public boolean isValid(RealmModel realm, UserModel user,
CredentialInput input) {
if (!supportsCredentialType(input.getType())) return false;
return apiClient.authenticate(
user.getUsername(),
input.getChallengeResponse());
}
@Override
public void close() {
apiClient.close();
}
}
isValid is the actual security anchor: the password submitted by the browser is forwarded to the legacy API, which returns a true or false. Keycloak never stores the password — each login asks the legacy API again.
Step 3 — adapter class for the user model. Keycloak expects a UserModel. Most fields are managed automatically through the base class AbstractUserAdapterFederatedStorage (profile fields, role assignments, group memberships). We override only username, email, first name, and last name to point at the external source:
package com.tenvias.keycloak;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
public class LegacyUserAdapter extends AbstractUserAdapterFederatedStorage {
private final LegacyApiClient.User legacyUser;
public LegacyUserAdapter(KeycloakSession session,
RealmModel realm,
ComponentModel model,
LegacyApiClient.User legacyUser) {
super(session, realm, model);
this.legacyUser = legacyUser;
this.storageId = new StorageId(model.getId(),
legacyUser.getUsername()).getId();
}
@Override
public String getUsername() { return legacyUser.getUsername(); }
@Override
public String getEmail() { return legacyUser.getEmail(); }
@Override
public String getFirstName() { return legacyUser.getFirstName(); }
@Override
public String getLastName() { return legacyUser.getLastName(); }
@Override
public void setUsername(String username) {
throw new UnsupportedOperationException(
"Read-only federation against legacy API");
}
}
Read-only federation means: profile changes a user makes in Keycloak's account console are not written back to the legacy API. If you want the reverse behaviour, implement the setter methods with real API calls.
Step 4 — ProviderFactory. The factory is Keycloak's entry point. It defines the configuration parameters the administrator sees in the UI and creates provider instances per request:
package com.tenvias.keycloak;
import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.UserStorageProviderFactory;
public class LegacyRestStorageProviderFactory
implements UserStorageProviderFactory<LegacyRestStorageProvider> {
public static final String PROVIDER_ID = "legacy-rest-storage";
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES =
ProviderConfigurationBuilder.create()
.property()
.name("apiUrl")
.label("Legacy API URL")
.helpText("Base URL of the external REST API")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue("https://legacy.intern.example.com/api")
.add()
.property()
.name("apiToken")
.label("API Bearer Token")
.helpText("Token used to authenticate against the legacy API")
.type(ProviderConfigProperty.PASSWORD)
.secret(true)
.add()
.property()
.name("connectTimeoutMs")
.label("Connect Timeout (ms)")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue("3000")
.add()
.build();
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "User federation against an external REST API (Tenvias)";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
@Override
public LegacyRestStorageProvider create(KeycloakSession session,
ComponentModel model) {
String apiUrl = model.get("apiUrl");
String apiToken = model.get("apiToken");
int timeout = Integer.parseInt(
model.get("connectTimeoutMs", "3000"));
LegacyApiClient client = new LegacyApiClient(apiUrl, apiToken, timeout);
return new LegacyRestStorageProvider(session, model, client);
}
}
The ProviderConfigProperty.PASSWORD type ensures the API token is masked in the admin console and handled accordingly in realm JSON exports — in setups with the Vault enabled, only the vault reference ends up in the export, not the plaintext.
Step 5 — SPI registration via the services file. So that Keycloak finds the factory at startup, a META-INF/services/ file with the class name is created:
If you ship multiple factories in one module, list them one per line.
Step 6 — build and deploy. The JAR is built with Maven and copied into the providers/ directory of the Keycloak installation. Keycloak then needs to rebuild so the provider is picked up into the optimised distribution:
# Build the JAR
mvn clean package
# Drop the JAR into the providers directory
cp target/legacy-rest-storage-provider-1.0.0.jar \
/opt/keycloak/providers/
# Rebuild Keycloak (registers the new provider)
/opt/keycloak/bin/kc.sh build
# Start
/opt/keycloak/bin/kc.sh start
In container setups, the standard path is a multi-stage Dockerfile: a Maven stage builds the JAR, a second stage copies it into the official Keycloak image and runs kc.sh build at image build time. The provider then becomes part of the image inventory and ships ready to run.
Step 7 — activate the provider in the realm. In the admin console: Realm → User Federation → Add Provider. The dropdown lists legacy-rest-storage — the configuration parameters defined in the factory (URL, token, timeout) appear as input fields. After saving, the federation is active. The next login with a username that exists in the legacy API runs through the full flow: getUserByUsername → the provider returns a LegacyUserAdapter → isValid validates the password against the legacy API → Keycloak issues a token to the requesting application.
Practical note
The LegacyApiClient we left out of the example should not be implemented naively with the JDK HttpClient and a loop. In production, four properties belong in by default: connection pooling (otherwise Keycloak opens a fresh TCP connection per login), caching on user lookups with a short TTL of 30–60 seconds (eases load on the legacy API noticeably), a circuit breaker via Resilience4j (so a failed legacy API does not stall the entire login chain in Keycloak), and tracing header propagation, so that an incident remains correlatable through the log system (see our article on logging in the enterprise).
Realm lifecycle, backup, and zero-downtime upgrades
Ongoing maintenance is the operational part that makes or breaks an IAM. Three disciplines matter most.
Realm export and import as code
Keycloak can export an entire realm — including clients, roles, groups, authentication flows, theme references, and (optionally) users — into a JSON file. That file is human-readable, suitable for code review, and can be versioned in Git. With this, the realm model becomes configuration code that can be rolled out through CI pipelines:
In Kubernetes setups, the KeycloakRealmImport CRD handles this declaratively. Realm JSONs land in a Git repository, pull requests get reviewed, a CI job rolls out the changed configuration. IAM configuration changes such as "Apprentice X gets access to procedure Y" become auditable Git commits — one of the most important compliance properties of all.
Backup strategy
Backup means three things at the same time for Keycloak. First: a full PostgreSQL backup with pg_dump or pgBackRest — the database is the source of truth for realms, users, and clients; sessions are only transient. Second: regular realm exports of important realms into the Git repository — a second, declarative backup that also survives an accidental realm deletion. Third: a backup of theme JARs and keycloak.conf, ideally as part of the container-image build process, so the image itself is reproducible.
Zero-downtime upgrades
Keycloak supports rolling updates within the same major version without trouble — old and new nodes work together in the same Infinispan cluster. The path for a major upgrade (e.g. from 25 to 26) looks like this:
Create a database backup. Major upgrades run schema migrations that are not backward-compatible.
Read the release notes of the versions in between — there are recurring deprecations and migration hints relevant in live operations.
Replay the upgrade in a staging environment with a copy of the production database. Open realms, run a test login, check theme rendering.
In production, replace nodes one by one. With the Keycloak operator, this happens automatically via the pod update strategy.
In most of our projects, major upgrades are planned twice a year — Keycloak releases four major versions per year, but n-2 versions remain supported. Upgrading more often is only worth it when a concrete feature from a new version is required.
When Keycloak fits — and when a SaaS solution fits better
Keycloak is powerful, established, and widespread in regulated industries — but it is not the right tool for every use case. An honest recommendation at the end.
Fits when …
operations must be self-hosted — KRITIS, BAIT/VAIT, DORA, NIS2, or contractual requirements forbid SaaS outsourcing of identities;
OIDC and SAML are needed in parallel — modern and legacy applications on the same IdP;
multiple realms or tenants have to be separated — staff, customers, B2B partners in one platform;
custom themes demand a dedicated branding and a tailored user flow;
an existing identity source (Active Directory, OpenLDAP, BundID) has to be integrated via federation or brokering;
FAPI-compliant banking APIs or other highly regulated interfaces are to be issued;
realm configuration is to be maintained as code in a Git repository with pull-request reviews.
Less suitable when …
a fully managed SaaS solution without in-house operations is preferred — Auth0, Okta, Microsoft Entra External ID offer markedly less operational depth there;
the setup is exclusively on the Microsoft stack and Entra ID is already the tenant IdP — Keycloak then doubles complexity;
the volume is so small (a few hundred users, a single front end) that a managed service is more economical;
there is no engineering capacity for upgrade discipline, cluster operations, and theme maintenance — Keycloak punishes neglect more than a SaaS tool does.
In most of the regulated industries we work with, Keycloak is the right choice — energy utilities, banks, insurers, the public administration. The combination of standards compliance, self-hosting, and open-source licence beats practically every SaaS alternative in this audience. In the next article of this series we cover the third topic we discuss especially often in client conversations: voice agents architecturally — how to build a productive voice assistant for municipal citizen telephony, with ElevenLabs, VAPI, and the integration with Anthropic and OpenAI.
Keycloak migration or HA rollout?
We review your IAM landscape together with your team — realm design, cluster topology, Postgres sizing, theme strategy, migration from the WildFly to the Quarkus distribution, FAPI requirements, GitOps-ready realm configuration. The result: a concrete action plan, tailored to the size and maturity of your platform.