The Keycloak Upgrade Ledger

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

2026-08-25 · run 2026-08-25-s9-ldap-federation

S9: LDAP federation survives the upgrade, and the fixture that claimed to test it did not exist

Upgrade
26.0.0 → 26.7.1
Scale
1,002 local users + 3 LDAP-federated, 2 realms
Database
PostgreSQL 16.15, single node, container
Topology
single container + ldap sidecar
Host
Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe
Condition
Real LDAP federation

Summary

Two results, and one of them is about this lab rather than about Keycloak.

The Keycloak result: LDAP user federation crosses 26.0.0 → 26.7.1 intact. Component config preserved verbatim, 3 federated users still linked, a fresh full sync still works afterwards, zero LDAP-related errors in the log, and a federated user gets a token from a password validated against LDAP.

The lab result: before this run, seed-realm.sh --ldap pointed at ldap://ldap:389, and no compose file in this repository defined a host called ldap. The kitchen-sink profile therefore created a federation component aimed at nothing, and every record produced with it carried an unticked Federation/brokering intact box that could never have been ticked. That is now fixed, and the fix took four separate corrections — none of which were the port number this scenario predicted.

Bonus finding, which resolves an open question from S7: the token endpoint's status for invalid_grant changed from 401 on 26.0.0 to 400 on 26.7.1.

Environment

FieldValue
Keycloak from → to26.0.0 → 26.7.1
Distribution / start modequay.io/keycloak/keycloak official image, start w/ external DB
DatabasePostgreSQL 16.15, single node, container
Postgres tuningstock — shared_buffers 128MB, maintenance_work_mem 64MB, statement_timeout 0
DirectoryOpenLDAP 2.6 (bitnamilegacy/openldap:2.6), dc=lab,dc=invalid, 3 users under ou=people
Adverse scenarioS9
Dataset scale1,002 local users + 3 LDAP-federated, 2 realms
Seeding methodpartialImport, plus a full LDAP sync
Federation configREAD_ONLY, importEnabled=true, usernameLDAPAttribute=uid, uuidLDAPAttribute=entryUUID, searchScope=2 — byte-for-byte what seed-realm.sh --ldap writes
Topologysingle container + ldap sidecar
HostHetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe
JVM heap-Xms1g -Xmx4g

Results

Check26.0.0 (pre)26.7.1 (post)
LDAP components in realm11
connectionUrl storedldap://ldap:1389ldap://ldap:1389
editMode storedREAD_ONLYREAD_ONLY
Users with federation_link33
alice presentyesyes
Full sync3 imported, 0 updated0 imported, 3 updated
LDAP errors in log00

Upgrade elapsed: 17s. Nothing about federation slowed or blocked it.

Proving the LDAP bind is actually evaluated

A federation component that is merely present proves nothing — that was the whole problem with the old fixture. So each side was probed with a wrong password and a correct one:

Probe26.0.026.7.1
alice / wrong password401 Invalid user credentials400 Invalid user credentials
alice / correct password400 Account is not fully set up200 TOKEN ISSUED

The two answers differ, which is the point: Keycloak is genuinely binding to LDAP and evaluating the result. Account is not fully set up is raised after successful credential validation — the realm's typical profile makes CONFIGURE_TOTP a default required action, so every freshly imported user carries one.

The post-upgrade TOKEN ISSUED is the first time this lab has verified a federated login end to end. It also exposes a detail worth keeping: clearing user_required_action by direct SQL did not take effect until the server restarted, because Keycloak caches the user. The pre-upgrade probe still said "not fully set up" after the delete; the post-upgrade one issued a token. Do not edit user state under a running Keycloak and expect it to be seen.

The invalid_grant status changed: 401 → 400

Same request, same realm, same wrong password. 26.0.0 answers 401. 26.7.1 answers 400. The body is Invalid user credentials in both.

2026-08-25-s7-custom-spi recorded this shift and could only rule out one cause (a broken event listener). This run isolates it properly: it appears on a clean realm with no custom providers loaded, and it tracks the version. RFC 6749 specifies 400 for invalid_grant, so the newer behaviour is the correct one — but any client keying on 401 to detect a bad password breaks on upgrade, and that code is common. The exact release between 26.0.0 and 26.7.1 that changed it was not identified.

Outcome

Verification

ClaimPrimary sourceChecked
Component config survives the upgradecomponent / component_config read from Postgres, before and after
Federated users survivefederation_link is not null count = 3 on both sides
Sync still works after the upgradeadmin API returns 0 imported users, 3 updated users
The LDAP bind is genuinely evaluatedwrong vs. correct password give different answers on both versions
A federated user can obtain a token on 26.7.1http=200, access_token present
invalid_grant status changed 401 → 400two independent runs (here and S7), clean realm, tracks the version
Which release changed it26.6.0 — see 2026-08-26-invalid-grant-401-to-400-narrowed
A federated user can obtain a token on 26.0.0not shown — blocked by a cached required action, not by federation
editMode=WRITABLE / UNSYNCEDnot tested — only READ_ONLY
LDAPS / StartTLSnot tested — plaintext 1389 only, and TLS is where real deployments break
Active Directory specificallynot tested — vendor: other, and AD differs in objectGUID, paging and referrals
Federation at scalenot tested — 3 users; sync cost against 100k LDAP entries is unknown
Kerberos / SPNEGOnot tested

Publishable extract

We upgraded a Keycloak with LDAP user federation from 26.0.0 to 26.7.1 and checked, specifically, that the federation still worked afterwards — rather than that it still appeared to be configured.

It did. Seventeen seconds, component configuration preserved value for value, all three federated users still linked, a fresh full sync returning 0 imported users, 3 updated users, zero LDAP errors in the log, and a federated user obtaining a token with a password validated against the directory.

We checked it the annoying way, because the easy way proves nothing. A federation component that merely exists after an upgrade tells you the row survived, not that anyone can log in. So we probed with a wrong password and a right one and confirmed Keycloak answered differently — which is the only way to know the bind is being evaluated at all.

That distinction is not academic, and here is our own embarrassment as evidence. Until this run, our lab's LDAP fixture pointed at a hostname that no compose file in the repository defined. Every previous run created a federation component aimed at nothing and reported it as configured. When we finally built the thing it was supposed to point at, we found four independent breakages, not one — a missing service, a container image that had been moved to a different namespace and no longer resolved at all, a deprecated environment variable that killed the server with exit code 68 and no error message, and users with no mail attribute so every import produced a null email.

None of those would have failed loudly. An inert fixture never does. If your upgrade test plan contains a step that has never actually run, it is not a test — and it is worth more than the step it replaced, because it reads like coverage.

One incidental finding for anyone writing client code: a bad password returned HTTP 401 on 26.0.0 and HTTP 400 on 26.7.1, with the same Invalid user credentials body. 400 is what RFC 6749 requires, so the change is a fix — but if your client checks for 401 to detect a bad password, it stops working on upgrade, and nothing in the release notes will look like it is about you.

← All runs