Abgelaufene digitale Zertifikate, IPv6-Adressen und eine falsche Weiterleitungsregel, technische Illustration
Homelab 6 Min Read

Eine Ziffer, und 26 Zertifikate waren tot

Von zweiunddreißig Zertifikaten auf meinem Docker-Host waren sechsundzwanzig abgelaufen. Nicht alle auf einmal, das wäre aufgefallen. Sie sind über Monate nacheinander umgefallen, eines nach dem anderen, seit Ende April. Gemerkt…

Audio-Guide KI-generierter Beitrag zum Thema

Von zweiunddreißig Zertifikaten auf meinem Docker-Host waren sechsundzwanzig abgelaufen. Nicht alle auf einmal, das wäre aufgefallen. Sie sind über Monate nacheinander umgefallen, eines nach dem anderen, seit Ende April.

Gemerkt habe ich es an einem Freitag, als Mealie nicht mehr laden wollte. Derselbe Abend, an dem ich auch den Cryptominer gefunden habe — aber das ist eine andere Geschichte.

Die Ursache war ein einziger falscher Wert in einer Weiterleitungsregel. Eine Ziffer.

Warum sechs Monate niemand etwas sagt

Der unangenehmste Teil zuerst. Ein abgelaufenes Zertifikat ist kein Ausfall. Der Dienst läuft weiter, der Server antwortet, die Anwendung funktioniert. Es steht nur eine Warnung davor, die man wegklickt.

Und die Dienste, um die es geht, benutze im Wesentlichen ich. Wenn ich seit Wochen nicht in einem Werkzeug war, merkt niemand, dass es seit Wochen eine Warnung zeigt. Es gab keine Überwachung der Restlaufzeiten. Nichts, das mir sagt: dieses Zertifikat hat noch zehn Tage und erneuert sich seit April nicht mehr.

Sechsundzwanzig Dienste haben also monatelang leise gesagt, dass etwas nicht stimmt. Nur in einer Sprache, die nur hört, wer gerade davorsteht.

Ein Wort in der Fehlermeldung

Als ich die Erneuerung von Hand angestoßen habe, kam das hier von Let’s Encrypt zurück:

Identifier: mail.marco-stankowitz.de
Detail: 2a01:4f8:c2c:f35c:...:e3ac: Fetching
        http://mail.marco-stankowitz.de/.well-known/acme-challenge/...
        Timeout after connect

Zwei Dinge stehen da, und beide sind wichtiger, als sie aussehen.

Erstens die Adresse. Das ist eine IPv6-Adresse. Let’s Encrypt hat also gar nicht erst über IPv4 angeklopft.

Zweitens, und das ist der eigentliche Schlüssel: „Timeout after connect”. Nicht „during”. Der Unterschied ist die halbe Diagnose:

  • during connect heißt: Die TCP-Verbindung kam nie zustande. Da blockt eine Firewall, oder es lauscht niemand.
  • after connect heißt: Die Verbindung kam zustande, jemand hat sie angenommen — und dann kam nie eine Antwort.

Ich hatte monatelang eine Firewall im Verdacht. Die Fehlermeldung sagte von Anfang an, dass es keine Firewall sein kann. Jemand nahm die Verbindung ja an. Er hatte nur nichts zu sagen.

Die Gegenprobe, die alles klärte

Sechs Zertifikate waren gültig. Die Frage war also nicht nur, warum sechsundzwanzig kaputt sind, sondern warum ausgerechnet diese sechs nicht.

Die Antwort stand im DNS: Es sind genau die Domains ohne AAAA-Record.

Kein AAAA, kein IPv6-Weg, also validiert Let’s Encrypt über IPv4 — und über IPv4 funktionierte alles einwandfrei. Jede Domain mit AAAA-Record war tot, jede ohne lebte. Kein Zufall mehr, sondern ein Muster mit zweiunddreißig Datenpunkten.

Das ist der Moment, in dem aus einer Vermutung ein Befund wird. Und es ist der Grund, warum ich inzwischen bei jedem Problem zuerst nach der Gegenprobe suche: Nicht „was ist bei den Kaputten gleich”, sondern „was ist bei den Funktionierenden anders”.

Die Ziffer

Meine Topologie: Nur die OPNsense hat öffentliche Adressen, IPv4 und IPv6. Alles andere hängt dahinter in einem privaten Netz. Die Weiterleitung nach innen macht kein NAT, sondern HAProxy.

Und dort stand für das IPv6-Frontend auf Port 80 als Ziel:

10.100.0.3:88

Auf Port 88 lauscht nichts. Der Nginx Proxy Manager hört auf 80.

Der Grund ist so banal wie ärgerlich: Bei der Ersteinrichtung lief NPM tatsächlich auf Port 88, weil 80 damals noch belegt war. Später ist er auf 80 umgezogen. HAProxy wurde nie nachgezogen — und weil über IPv4 ein anderer Pfad greift, hat es im Alltag exakt null Auswirkungen gehabt.

Bis auf eine: Jede einzelne ACME-Challenge über IPv6 lief ins Leere. HAProxy nahm die Verbindung an, reichte sie an einen Port weiter, an dem niemand war, und schwieg dann. Genau das, was Let’s Encrypt gemeldet hat. Seit April. Wörtlich.

Die Korrektur war ein Wert:

Server web88:  10.100.0.3:88  →  10.100.0.3:80

Danach am selben Zertifikat, das eben noch scheiterte:

Congratulations, all simulated renewals succeeded

Der Teil, den ich nicht auf dem Zettel hatte

Warum sind auch Zertifikate umgefallen, deren Erneuerung eigentlich noch hätte klappen können?

Weil Let’s Encrypt irgendwann aufhört, es zu versuchen. Wer über Monate durchgehend fehlschlägt, dessen Account wird pausiert. Danach scheitert jede Anfrage, unabhängig davon, ob die Domain gerade erreichbar wäre oder nicht.

Aus einem kaputten Weg wurde damit ein kaputter Zugang. Das ist die Art Verstärkung, mit der man nicht rechnet: Das ursprüngliche Problem betraf nur IPv6, die Folge betraf alles.

Zwei Irrwege, die ich unterwegs hatte

Ich schreibe die auf, weil beide vollkommen plausibel wirkten.

Irrweg eins: „Die AAAA-Einträge zeigen auf eine tote Maschine.” Die IPv6-Adresse endet auf ...:fe1d:e3ac, und da eine solche Adresse aus der MAC-Adresse gebildet wird, lässt sie sich zurückrechnen. Die so errechnete MAC passte nicht zu meinem Docker-Host. Also zeigt der DNS-Eintrag auf ein Gerät, das es nicht mehr gibt, dachte ich. Falsch: Die Adresse gehört zum WAN-Anschluss der OPNsense. Der Docker-Host steht dahinter und hat nach außen gar keine eigene Adresse. Die Rechnung stimmte, der Schluss war Unsinn.

Irrweg zwei: „Dann schotte ich halt den Port ab.” Hätte ich das Binding eines Dienstes auf die lokale Adresse verengt, hätte ich damit den Nginx Proxy Manager ausgesperrt, der über die interne Adresse zugreift. Ein Sicherheitsgewinn, der einen funktionierenden Dienst zerlegt hätte.

Beides kam aus derselben Sorte Fehler: zu früh festgelegt und danach nur noch nach Bestätigung gesucht. Die Lehre steht inzwischen an meiner Wand: Bei Netzen mit vorgelagerter Firewall nie von der MAC-Adresse auf die Maschine schließen. Und bevor du ein Binding verengst, sieh nach, wer tatsächlich darauf zugreift.

Was danach anders ist

Zwei Dinge, die den Fehler künftig unwahrscheinlicher machen.

Die Wildcard läuft über DNS-01. Statt für jede Subdomain eine Datei abrufen zu lassen, legt mein Client einen Eintrag im DNS ab, den Let’s Encrypt abfragt. Damit spielt es überhaupt keine Rolle mehr, ob ein Frontend auf Port 80 oder Port 88 zeigt — es wird gar nichts mehr angeklopft. Ein Zertifikat für alles, ein Weg, der von der Erreichbarkeit des Webservers unabhängig ist.

Vierundzwanzig verwaiste Zertifikats-Verzeichnisse sind weg. Altlasten aus Jahren, für Domains, die es nicht mehr gibt. Bevor ich sie angefasst habe, habe ich jedes einzelne gegen Bind-Mounts, nginx-Konfigurationen und Skripte geprüft — weil ich am selben Abend gelernt hatte, wie teuer die Annahme „das braucht keiner mehr” werden kann. Verschoben, nicht gelöscht. Übrig geblieben sind zehn, alle nachweislich in Benutzung.

Am Ende standen zwanzig von zwanzig Diensten mit gültigem TLS.

Die Lehre

Dual-Stack ist nicht ein Netz mit zwei Adressen. Es sind zwei Netze, die so tun, als wären sie eins. Du kannst jede Regel doppelt bauen, doppelt pflegen und doppelt vergessen — und weil praktisch jeder Test, den man beiläufig macht, über IPv4 läuft, fällt die kaputte Hälfte jahrelang nicht auf. Wer testet schon mit curl -6.

Und die zweite, unbequemere: Die Fehlermeldung hat mir vom ersten Tag an gesagt, was los ist. Sie stand in einem Log, das ich nicht gelesen habe, in einem Wort, dessen Bedeutung ich nicht kannte. „after” statt „during”.

Lies die Fehlermeldung wörtlich. Nicht sinngemäß.