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
| Field | Value |
|---|---|
| Keycloak from → to | 26.0.0 → 26.7.1 |
| Distribution / start mode | quay.io/keycloak/keycloak official image, start w/ external DB |
| Database | PostgreSQL 16.15, single node, container |
| Postgres tuning | stock — shared_buffers 128MB, maintenance_work_mem 64MB, statement_timeout 0 |
| Directory | OpenLDAP 2.6 (bitnamilegacy/openldap:2.6), dc=lab,dc=invalid, 3 users under ou=people |
| Adverse scenario | S9 |
| Dataset scale | 1,002 local users + 3 LDAP-federated, 2 realms |
| Seeding method | partialImport, plus a full LDAP sync |
| Federation config | READ_ONLY, importEnabled=true, usernameLDAPAttribute=uid, uuidLDAPAttribute=entryUUID, searchScope=2 — byte-for-byte what seed-realm.sh --ldap writes |
| Topology | single container + ldap sidecar |
| Host | Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe |
| JVM heap | -Xms1g -Xmx4g |
Results
| Check | 26.0.0 (pre) | 26.7.1 (post) |
|---|---|---|
| LDAP components in realm | 1 | 1 |
connectionUrl stored | ldap://ldap:1389 | ldap://ldap:1389 |
editMode stored | READ_ONLY | READ_ONLY |
Users with federation_link | 3 | 3 |
alice present | yes | yes |
| Full sync | 3 imported, 0 updated | 0 imported, 3 updated |
| LDAP errors in log | 0 | 0 |
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:
| Probe | 26.0.0 | 26.7.1 |
|---|---|---|
alice / wrong password | 401 Invalid user credentials | 400 Invalid user credentials |
alice / correct password | 400 Account is not fully set up | 200 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
- Reached ready
- Realm config intact — component and all config values preserved
- Login flow works — federated login, token issued post-upgrade
- Federation/brokering intact — first time this box has been truthfully ticked
Verification
| Claim | Primary source | Checked |
|---|---|---|
| Component config survives the upgrade | component / component_config read from Postgres, before and after | ☑ |
| Federated users survive | federation_link is not null count = 3 on both sides | ☑ |
| Sync still works after the upgrade | admin API returns 0 imported users, 3 updated users | ☑ |
| The LDAP bind is genuinely evaluated | wrong vs. correct password give different answers on both versions | ☑ |
| A federated user can obtain a token on 26.7.1 | http=200, access_token present | ☑ |
invalid_grant status changed 401 → 400 | two independent runs (here and S7), clean realm, tracks the version | ☑ |
| Which release changed it | 26.6.0 — see 2026-08-26-invalid-grant-401-to-400-narrowed | ☑ |
| A federated user can obtain a token on 26.0.0 | not shown — blocked by a cached required action, not by federation | ☐ |
editMode=WRITABLE / UNSYNCED | not tested — only READ_ONLY | ☐ |
| LDAPS / StartTLS | not tested — plaintext 1389 only, and TLS is where real deployments break | ☐ |
| Active Directory specifically | not tested — vendor: other, and AD differs in objectGUID, paging and referrals | ☐ |
| Federation at scale | not tested — 3 users; sync cost against 100k LDAP entries is unknown | ☐ |
| Kerberos / SPNEGO | not 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
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 credentialsbody. 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.