Vom PEM zum laufenden Stack – Teil 3: Drei Passwörter, zwei Verzeichnisse, ein verwechseltes Zertifikat
Teil 3 der Serie. Die Zertifikate waren technisch in Ordnung. Trotzdem startete nichts. Dieser Teil handelt von den Fehlern, die nicht in der Krypto liegen, sondern in der Konfiguration – und die am schwersten zu finden sind, weil alles „eigentlich richtig" aussieht.
Wenn der Fehler immer gleich bleibt
Nachdem die Keystore-Datei endlich im richtigen Format und mit einem echten Eintrag vorlag, startete ich den Dienst neu – und bekam exakt denselben Fehler wie vorher:
ERROR: keystore password was incorrect javax.crypto.BadPaddingException: Given final block not properly padded
Hier liegt eine der wichtigsten Lektionen des ganzen Projekts: Wenn sich nach einer Änderung nichts ändert, änderst du wahrscheinlich am falschen Ort. Statt weiter an der Datei zu drehen, muss man einen Schritt zurücktreten und prüfen, ob der Dienst überhaupt die Datei liest, die man bearbeitet – und mit dem Passwort, das man annimmt.
Die Detektivarbeit: Welches Passwort erwartet der Dienst wirklich?
Der Stack bezog seine Konfiguration aus mehreren env-Dateien, die einander überschreiben (Standardwerte in *-default.env, eigene Anpassungen in *-custom.env). Solche Schichtsysteme sind praktisch, aber sie verschleiern, welcher Wert am Ende tatsächlich gilt.
Der entscheidende Schritt war, nicht zu raten, sondern den Dienst direkt zu fragen, was bei ihm ankommt:
docker compose run --rm --entrypoint sh keycloak -c \ 'printf "PW=[%s]\n" "$KC_SPI_TRUSTSTORE_FILE_PASSWORD"'
Die eckigen Klammern sind Absicht: Sie machen sichtbar, ob am Ende ein unsichtbares Leerzeichen oder ein Windows-Zeilenumbruch (\r) im Wert steckt – etwas wie [changeit ] oder [wert\r] ist sonst nicht zu erkennen.
Das Ergebnis war ein Augenöffner: Der Dienst erwartete ein anderes Passwort als das, mit dem ich die Datei gebaut hatte. Es gab nämlich mehrere Passwort-Variablen im Spiel, für verschiedene Zwecke – und ich hatte die ganze Zeit die falsche verwendet.
Dieser Stack hatte tatsächlich drei verschiedene Passwort-Einstellungen, die alle ähnlich klangen, aber unterschiedliche Dinge steuerten:
- eine für den System-Truststore des Auth-Servers,
- eine für den HTTPS-Server-Keystore (das Zertifikat, mit dem der Dienst selbst TLS anbietet),
- eine für eine dritte, dienstspezifische Variante.
Hinzu kam ein weiterer Dienst (eine REST-API-Middleware), der sein Truststore-Passwort fest im Startskript verdrahtet hatte und gar nicht über env-Dateien konfigurierbar war.
BEWEIS: Teilen sich Dienste eine Datei, müssen sie sich auf ein Passwort einigen – eine Datei kann nur eines haben.
Die Lösung war keine technische, sondern eine organisatorische Entscheidung: alle auf denselben Wert vereinheitlichen. Da der eine Dienst sein Passwort hartkodiert hatte und sich nicht ändern ließ, habe ich alle übrigen Variablen auf genau diesen Wert gesetzt:
KC_HTTPS_KEY_STORE_PASSWORD=<TRUSTSTORE_PW> KC_HTTPS_TRUST_STORE_PASSWORD=<TRUSTSTORE_PW> KC_SPI_TRUSTSTORE_FILE_PASSWORD=<TRUSTSTORE_PW>
– und natürlich alle Keystore-Dateien mit ebendiesem Passwort neu gebaut.
URTEIL: Bei Passwort-Mismatches ist die Frage nie nur „stimmt das Passwort der Datei?", sondern „stimmt es mit dem überein, was dieser konkrete Dienst erwartet?". Und: Wenn mehrere Dienste sich eine Datei teilen, müssen sie sich auf ein Passwort einigen.
Ein Tipp am Rande: env-Dateien und Zeilenenden
Weil der Wert aus einer env-Datei kam, die vielleicht unter Windows entstand, lohnt sich nach jeder Bearbeitung:
sed -i 's/\r$//' service-custom.env
Ein \r am Ende eines Passwort-Wertes ist unsichtbar, aber es macht aus changeit ein changeit\r – und damit ein anderes Passwort.
Nachdem die Passwörter saßen, lief der Auth-Server. Aber die Middleware-Komponente warf bei der TLS-Verbindung zum Auth-Server weiterhin einen Vertrauensfehler – obwohl ich das passende CA-Zertifikat importiert hatte.
Wieder galt: Wenn sich trotz Reparatur nichts ändert, liest der Dienst vermutlich eine andere Datei. Also nachsehen, was der Container tatsächlich mountet:
docker inspect <container> \
--format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
BEWEIS: Der Auth-Server bezog seine Zertifikate aus einem Verzeichnis (/host/ssl/), die Middleware aber aus einem völlig anderen (/host/installation/certs/). Ich hatte die ganze Zeit die richtige Datei am falschen Ort repariert.
URTEIL: „Wo liegt die Datei, die der Container wirklich liest?" ist eine Frage, die man früh und explizit beantworten sollte – nicht annehmen, dass alle Dienste denselben Pfad teilen. docker inspect (oder docker compose config) zeigt die Wahrheit.
Die hinterhältigste Verwechslung des ganzen Projekts: Mein Truststore enthielt einen Eintrag mit dem Alias corp-ca und einem gültigen trustedCertEntry. Sah perfekt aus. Trotzdem schlug die Vertrauensprüfung fehl.
Der Grund: Der Eintrag hieß zwar corp-ca, aber sein Inhalt war das Server-Endzertifikat (das des ERP-Hosts), nicht das eigentliche CA-Wurzelzertifikat. Ich hatte früh eine Datei falsch zugeordnet und seitdem konsequent das falsche Zertifikat unter dem richtigen Namen mitgeschleppt.
Aufgedeckt hat das ein Fingerprint-Vergleich. Man kann sich nicht auf den Alias verlassen – nur der Fingerabdruck sagt die Wahrheit:
BEWEIS: Ein Eintrag im Keystore beweist gar nichts über seinen Inhalt. Nur der Fingerprint ist eine verlässliche Identität.
# What's in the truststore?
keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW>
# -> corp-ca ... Fingerprint (SHA-256): 79:75:09:...
# What does the server actually serve? Look at the chain:
echo | openssl s_client -connect keycloak.example.local:5443 -showcerts 2>/dev/null \
| awk '/BEGIN CERT/{c++} {print > "cert"c".pem"}'
for f in cert*.pem; do
openssl x509 -in "$f" -noout -subject -issuer -fingerprint -sha256
done
Im echten CA-Wurzelzertifikat sind Subject und Issuer identisch (es ist selbstsigniert). Genau dieses gehört in den Truststore – und sein Fingerprint war ein ganz anderer als der, den ich drinhatte. Erst der Import des richtigen Zertifikats (verifiziert über den übereinstimmenden Fingerprint) löste das Vertrauensproblem.
URTEIL: Verifiziere Zertifikate über ihren Fingerprint, nicht über ihren Namen. Der Name lügt, der Fingerprint nicht. Und für eine Vertrauenskette brauchst du das CA-Wurzelzertifikat (Subject = Issuer), nicht das Endzertifikat.
Was mich Teil 3 gelehrt hat
- Ändert sich der Fehler nach einer Änderung nicht, war die Änderung am falschen Ort. Tritt einen Schritt zurück.
- Frag den Dienst, welcher Wert bei ihm ankommt (
printf "[%s]"mit Klammern), statt env-Schichten im Kopf aufzulösen. - Mehrere ähnliche Passwort-Variablen sind eine klassische Falle – kläre, welche der Dienst wirklich liest.
- Teilen sich Dienste eine Datei, brauchen sie dasselbe Passwort. Vereinheitlichen ist oft pragmatischer als jonglieren.
docker inspectverrät, welche Datei ein Container tatsächlich mountet – verlass dich nicht auf Annahmen über Pfade.- Verifiziere Zertifikate per Fingerprint, nie per Alias. Der Name lügt, der Fingerabdruck nicht.
- Truststore = CA-Wurzelzertifikat (Subject = Issuer), nicht das Endzertifikat.
Im Finale geht es um die letzte Hürde, die nichts mehr mit kaputten Dateien zu tun hat, sondern mit der Vertrauensarchitektur selbst: Was tun, wenn ein Dienst gleichzeitig einer internen und einer öffentlichen Zertifizierungsstelle vertrauen muss?