Run D: LDAPS works across three majors; what breaks is trust, and it says "SocketReset"
- Database
- PostgreSQL 16, single node, container
- Host
- Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe
- Condition
- Real LDAP federation — 's untested row: *LDAPS / StartTLS*
Summary
S9 ran federation over plaintext and said TLS is where real deployments break. Two candidate reasons were on the table. One is refuted and one is confirmed, and the confirmed one is confirmed for a reason nobody would guess from the error message.
Refuted: the truststore option rename does not break federation across 23 → 24. A deployment configured the pre-24 way keeps working on 24.0.5 and on 26.7.1, three majors later, with only deprecation warnings.
Confirmed: an untrusted certificate breaks it completely — and Keycloak
reports it as SocketReset, with no mention of a certificate anywhere in the
response. The server starts perfectly happily; nothing surfaces until something
touches LDAP.
Environment
| Field | Value |
|---|---|
| Directory | bitnamilegacy/openldap:2.6, TLS on 1636, plaintext 1389 still up |
| Certificates | private CA, server cert CN=ldap, SAN DNS:ldap, DNS:localhost, IP:127.0.0.1, generated by bin/make-ldap-certs.sh |
| Federation config | seed-realm.sh --ldap --ldap-host ldaps://ldap:1636, editMode=READ_ONLY, importEnabled=true |
| Trust (24+) | KC_TRUSTSTORE_PATHS=/opt/keycloak/conf/truststores, a bind-mounted directory |
| Trust (pre-24) | KC_SPI_TRUSTSTORE_FILE_FILE + _PASSWORD, a PKCS12 built with keytool |
| Database | PostgreSQL 16, single node, container |
| Host | Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe |
| JVM heap | -Xms1g -Xmx4g |
| Lab change | docker-compose.ldaps.yml, docker-compose.ldaps-legacy.yml, bin/make-ldap-certs.sh |
Breakage observed
A certificate trust failure is reported as SocketReset
- Symptom: federation returns nothing; sync fails with
{"errorMessage":"SocketReset"}and HTTP 400. Server healthy throughout. - Evidence: D1 against D2 — same server, same config, the only difference being one CA certificate in the truststore directory.
- Cause: confirmed by the differential.
- Fix: put the directory's CA in the truststore —
KC_TRUSTSTORE_PATHSon 24+,KC_SPI_TRUSTSTORE_FILE_FILEbefore that. - Would this hit a customer? Every customer using LDAPS with an internal CA, which is most of them. The failure is undiagnosable from its message.
A malformed truststore fails the same way as no truststore
- Symptom: trust configured, path correct, file present, and LDAPS still fails exactly as if nothing had been configured.
- Evidence: found by making the mistake.
openssl pkcs12 -export -nokeyswrites the CA as a plain certBag; Java requires atrustedCertEntry. The store loads, the trust anchor set is empty, and the only trace isPKIXParameters.setTrustAnchorsin a stack trace. - Fix: build truststores with
keytool -importcert, and verify withkeytool -listthat the entry type istrustedCertEntry.bin/make-ldap-certs.shnow does both. - Would this hit a customer? Yes, and it is a plausible pre-flight finding — "your truststore is empty" is not visible from the file existing.
Verification
| Claim | Primary source | Checked |
|---|---|---|
| LDAPS federation works with the CA trusted | D2 — 3 users imported, HTTP 200, lookup returns alice | ☑ |
Without trust it fails as SocketReset, HTTP 400 | D1, response quoted | ☑ |
| Keycloak starts and passes readiness either way | both legs reached ready before the sync was attempted | ☑ |
| No certificate wording in the response or default log | grepped for PKIX, handshake, truststore, certification — nothing in D1 | ☑ |
| TLS federation survives 26.0.0 → 26.7.1 | D3 — READY, 3 users still linked | ☑ |
| Pre-24 truststore config still works on 24.0.5 and 26.7.1 | D5/D6/D7 — syncs return 200, 3 users at every step | ☑ |
| Keycloak warns that the pre-24 options are deprecated | both warnings quoted, from 24.0.5 and 26.7.1 | ☑ |
| A certBag-only PKCS12 behaves as an empty truststore | reproduced, then fixed; keytool -list confirms trustedCertEntry after | ☑ |
| StartTLS on 1389 | tested — works trusted, fails silently (HTTP 200, 0 imported) — 2026-08-26-starttls-1389 | ☑ |
| Hostname-verification failures (cert whose SAN does not match) | tested — hard failure, SSLPeerUnverifiedException, silent at the API — 2026-08-26-starttls-1389 | ☑ |
| Expired or revoked directory certificates | expired: tested (silent, CertificateExpiredException); revoked: not tested — no CRL in the lab CA | ☑ (expired) / ☐ (revoked) |
| Behaviour when the truststore path exists but is empty on 24+ | not tested separately — D1's empty directory is that case, but it was not distinguished from "no truststore configured" | ☐ |
editMode=WRITABLE / UNSYNCED over TLS | not tested — still READ_ONLY, as S9 left it | ☐ |