Let's Encrypt Zertifikatskette (Generation Y) in OPNsense

Aus Xinux Wiki
Zur Navigation springen Zur Suche springen

Let's Encrypt Zertifikatskette (Generation Y) in OPNsense

Ausgangslage

Ein Wildcard-Zertifikat von Let's Encrypt (*.it2XX.xinmen.de) soll in eine OPNsense importiert werden. Dabei treten zwei Effekte auf:

  • OPNsense lehnt den Import der fullchain.pem ab: Only a single certificate is allowed (found 7 sections).
  • Nach dem Import funktioniert die Web-GUI in manchen Browsern, in anderen nicht (z.B. Firefox auf Debian ja, Firefox auf Rocky nein).

Beides hat dieselbe Ursache: Let's Encrypt hat die Zertifikatshierarchie umgestellt, und die Kette ist dadurch deutlich länger geworden.

Wie ein Client eine Zertifikatskette prüft

Der Server schickt im TLS-Handshake sein eigenes Zertifikat (Leaf) und die Zwischenzertifikate (Intermediates). Der Client hat einen Truststore, in dem nur Root-Zertifikate liegen.

Der Client baut vom Leaf aus einen Pfad nach oben:

  • Wer hat das Leaf ausgestellt? Ist dieses Zertifikat dabei?
  • Wer hat dieses Zertifikat ausgestellt? Ist das wiederum dabei?
  • ... bis er auf ein Zertifikat trifft, das in seinem Truststore liegt. Dort hört er auf.

Fehlt ein Glied, ist die Kette gebrochen und die Verbindung wird abgelehnt.

Wichtig: Zwischenzertifikate liegen in keinem Truststore. Der Server muss sie mitschicken.

Was sich bei Let's Encrypt geändert hat

Zeitraum Kette in fullchain.pem Anzahl
bis Anfang 2024 Leaf -> R3 -> ISRG Root X1 (Cross-Sign von DST Root CA X3) 3
2024 bis 2026 Leaf -> R10/R11 bzw. E5/E6 2
jetzt (Generation Y) Leaf -> YE2 -> Root YE (Cross-Sign) -> ISRG Root X2 (Cross-Sign) 4

Bis 2026 hing das Zwischenzertifikat direkt unter ISRG Root X1 bzw. X2. Diese Roots sind seit Jahren in jedem Truststore. Leaf + ein Intermediate reichten also immer.

Mit Generation Y hängt das Zertifikat unter der neuen Root Root YE. Diese Root ist neu und noch nicht in allen Truststores enthalten. Damit trotzdem jeder Client durchkommt, liefert Let's Encrypt zwei Cross-Signs mit.

Cross-Signs

Ein Cross-Sign ist ein zweites Zertifikat mit demselben Namen und demselben Schlüssel, aber von einer anderen CA unterschrieben.

Root YE   selbstsigniert        -> liegt bei neuen Clients im Truststore
Root YE   signiert von X2       -> Zertifikat Nr. 3 der Kette

ISRG Root X2  selbstsigniert    -> liegt seit 2020 in den Truststores
ISRG Root X2  signiert von X1   -> Zertifikat Nr. 4 der Kette

Weil Name und Schlüssel gleich sind, passt die Signatur von YE2 zu beiden Varianten von Root YE. Der Cross-Sign ist eine Brücke: die neue Root wird von einer alten, weit verbreiteten Root bezeugt.

Je nach Truststore endet die Prüfung an einer anderen Stelle:

Client kennt Root YE   Leaf -> YE2 -> [Root YE]
Client kennt nur X2    Leaf -> YE2 -> Root YE (cross) -> [X2]
Client kennt nur X1    Leaf -> YE2 -> Root YE (cross) -> X2 (cross) -> [X1]

ISRG Root X1 kennt praktisch jeder Client. Mit der vollständigen Kette funktioniert es also überall.

Die Dateien von certbot

cert.pem       nur das Leaf
chain.pem      nur die Zwischenzertifikate (3 Stück)
fullchain.pem  Leaf + Zwischenzertifikate (4 Stück)
privkey.pem    privater Schlüssel

Das Feld Certificate data in OPNsense nimmt nur ein einzelnes Zertifikat an. Mit der fullchain.pem kommt die Meldung:

Only a single certificate is allowed (found 7 sections)

Die Angabe 7 sections ist die interne Zählweise von OPNsense, nicht die Zahl der Zertifikate. Die Datei enthält 4.

Das ist neu seit OPNsense 26.7.8 (November 2026). Vorher konnte die fullchain.pem direkt eingefügt werden. Wegen Parsing-Problemen, die im schlimmsten Fall zu Konfigurations-Injection führen konnten, erlaubt OPNsense seitdem nur noch ein Zertifikat pro Eintrag. Es treffen also zwei Änderungen zusammen: die längere Kette bei Let's Encrypt und die strengere Prüfung in OPNsense.

Mehrere parallele Zwischenzertifikate

Let's Encrypt verteilt die Ausstellung zufällig auf mehrere Zwischenzertifikate (z.B. YE1 und YE2). Zwei Teilnehmer im selben Kurs können also Zertifikate von unterschiedlichen Intermediates bekommen. Die beiden Cross-Signs (Root YE, ISRG Root X2) sind für alle gleich.

Welches Zwischenzertifikat braucht mein Zertifikat?
  • openssl x509 -in fullchain.pem -noout -issuer

Warum es in manchen Browsern geht und in anderen nicht

AIA - Nachladen fehlender Zertifikate

Jedes Zertifikat enthält die Erweiterung Authority Information Access (AIA). Dort steht, wo der Aussteller heruntergeladen werden kann.

AIA-Eintrag im Leaf anzeigen
  • openssl x509 -in cert.pem -noout -ext authorityInfoAccess
Authority Information Access:
    CA Issuers - URI:http://ye2.i.lencr.org/

Fehlt dem Client ein Glied, kann er per HTTP-GET auf diese URL das Ausstellerzertifikat holen, die Signatur prüfen und den Pfad weiterbauen. Das Ergebnis wird gecacht.

Die URL ist absichtlich HTTP und nicht HTTPS: um eine HTTPS-Verbindung zu prüfen, bräuchte man wieder eine gültige Kette. Sicher ist es trotzdem, weil das geladene Zertifikat signiert ist. Ein Angreifer kann den Download nur verhindern, aber nichts unterschieben.

Wer nachlädt und wer nicht

Client AIA-Nachladen
Windows (Edge, Chrome) ja, systemweit über CryptoAPI
macOS / iOS (Safari, Chrome) ja
Android ab 7 ja
Chrome unter Linux ja, eigene Implementierung
Firefox nein
curl, wget, OpenSSL, Python, Mailserver nein

Firefox verzichtet absichtlich darauf: jeder AIA-Abruf verrät der CA, welche Seite gerade besucht wird. Stattdessen lädt Firefox die bekannten Zwischenzertifikate vorab aus der CCADB (gemeinsame Datenbank der Browserhersteller) und cacht Zwischenzertifikate von anderen Seiten.

Daraus ergibt sich das Verhalten im Labor:

  • Firefox auf Debian: kennt den fehlenden Cross-Sign schon (Preload oder Cache) -> geht
  • Firefox auf Rocky (frisches Profil, Truststore über p11-kit aus dem System) -> kennt ihn nicht -> SEC_ERROR_UNKNOWN_ISSUER

Der Browsertest ist also kein Beweis, dass der Server richtig konfiguriert ist. Massgeblich ist, was der Server tatsächlich ausliefert.

Umsetzung in OPNsense

OPNsense erwartet im Zertifikatsfeld nur das Leaf. Die Zwischenzertifikate werden getrennt als Authorities angelegt. OPNsense verknüpft das Zertifikat dann über den Aussteller mit den Authorities und baut die Kette selbst.

Kette prüfen und aufteilen

Anzahl der Zertifikate in der Datei
  • grep -c "BEGIN CERTIFICATE" fullchain.pem
Inhalt anzeigen (Subject und Issuer jedes Zertifikats)
  • openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
Datei in Einzelzertifikate aufteilen
  • csplit -z -f cert- fullchain.pem '/-----BEGIN CERTIFICATE-----/' '{*}'

Ergebnis:

cert-00   Leaf          *.it2XX.xinmen.de
cert-01   YE1 oder YE2  Zwischenzertifikat
cert-02   Root YE       Cross-Sign von ISRG Root X2
cert-03   ISRG Root X2  Cross-Sign von ISRG Root X1
Kontrolle eines Einzelzertifikats
  • openssl x509 -in cert-02 -noout -subject -issuer

Authorities importieren

Unter System -> Trust -> Authorities jeweils Import an existing Certificate Authority wählen. Nur das Zertifikat einfügen, das Feld für den Private Key bleibt leer.

Reihenfolge von oben nach unten, damit jede Authority ihren Aussteller schon vorfindet:

  • cert-03 (ISRG Root X2, Cross-Sign)
  • cert-02 (Root YE, Cross-Sign)
  • cert-01 (YE1 bzw. YE2 - im Zweifel beide anlegen)

Zertifikat importieren

Unter System -> Trust -> Certificates die Methode Import an existing Certificate wählen.

  • Certificate data: nur das Leaf, also cert-00 (bzw. cert.pem)
  • Private key data: privkey.pem

OPNsense sollte das Zwischenzertifikat als Aussteller erkennen.

Leaf direkt aus der fullchain.pem ausgeben (openssl liest nur das erste Zertifikat)
  • openssl x509 -in fullchain.pem

Zertifikat verwenden

  • Web-GUI: System -> Settings -> Administration -> SSL Certificate
  • HAProxy und andere Dienste: im jeweiligen Plugin auswählen

Kontrolle

Nicht im Browser prüfen, sondern mit openssl. openssl lädt nichts nach und hat keinen Cache.

Welche Zertifikate schickt der Server?
  • openssl s_client -connect opnsense.it2XX.xinmen.de:443 -servername opnsense.it2XX.xinmen.de -showcerts </dev/null | grep -E '^ *[0-9] s:|i:'

Falsch (Kette unvollständig):

 0 s:CN=*.it2XX.xinmen.de
   i:C=US, O=Let's Encrypt, CN=YE2
 1 s:C=US, O=Let's Encrypt, CN=YE2
   i:C=US, O=ISRG, CN=Root YE

Richtig (Kette vollständig):

 0 s:CN=*.it2XX.xinmen.de
   i:C=US, O=Let's Encrypt, CN=YE2
 1 s:C=US, O=Let's Encrypt, CN=YE2
   i:C=US, O=ISRG, CN=Root YE
 2 s:C=US, O=ISRG, CN=Root YE
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X2
 3 s:C=US, O=Internet Security Research Group, CN=ISRG Root X2
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
Ergebnis der Prüfung
  • openssl s_client -connect opnsense.it2XX.xinmen.de:443 -servername opnsense.it2XX.xinmen.de </dev/null 2>/dev/null | grep "Verify return code"
Verify return code: 0 (ok)

Übung: AIA live beobachten

Was liegt hinter den AIA-URLs?
Nachladen im Browser beobachten
  • Wireshark starten, Filter: http.host contains "lencr"
  • OPNsense mit unvollständiger Kette (nur Leaf + YE2) in Chrome aufrufen -> die AIA-Abrufe sind zu sehen
  • Dasselbe in Firefox -> kein Abruf, Fehlermeldung SEC_ERROR_UNKNOWN_ISSUER

Vorher frisches Browserprofil verwenden, sonst kommen die Zertifikate aus dem Cache und es wird nichts nachgeladen.

Dauerhafte Lösung: os-acme-client

Let's-Encrypt-Zertifikate sind 90 Tage gültig. Der manuelle Import müsste also regelmäßig wiederholt werden. Mit dem Plugin os-acme-client holt sich die OPNsense das Zertifikat selbst und erneuert es automatisch.

Für ein Wildcard-Zertifikat ist die DNS-01-Challenge nötig. Dafür braucht die OPNsense API-Zugang zum DNS-Anbieter der Domain.

Merksätze

  • Der Server muss die vollständige Kette ausliefern. Zwischenzertifikate stehen in keinem Truststore.
  • Generation Y: die Kette ist 4 Zertifikate lang, weil die neue Root YE über Cross-Signs an X2 und X1 angebunden ist.
  • In OPNsense: nur das Leaf als Certificate (das Feld erlaubt nur ein Zertifikat), alle Zwischenzertifikate als Authorities.
  • Browsertests täuschen (AIA, Preload, Cache). Geprüft wird mit openssl s_client -showcerts.