Du willst einer Docker-Applikation ein eigenes CA-Zertifikat beibringen — zum Beispiel weil du eine interne CA betreibst und der Container intern gehostete Dienste per TLS erreichen soll.
Der Standard-Ansatz: Zertifikat ins Container mounten, update-ca-certificates beim Start ausführen. Klingt gut. Funktioniert — solange der Container als root läuft.
Tut er aber nicht immer.
Das Problem
Du configurierst ein Docker Compose Override mit Entrypoint-Wrapper:
services:
app:
volumes:
- ./certs/internal-ca.crt:/usr/local/share/ca-certificates/internal-ca.crt:ro
entrypoint: ["/bin/bash", "-c", "update-ca-certificates && exec /docker-entrypoint.sh"]
Container startet. Crasht sofort. Restart-Loop. Logs:
Updating certificates in /etc/ssl/certs...
/usr/sbin/update-ca-certificates: 101: cannot create /etc/ssl/certs/ca-certificates.crt.new: Permission denied
update-ca-certificates muss in /etc/ssl/certs/ schreiben — das geht nur als root. Der Container läuft als normaler User.
Bei Anwendungen wie Nextcloud (läuft als www-data, Entrypoint startet aber initial als root und wechselt dann) funktioniert der Wrapper. Bei Anwendungen die von Anfang an als Non-Root-User starten — zum Beispiel Zammad (zammad-User) — schlägt es fehl.
Die Diagnose
# Welchen User nutzt der Container?
docker exec srv-r2d2 id
# uid=1000(zammad) gid=1000(zammad)
# Warum läuft update-ca-certificates nicht?
docker exec srv-r2d2 ls -la /etc/ssl/certs/ca-certificates.crt
# -r--r--r-- 1 root root ... (nur lesbar, nicht schreibbar für andere)
Der Container-User kann die Cert-Datei lesen, aber nicht ersetzen. update-ca-certificates muss eine neue Datei schreiben.
Die Lösung: Pre-Built Combined CA Bundle
Statt update-ca-certificates im Container auszuführen, erstellen wir das fertige Bundle außerhalb und mounten es direkt.
Schritt 1: Bundle aus dem laufenden Container extrahieren
# Standard-CA-Bundle aus dem Container holen (während er noch läuft)
docker exec srv-r2d2 cat /etc/ssl/certs/ca-certificates.crt \
> /opt/srv-r2d2/certs/combined-ca-bundle.crt
# Eigene CA anhängen
cat /opt/srv-r2d2/certs/internal-ca.crt >> /opt/srv-r2d2/certs/combined-ca-bundle.crt
# Verifizieren: Letzte Zeilen prüfen
tail -3 /opt/srv-r2d2/certs/combined-ca-bundle.crt
# Erwartung: -----END CERTIFICATE----- (deine CA)
Schritt 2: docker-compose.override.yml
services:
app:
volumes:
- ./certs/combined-ca-bundle.crt:/etc/ssl/certs/ca-certificates.crt:ro
Das war’s. Kein entrypoint-Override. Kein Build-Step. Kein root.
Der Container-User kann das Bundle lesen (world-readable), nicht schreiben. Genau das wollen wir.
Schritt 3: Verifizieren
# Container neu starten
docker compose -f docker-compose.yml -f docker-compose.override.yml \
up -d app
# SSL-Verbindung zum internen Dienst testen
docker exec srv-r2d2 \
sh -c 'openssl s_client -connect mail-chewbacca.darkside.local:993 -quiet < /dev/null 2>&1 | head -4'
Erwartete Ausgabe:
depth=1 CN = Internal Root CA
verify return:1
depth=0 CN = mail-chewbacca.darkside.local
verify return:1
verify return:1 = CA bekannt, Zertifikat gültig. ✅
Warum funktioniert das Mounten?
Das Mount überschreibt /etc/ssl/certs/ca-certificates.crt im Container mit deiner Datei. Der Inhalt ist schreibgeschützt (:ro-Flag), aber der Prozess liest ihn nur — das geht als beliebiger User.
OpenSSL und die meisten TLS-Bibliotheken lesen das System-CA-Bundle beim Verbindungsaufbau. Das Bundle ist jetzt dein kombiniertes Bundle.
Falle: Bundle wird outdated
Das Bundle enthält die CAs zum Zeitpunkt der Erstellung. Wenn der Container-Image ein Update kriegt und neue Root-CAs eingebaut werden, hast du die im Bundle nicht.
Fix: Bundle regelmäßig neu erstellen oder nach Image-Updates refreshen:
# Image updaten
docker compose pull app
# Neues Bundle erstellen (Container kurz ohne Override starten)
docker run --rm <IMAGE> cat /etc/ssl/certs/ca-certificates.crt \
> /opt/srv-r2d2/certs/combined-ca-bundle.crt
cat /opt/srv-r2d2/certs/internal-ca.crt >> /opt/srv-r2d2/certs/combined-ca-bundle.crt
# Dann wieder mit Override starten
docker compose -f docker-compose.yml -f docker-compose.override.yml up -d app
Wann welchen Ansatz?
| Situation | Ansatz |
|---|---|
| Container startet initial als root | Entrypoint-Override mit update-ca-certificates |
| Container startet als Non-Root | Combined CA Bundle mounten (dieser Artikel) |
| Eigenes Dockerfile erlaubt | RUN update-ca-certificates im Build-Step |
| Keine Kontrolle über Image | Combined CA Bundle ist die einzige Option |
Bonus: Zweite Falle mit docker compose exec
Wenn du docker compose exec app rails runner '...' ausführst und die App eine DATABASE_URL braucht — diese Variable wird im Entrypoint via export gesetzt, gilt aber nicht für exec-Subprozesse. export vererbt sich nur an Kindprozesse des Entrypoints, nicht an nachträglich gestartete exec-Prozesse.
Fix: Explizit übergeben:
docker compose exec -T \
-e DATABASE_URL='postgres://user:pass@db:5432/myapp' \
app rails runner 'puts "Hello"'
Fazit
Non-Root-Container sind gut für die Sicherheit. Aber sie brechen manche Annahmen. update-ca-certificates ist eine davon.
Das Combined CA Bundle ist kein Hack — es ist konzeptuell sauberer als ein Entrypoint-Wrapper: Du gibst dem Container genau das Bundle, das er braucht, unveränderlich und nachvollziehbar.
Und das Beste: Es funktioniert mit jedem Image, unabhängig davon wie der Entrypoint aussieht.