TLS abgelaufen — warum mein Monitoring schlief und die Erneuerung still blieb

Red Panda Mechanic

Incident

Dienstag, 14:47 Uhr — die Produktion war ploetzlich weg. Nicht langsam, nicht mit Warnung, sondern sofort: Alle Browser zeigten ERR_CERT_DATE_INVALID, die API-Clients schlugen mit TLS-Handshake-Fehlern fehl, und das Monitoring war gruen. Gruen, obwohl die Seite seit zwölf Minuten nicht mehr erreichbar war. Fuer weitere 18 Minuten lag der Dienst komplett brach, bis ein Kollege zufaellig im Browser nachsah und den Fehler bemerkte.

Root Cause

Was passiert war, liess sich schnell rekonstruieren: Das Let’s-Encrypt-Zertifikat war um 14:35 Uhr abgelaufen. Certbot hatte die Erneuerung zwar sauber durchgefuehrt, aber Nginx wurde nicht neu geladen. Der Webserver servierte also weiterhin das alte, abgelaufene Zertifikat aus seinem Speicher.

Warum fiel das nicht auf? Drei Dinge kamen zusammen:

  1. Monitoring pruefte nur HTTP, nicht HTTPS. Die Uptime-Checks liefen auf Port 80 und sahen einen 200 OK. Die TLS-Schicht wurde nie getestet.
  2. Der Certbot-Cronjob hatte den korrekten Exit-Code, aber keinen Post-Hook. certbot renew lief durch, ohne Fehler, aber Nginx wusste nichts von dem neuen Zertifikat.
  3. Nginx cached das alte Zertifikat. Selbst wenn die Datei auf der Platte neu ist, bleibt das in den Worker-Prozessen geladene Zertifikat gueltig, bis ein nginx -s reload erfolgt.

Die Kombination aus silent failure und einem fehlenden HTTPS-Check machte den Ausfall moeglich.

Loesung

Der Fix besteht aus drei minimalen Aenderungen: einem korrekten Certbot-Hook, einem HTTPS-Uptime-Check und einer kurzen Alert-Regel fuer ablaufende Zertifikate.

1. Certbot mit Post-Hook statt nur Renew

# /etc/cron.d/certbot-renew
# Fuehre die Erneuerung durch und lade Nginx danach neu.
# --quiet unterdrückt normale Ausgabe, --no-self-upgrades verhindert unbeabsichtigte Aenderungen.
0 3 * * * root certbot renew --quiet --post-hook "nginx -s reload"

Warum --post-hook und nicht nur --renew-hook? --renew-hook wird nur fuer tatsaechlich erneuerte Zertifikate ausgefuehrt. Wenn Certbot entscheidet, dass nichts zu tun ist, wird der Hook uebersprungen — genau dann, wenn das Zertifikat noch gueltig ist, aber die Erneuerung kurz bevorsteht. --post-hook laeuft immer, egal ob erneuert wurde oder nicht. Er ist also sicherer fuer einen zuverlaessigen Reload.

2. HTTPS-Uptime-Check hinzufuegen

# Minimaler Curl-Check fuer das TLS-Zertifikat.
# -s: silent, -o /dev/null: verwerfe Body, -w '%{http_code}': gib nur den Statuscode aus.
# --max-time 5: Timeout nach fuenf Sekunden, damit der Check nicht haengt.
curl -s -o /dev/null -w '%{http_code}' --max-time 5 https://example.com/healthz

Erwartete Ausgabe: 200

Ein einfaches Skript, das alle fuenf Minuten laeuft, reicht. Fuer eine robustere Loesung kann man zusätzlich openssl s_client nutzen, um das Ablaufdatum direkt zu pruefen:

# Pruefe das Ablaufdatum des gelieferten Zertifikats.
# | grep "notAfter" | cut -d= -f2-: extrahiere das Datum.
# date -d ... +%s: wandle es in einen Unix-Timestamp um.
openssl s_client -servername example.com -connect example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -enddate \
  | cut -d= -f2-

Erwartete Ausgabe: Jun 15 23:59:59 2026 GMT (oder ein Datum in der Zukunft).

3. Alert-Schwelle fuer ablaufende Zertifikate

# Skript: check-cert-expiry.sh
# Warnung, wenn das Zertifikat in weniger als 14 Tagen ablaeuft.
HOST="example.com"
PORT=443
DAYS=14

EXPIRY=$(echo | openssl s_client -servername "$HOST" -connect "$HOST:$PORT" 2>/dev/null \
  | openssl x509 -noout -enddate 2>/dev/null \
  | cut -d= -f2)

if [ -z "$EXPIRY" ]; then
  echo "CRITICAL: Kein Zertifikat fuer $HOST gefunden."
  exit 2
fi

EXP_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DIFF_DAYS=$(( (EXP_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DIFF_DAYS" -lt "$DAYS" ]; then
  echo "WARNING: Zertifikat fuer $HOST laeuft in $DIFF_DAYS Tagen ab ($EXPIRY)."
  exit 1
fi

echo "OK: Zertifikat fuer $HOST laeuft noch $DIFF_DAYS Tage ($EXPIRY)."
exit 0

Erwartete Ausgabe bei gueltigem Zertifikat: OK: Zertifikat fuer example.com laeuft noch 372 Tage (Jun 15 23:59:59 2027 GMT).

Verifikation

  1. Reload pruefen: Nach einer Aenderung der Certbot-Konfiguration einen manuellen Lauf starten und den Exit-Code sowie den Reload pruefen.

    certbot renew --dry-run && echo "Renew-Dry-Run: OK"

    Erwartete Ausgabe: Congratulations, all renewals succeeded.

  2. Nginx-Status pruefen:

    nginx -t && echo "Nginx-Config: OK"

    Erwartete Ausgabe: nginx: configuration file /etc/nginx/nginx.conf test is successful

  3. Uptime-Check testen: Den Curl-Check von Hand ausfuehren und den Statuscode notieren.

    curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://example.com/healthz

    Erwartete Ausgabe: 200

  4. Zertifikatsablauf pruefen: Das Alert-Skript mit einer kurzen Frist testen, um die Logik zu verifizieren.

    bash check-cert-expiry.sh

    Erwartete Ausgabe: OK: ... laeuft noch ... Tage ...

Fazit

Der Ausfall dauerte 30 Minuten — nicht weil das Zertifikat ploetzlich ungueltig wurde, sondern weil der Reload fehlte und das Monitoring die TLS-Schicht nicht pruefte. Die Lehre ist einfach: Teste den gesamten Pfad, nicht nur den Teil, der einfach zu testen ist. HTTPS-Uptime-Checks und ein zuverlaessiger Post-Hook kosten fast nichts, aber sie verhindern, dass ein abgelaufenes Zertifikat zur Stoerung wird.

Metrik: Vor der Aenderung: 1 Ausfall in 90 Tagen, Dauer 30 Minuten, keine Warnung. Nach der Aenderung: Null Ausfaelle in 90 Tagen, beide Checks gruen.

Offene Frage: Wie prueft ihr ablaufende Zertifikate? Nutzt ihr ein externes Monitoring wie UptimeRobot, oder prueft ihr direkt ueber openssl auf den Servern?

Siehe auch: