← Zur Uebersicht

LDAPS: TLS zwischen Liberty und dem LDAP-Verzeichnis

Status: verifiziert. Vollständige Kette real getestet: unauthentifiziert → 401, korrekter Nutzer über LDAPS → 200 mit korrektem Rollen-Ergebnis, falsches Passwort → 401 — alles über eine echte TLS-verschlüsselte Verbindung zum LDAP-Container, nicht mehr über Klartext-LDAP wie in den früheren Ausbaustufen.

Warum das nachträglich ergänzt wurde

02-ldap-registry.md beschreibt die ursprüngliche Konfiguration gegen Klartext-LDAP (Port 1389) — funktional korrekt, aber eine echte Lücke beim Anzeigen-Punkt "Security Lifecycle: ... Zertifikate, SSL": Zugangsdaten (Bind-Passwort, später auch die Nutzer-Credentials beim Login) liefen unverschlüsselt über die Leitung. Dieses Dokument schließt die Lücke — und zeigt dabei zwei reale, in dieser Session tatsächlich aufgetretene Stolpersteine, die für die Anzeigen-Punkte "Troubleshooting" und "Zertifikate" beide lehrreich sind.

Schritt 1: TLS im Container aktivieren — Stolperstein 1 (abgelaufene Default-CA)

infra/docker-compose.yml setzt LDAP_TLS=true — das osixia/openldap-Image erzeugt daraufhin beim Start automatisch ein selbstsigniertes Zertifikat. Realer Befund: die vom Image mitgelieferte Standard-CA (docker-light-baseimage) war zum Zeitpunkt dieser Session bereits abgelaufen (notAfter=Jan 15 2026, geprüft mit openssl s_client -connect localhost:1636). Ein Zertifikat, dessen Aussteller-Kette ein abgelaufenes Glied enthält, ist kein verlässlicher Vertrauensanker — Java/Liberty hätte das bei strikter Kettenprüfung zu Recht abgelehnt.

Konsequenz für dieses Projekt: statt der Image-Standard-CA zu vertrauen, erzeugt scripts/ldap-tls-cert-generate.sh ein eigenes selbstsigniertes Zertifikat (openssl, 365 Tage gültig), das über LDAP_TLS_CRT_FILENAME/ LDAP_TLS_KEY_FILENAME/LDAP_TLS_CA_CRT_FILENAME in den Container eingespielt wird — volle Kontrolle über Gültigkeit und Inhalt, statt einer stillen Abhängigkeit von einem Drittanbieter-Image-Default.

Schritt 2: Liberty gegen LDAPS konfigurieren — Stolperstein 2 (Hostname-Verifikation)

Erster Versuch: ldapRegistry auf Port 1636 mit sslEnabled="true" umgestellt, Zertifikat des Containers reingezogen und in einen Liberty-Truststore importiert. Ergebnis: 401 trotz korrekter Credentials. Log-Auszug (echt, aus messages.log):

CWIML4520E: ... javax.net.ssl.SSLHandshakeException: (certificate_unknown)
No subject alternative DNS name matching localhost found.

Diagnose: das vom Image automatisch erzeugte Zertifikat trug als Subject Alternative Name nur den zufälligen Container-Hostnamen (z. B. 553f56c5256c) — nicht localhost, worüber Liberty sich verbindet. Das Zertifikat war korrekt vertrauenswürdig (im Truststore), aber die Hostname-Prüfung (SAN muss zum verwendeten Hostnamen passen) schlug fehl — ein Zertifikat kann vertrauenswürdig UND trotzdem für die falsche Adresse ausgestellt sein, zwei unabhängige Prüfungen.

Die falsche Antwort wäre gewesen, die Hostname-Verifikation abzuschalten. Die richtige Antwort: ein Zertifikat erzeugen, dessen SAN tatsächlich zur verwendeten Adresse passt — siehe scripts/ldap-tls-cert-generate.sh (subjectAltName=DNS:localhost,DNS:concordia-openldap).

Schritt 3: der Liberty-seitige Stolperstein (keyStoreRef ist Pflicht)

Ein dritter, kleinerer realer Fund beim ersten Anlauf:

CWWKG0058E: Im Element ssl mit der eindeutigen ID ldapSSLConfig fehlt das
erforderliche Attribut keyStoreRef.

Liberty verlangt bei <ssl> immer ein keyStoreRef, auch für eine reine Outbound-Verbindung ohne eigenes Client-Zertifikat. Lösung: den bereits vorhandenen, automatisch erzeugten HTTPS-Default-Keystore (defaultKeyStore, siehe 03-zertifikate-ssl.md) mitreferenzieren, statt einen weiteren eigens dafür anzulegen — er wird für diese Verbindung inhaltlich nicht gebraucht, das Attribut ist trotzdem Pflicht.

Finale, verifizierte Konfiguration

<ldapRegistry id="concordiaLdap"
              realm="ConcordiaBankLDAP"
              host="localhost"
              port="1636"
              sslEnabled="true"
              sslRef="ldapSSLConfig"
              ...>
    ...
</ldapRegistry>

<ssl id="ldapSSLConfig" keyStoreRef="defaultKeyStore" trustStoreRef="ldapTrustStore"/>
<keyStore id="ldapTrustStore"
          location="resources/security/ldap-truststore.p12"
          type="PKCS12"
          password="ConcordiaTrust123!"/>

Reproduzierbarer Ablauf (drei Skripte, in dieser Reihenfolge)

bash scripts/ldap-tls-cert-generate.sh   # 1) eigenes Zertifikat mit korrektem SAN erzeugen
cd infra && python -m podman_compose down -v && python -m podman_compose up -d   # 2) Container damit neu starten
cd .. && bash scripts/ldap-tls-trust-setup.sh   # 3) Liberty-Truststore daraus bauen
cd concordia-portal && CONCORDIA_LDAP_BIND_PASSWORD="..." mvn liberty:run

Warum drei separate Skripte statt eines eingecheckten Zertifikats: das Zertifikat enthält einen privaten Schlüssel (gehört nicht ins Repo) und müsste bei jedem podman-compose down -v ohnehin neu zum Container passen — Vertrauens-Bootstrapping ist hier bewusst als wiederholbarer, lokaler Ablauf modelliert, nicht als statische Datei (siehe Kommentar in ldap-tls-trust-setup.sh).

Verifikationsnachweis (echte curl-Ergebnisse)

$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:19080/concordia-portal/secure/whoami
401

$ curl -s -u "m.bauer:AdminDemo123!" http://localhost:19080/concordia-portal/secure/whoami
{"principal":"m.bauer","isPortalAdmin":true,"isPortalUser":false}

$ curl -s -o /dev/null -w "%{http_code}\n" -u "m.bauer:falsch123" http://localhost:19080/concordia-portal/secure/whoami
401

Alle drei Fälle liefen jetzt über den durch LDAPS verschlüsselten Kanal — dieselben Testfälle wie in 01-rollen-und-rechte.md, nur mit verschlüsselter statt unverschlüsselter LDAP-Verbindung.

⌂ Cockpit