The Keycloak Upgrade Ledger

Every upgrade we have rehearsed, with the environment stated and the clock running.

2026-08-26 · run 2026-08-26-rich-21-realm-ladder-100k

a rich 21-era realm survives the six-hop ladder to 26.7.1 at 100k

Upgrade
21.1.2 → 26.7.1
Scale
100,006 users (100,000 seed-sql + 5 partialImport + 1 template), 1 realm, 10 clients, 11 client scopes, 8 realm roles, 1 group, 10 auth flows, 9 required actions, 2 IDPs, 6 key providers
Database
PostgreSQL 16, single node, container, stock tuning
Topology
single container
Host
Hetzner CCX33 (fsn1) — 8 vCPU, 30 GB RAM
Condition
routine — no adverse condition applied

Why this run existed

Run E proved the schema migrates across five majors but ran at small scale with a partially-applied seed, so it explicitly could not say whether features survive. This run fixes both gaps: a genuinely rich 21-era realm, at 100k users, walked the full six-hop ladder, with a feature inventory captured before and after.

Environment

FieldValue
Keycloak from → to21.1.2 → 26.7.1, stepped 22.0.5 → 23.0.7 → 24.0.5 → 25.0.6
Distributionquay.io/keycloak/keycloak (official image), start w/ external DB
DatabasePostgreSQL 16, single node, container, stock tuning
Dataset scale100,006 users (100,000 seed-sql + 5 partialImport + 1 template), 1 realm, 10 clients, 11 client scopes, 8 realm roles, 1 group, 10 auth flows, 9 required actions, 2 IDPs, 6 key providers
Seeding methodseed-realm.sh (features) + seed-sql.sh (100k users, direct SQL) — record both
Fixture profilerich 21-era: clients, client-scopes, groups-roles, auth-flows, required-actions, events, themes, localization, lifespans, password-policy, key-providers, idp
Topologysingle container
HostHetzner CCX33 (fsn1) — 8 vCPU, 30 GB RAM
JVM heap-Xms1g -Xmx4g

Result — the ladder

HopElapsedOutcomeIndex audit
21.1.2 → 22.0.521sREADY82/82
22.0.5 → 23.0.720sREADY82/82
23.0.7 → 24.0.521sREADY86/86
24.0.5 → 25.0.615sREADY86/86
25.0.6 → 26.7.117sREADY118/118

~94s wall-clock for the whole ladder, every hop clean. (Run E's six-hop ladder at small scale was 106s; the 100k run is no slower, consistent with the index-skip threshold keeping the migration flat.)

Result — feature survival

A diffable inventory (bin/feature-inventory.sh) captured at 21.1.2 and 26.7.1. Every custom artifact survived unchanged:

The only differences are Keycloak's own built-ins changing across the majors, none of which touched the customer's artifacts:

ChangeDirectionNotes
client scopes AuthnContextClassRef, basicaddednew built-ins
role/client-scope service_accountaddednew built-in
required actions delete_credential, idp_linkaddednew built-ins
component declarative-user-profileaddednew in 24+
built-in auth flow http challengeremovedsee below

Breakage observed

The built-in http challenge flow is gone by 26.7.1

Verification

ClaimPrimary sourceChecked
Six-hop ladder reaches READY at 100kper-hop upgrade.sh outcome, elapsed
Index audit clean at every hop82/82 → 86/86 → 118/118
Every custom feature survivesfeature-inventory.sh diff, 21.1.2 vs 26.7.1
User count unchanged (100,006)/users/count before and after
Credentials functional post-ladderpassword-grant login HTTP 200 for both seed paths
Custom flows and IDPs still resolvableAPI reads at 26.7.1
The exact hop that removed http challengenarrowed — 22.0.0 (the 21→22 hop). See 2026-08-26-http-challenge-flow-removal-narrowed
A custom flow that references http challengeansweredMigrateTo22_0_0 leaves it in place and logs an ERROR ("is in use … cannot be removed"); not silent data loss
Realm export/import equivalence (vs in-place migration)not compared — in-place ladder only

Publishable extract

We built a Keycloak realm on 21.1.2 with the full surface a real install has — custom themes, ten clients, custom client scopes, groups, composite roles, two custom authentication flows, required actions, an OIDC and a SAML broker, and six key providers — then seeded it to 100,000 users and walked it up the six-hop ladder to 26.7.1, one major at a time. Every hop reached ready; the whole ladder took about 94 seconds; the index audit was clean at every step; and every custom artifact, all 100,006 users, and both credential paths survived unchanged. The only things that changed were Keycloak's own built-ins — new default scopes and required actions, and one built-in flow (http challenge) that Keycloak removed. If a custom flow references that built-in, check it; ours did not, and nothing else was lost.

← All runs