LabHub

Blog

Keycloak 26 Architecture Deep Dive — Understanding Realms, Clients, and the Quarkus Runtime

한국어English日本語

Introduction

As of 2026, Keycloak holds a de facto standard position in the open-source IAM (Identity and Access Management) space. Since becoming a CNCF incubating project, its release cadence has stabilized, and the latest version as of May 2026 is 26.6.2. Amid 2026 trends such as passkeys becoming the default, AI agent identity (non-human identity), and Zero Trust architecture, Keycloak is evolving rapidly with final support for the FAPI 2.0 Security Profile, the JWT Authorization Grant, and experimental OAuth Client ID Metadata Document (CIMD) support.

However, operating Keycloak reliably in production — beyond just "spinning it up and using it" — requires an understanding of its internal architecture. This article covers the following.

A History of Keycloak — From WildFly to Quarkus

Keycloak began as a Red Hat project in 2014 and was initially a traditional Java EE application running on top of the JBoss/WildFly application server. The WildFly-based distribution had the following limitations.

The Quarkus transition, which started as the Keycloak.X project in 2021, became the default distribution in Keycloak 17 (2022), and the WildFly distribution was removed entirely after version 19. The core of the Quarkus-based transition is build-time optimization.

AspectWildFly distributionQuarkus distribution
ConfigurationEditing standalone.xmlconf file, environment variables, CLI options
Startup time20+ secondsWithin a few seconds
Memory usageHighComparatively low
Extension deploymentEAR/WAR modulesJARs in the providers directory
Container friendlinessLowVery high (optimized official images)
Applying config changesRestartRe-run the build phase, then start

Quarkus pushes as much work as possible — CDI wiring, classpath scanning, configuration parsing — to build time. Consequently, Keycloak cleanly separates "build options" from "runtime options," and that separation is the reason the kc.sh build phase, covered later, exists at all.

The Domain Model — Realm, Client, User, Role, Group

Every concept in Keycloak can be understood as a tree rooted at the realm.

+--------------------------------------------------------------+
|  Keycloak Server                                             |
|                                                              |
|  +--------------------+      +--------------------+          |
|  |  Realm: master     |      |  Realm: production |          |
|  |  (admin use only)  |      |                    |          |
|  +--------------------+      |  +--------------+  |          |
|                              |  | Clients      |  |          |
|                              |  |  - web-app   |  |          |
|                              |  |  - api-gw    |  |          |
|                              |  |  - cli-tool  |  |          |
|                              |  +--------------+  |          |
|                              |  +--------------+  |          |
|                              |  | Users        |  |          |
|                              |  |  - alice     |--+-- Groups |
|                              |  |  - bob       |  |   Roles  |
|                              |  +--------------+  |          |
|                              |  +--------------+  |          |
|                              |  | Identity     |  |          |
|                              |  | Providers    |  |          |
|                              |  +--------------+  |          |
|                              +--------------------+          |
+--------------------------------------------------------------+

Realm

A realm is an isolation unit that contains users, clients, roles, groups, authentication flows, and token policies. Think of it as a tenant boundary. From an operational standpoint, the important principles are:

Client

A client is an application or service that delegates authentication to Keycloak. It corresponds to a Relying Party in OIDC terminology and a Service Provider in SAML terminology. Web apps, SPAs, mobile apps, backend APIs, and CLI tools are all registered as clients.

User, Role, Group

The criteria for choosing between roles and groups can be summarized as follows.

CriterionRoleGroup
MeaningExpression of a permissionExpression of an organization/user set
HierarchyCan be emulated with compositesNatively supported
Token representationrealm_access, resource_access claimsgroups claim (requires a protocol mapper)
Recommended patternThe unit your application checksThe operational unit for bulk role assignment

Confidential vs Public Clients

The OAuth 2.0 client type distinction is expressed in Keycloak configuration via the Client authentication toggle.

AspectConfidential ClientPublic Client
Secret storagePossible (server side)Impossible (browser/mobile)
ExamplesSpring Boot backend, API gatewaySPA, native mobile app
Client authenticationclient secret, private_key_jwt, mTLSNone
PKCERecommendedMandatory (per OAuth 2.1)
Token endpoint accessWith the secretWith the code_verifier

The OAuth 2.1 draft mandates PKCE for all clients and removes the Implicit and ROPC grants. In 2026, enabling PKCE (S256) for every new client regardless of type is standard practice. In Keycloak, you can enforce S256 via the Proof Key for Code Exchange Code Challenge Method setting under the client Advanced tab.

Authentication methods for confidential clients are also evolving. Rather than a plain client secret, private_key_jwt (signature-based) or mTLS is recommended, and the Federated client authentication feature added in 26.6 lets clients authenticate with JWTs issued by workload identity systems such as SPIFFE/SVID — removing the secret entirely.

Client Scopes — Assembly Units for Token Contents

A client scope bundles "which claims and roles go into the token" into a reusable unit. To be precise, it is a bundle of protocol mappers (claim generators) and role scope mappings (role filters).

For example, when an SPA sends an authorization request like this:

GET /realms/production/protocol/openid-connect/auth?
    client_id=web-app
    &response_type=code
    &scope=openid profile email offline_access
    &redirect_uri=https://app.example.com/callback
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256

openid, profile, and email are included automatically as default scopes, while offline_access is an optional scope that must be requested explicitly for the refresh token to be issued as an offline token. To prevent token bloat, minimize the default scopes and manage the aud claim explicitly with an audience mapper. The issued access token payload looks roughly like this.

{
  "exp": 1765540800,
  "iat": 1765540500,
  "iss": "https://sso.example.com/realms/production",
  "aud": ["api-gateway"],
  "sub": "f3a2c1d0-1234-5678-9abc-def012345678",
  "typ": "Bearer",
  "azp": "web-app",
  "scope": "openid profile email",
  "realm_access": { "roles": ["user"] },
  "resource_access": {
    "api-gateway": { "roles": ["orders:read"] }
  },
  "preferred_username": "alice"
}

The Authentication Flow Engine — Inside the Browser Flow

Authentication in Keycloak operates as a tree-structured execution engine called an authentication flow. The browser flow is the most representative, executed when a user arrives at the authorization endpoint.

Browser Flow (default structure)
|
+-- Cookie                          [ALTERNATIVE]
|     (pass immediately if an SSO session cookie exists)
|
+-- Identity Provider Redirector    [ALTERNATIVE]
|     (redirect to an external IdP if kc_idp_hint is present)
|
+-- Forms                           [ALTERNATIVE]
      |
      +-- Username Password Form    [REQUIRED]
      |
      +-- Browser - Conditional 2FA [CONDITIONAL]
            |
            +-- Condition - User Configured  [REQUIRED]
            +-- OTP Form                     [ALTERNATIVE]
            +-- WebAuthn Authenticator       [ALTERNATIVE]

Each node is called an execution and consists of three elements.

  1. Authenticator — an SPI implementation that performs the actual authentication logic (cookie check, password form, OTP validation)
  2. Requirement — one of REQUIRED, ALTERNATIVE, CONDITIONAL, or DISABLED
  3. Sub-flow — a group of executions (with its own requirement)

The evaluation rules are simple. At a given level, if any ALTERNATIVE succeeds, that level passes; every REQUIRED must succeed for the level to pass. A CONDITIONAL sub-flow behaves like REQUIRED only when its inner condition executions evaluate to true. With just these rules, the structure above produces the behavior "skip the form if an SSO cookie exists; otherwise ask for a password, and only users with OTP configured get 2FA."

In 26.x, passkeys are integrated into the login form (conditional/modal UI): via WebAuthn conditional UI, users can pick a passkey from the browser autofill without typing a username, supported by the default theme. For details, see the WebAuthn Level 3 specification and the FIDO Alliance passkeys documentation.

Once authentication completes, a user session is created, with per-client client sessions hanging beneath it. These sessions are stored in the Infinispan caches (persisted to the DB by default since 26), and an SSO cookie (KEYCLOAK_IDENTITY) is issued to the browser.

Database Schema Overview

Keycloak stores all configuration and users in a relational database via JPA (Hibernate). An overview of the core table groups:

REALM                     -- realm definitions and policies (many columns incl. token lifetimes)
REALM_ATTRIBUTE           -- extended realm attributes (key-value)
CLIENT                    -- client definitions
CLIENT_ATTRIBUTES         -- extended client attributes (PKCE enforcement, etc.)
CLIENT_SCOPE              -- client scope definitions
PROTOCOL_MAPPER           -- claim mapper configuration
USER_ENTITY               -- user records (username, email, ...)
USER_ATTRIBUTE            -- custom user attributes
CREDENTIAL                -- hashed passwords, OTP, passkey metadata
USER_ROLE_MAPPING         -- user-role mappings
KEYCLOAK_GROUP            -- groups (hierarchy via the PARENT_GROUP column)
GROUP_ROLE_MAPPING        -- group-role mappings
KEYCLOAK_ROLE             -- realm/client role definitions
FEDERATED_IDENTITY        -- external IdP account links
EVENT_ENTITY              -- login events (when enabled)
ADMIN_EVENT_ENTITY        -- admin event audit log
OFFLINE_USER_SESSION      -- offline/persistent sessions
OFFLINE_CLIENT_SESSION    -- persistent per-client sessions
MIGRATION_MODEL           -- schema version tracking (Liquibase)

Useful operational points to keep in mind:

kc.sh — The Two-Phase Build and Start Structure

The most distinctive trait of Quarkus-based Keycloak is that configuration splits into build options and runtime options.

# Phase 1: build (run once when producing an immutable image)
bin/kc.sh build \
  --db=postgres \
  --health-enabled=true \
  --metrics-enabled=true \
  --features=token-exchange

# Phase 2: start (runtime options only; --optimized skips the rebuild)
bin/kc.sh start --optimized \
  --db-url=jdbc:postgresql://db.internal:5432/keycloak \
  --db-username=keycloak \
  --db-password=changeme \
  --hostname=sso.example.com \
  --proxy-headers=xforwarded \
  --http-enabled=true

When building container images, the standard practice is to perform the build phase ahead of time in the Dockerfile to reduce startup time.

FROM quay.io/keycloak/keycloak:26.6 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.6
COPY --from=builder /opt/keycloak/ /opt/keycloak/
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start", "--optimized"]

The start-dev mode is for development only. Its cache is local, HTTP is open, and hostname validation is relaxed — never use it in production.

Configuration Methods — Environment Variables, conf Files, CLI

The same option can be specified in three ways, with CLI arguments taking the highest precedence, then environment variables, then the conf file.

# Method 1: CLI arguments
bin/kc.sh start --db-url-host=db.internal --log-level=info

# Method 2: environment variables (option name uppercased with underscores, KC_ prefix)
export KC_DB_URL_HOST=db.internal
export KC_LOG_LEVEL=info

# Method 3: the conf/keycloak.conf file
# db-url-host=db.internal
# log-level=info

In Kubernetes environments, the environment variable approach combines naturally with ConfigMaps/Secrets and is therefore the most widely used. Mounting secret values as files and referencing them via file references in option values is also supported. The full option list is available via per-subcommand help in kc.sh and in the official server configuration guide.

New Features in Keycloak 26.x

The 26.x series — especially the 26.6.0 release — ships several headline features.

Workflows (realm management automation)

A feature for declaratively automating lifecycle tasks inside the server, such as disabling long-inactive accounts or revoking privileges after a set period. Work that previously required external batch jobs calling the Admin API can now be internalized as realm configuration.

Zero-downtime rolling patch

Starting with 26.6, cache compatibility is guaranteed between patch versions (such as 26.6.0 to 26.6.1), so Kubernetes rolling updates can replace pods sequentially without restarting the whole cluster. With the Operator, setting the update strategy to Auto performs the rolling update automatically after a compatibility check.

Federated client authentication

Clients authenticate with JWTs issued by an external trust authority (for example, SPIFFE workload identity) instead of a client secret. This direction fundamentally eliminates secret distribution and rotation problems.

JWT Authorization Grant

Based on RFC 7523, the grant that exchanges an externally issued JWT assertion for an access token is now officially supported. Token brokering becomes much simpler for machine-to-machine integration.

And more

Production Checklist

Finally, a checklist to run through before going to production.

[ ] No application users in the master realm
[ ] Admin console access restricted to a separate hostname or network
    (--hostname-admin option, firewall/Ingress separation)
[ ] HTTPS termination and --proxy-headers settings are consistent
[ ] Production-grade DB (PostgreSQL recommended) with sized connection pool
[ ] Token lifetimes: 5-15 min access tokens, SSO idle/max policies defined
[ ] Refresh token rotation or revoke-refresh-token enabled
[ ] PKCE S256 enforced for all new clients
[ ] Wildcard redirect URIs banned (register exact URIs only)
[ ] Brute force detection enabled
[ ] Event logging + retention + external SIEM integration configured
[ ] Health/metrics endpoints enabled and wired into monitoring
[ ] Backups: periodic DB backups + realm exports
[ ] Upgrade path: tracking breaking changes in release notes
[ ] Unused features (e.g., the temporary admin account) cleaned up via feature flags

For token policy in particular, we recommend anchoring on the OAuth 2.0 Security BCP (RFC 9700). The essentials are sender-constrained tokens (DPoP/mTLS), short access token lifetimes, and refresh token rotation.

Troubleshooting — Common Problems and Causes

1. Invalid redirect_uri error

The most common initial configuration error. It occurs when the redirect URI registered on the client does not exactly match the redirect_uri in the authorization request.

Cause checklist:
- Protocol mismatch (http vs https)
- Trailing slash differences
- Missing port (https://app.example.com vs https://app.example.com:443)
- Relying on a wildcard (*) that was removed in production

The fix is simple: register the callback URL exactly (ideally without wildcards) in the client Valid redirect URIs setting.

2. Infinite redirects or wrong issuer behind a reverse proxy

If the proxy terminates TLS and Keycloak does not know about it, the iss claim in issued tokens and the redirect URLs are generated with internal addresses.

# If the proxy sets X-Forwarded-* headers
bin/kc.sh start --optimized \
  --hostname=sso.example.com \
  --proxy-headers=xforwarded \
  --http-enabled=true

# Verify: confirm the issuer in the OIDC discovery document is the external address
curl -s https://sso.example.com/realms/production/.well-known/openid-configuration

Check that the issuer and authorization_endpoint in the discovery document both use the external hostname.

3. A build option changed at runtime is not taking effect

Build options like the database vendor or features are ignored — or trigger an automatic rebuild — when specified at start time. If you use the --optimized flag they are ignored; otherwise startup slows down. Build option changes must be handled by rebuilding the image.

4. All sessions logged out after an upgrade

Before 26, in-memory sessions were the default, so a full restart evaporated sessions. Since 26, persistent user sessions are the default and sessions survive in the DB, but a full restart may still be required between minor versions due to cache protocol incompatibility. Always check the upgrade section of the release notes.

5. Memory usage keeps growing

The common causes are these three.

1. Unbounded growth of EVENT_ENTITY / ADMIN_EVENT_ENTITY
   -> configure event expiration, offload to an external SIEM
2. Excessive offline sessions (offline_access granted to every client)
   -> restrict the offline_access scope to clients that truly need it
3. Unset JVM heap letting caches grow to the container limit
   -> check JAVA_OPTS_KC_HEAP or container-memory-based auto sizing

Anti-Pattern Collection

Conclusion

Keycloak 26 has become a completely different piece of software from the "heavy SSO server of the WildFly era." The Quarkus-based build/runtime separation, declarative configuration, Kubernetes-friendly operational model, and standards support spanning passkeys and AI agent identity make Keycloak a strong central component for the identity-first Zero Trust architecture of 2026.

If you have absorbed the domain model and runtime structure covered here, the next steps are high-availability clustering and SPI extension development. We will cover each in depth in separate articles.

References

Comments

No comments yet.

Sign in to leave a comment