Let's Encrypt cert_exists() kürzt die CA-Kette, was zu erzwungener Neuerteilung und Rate-Limit-Fehlern führt

Ich habe einen reproduzierbaren Bug in web.letsencrypt.ssl.template.yml gefunden: cert_exists() lehnt ein gültiges Zertifikat ab, weil es nur das erste Zertifikat aus dem in ca.cer gespeicherten Aussteller-Bundle behält. Dies löst unnötigerweise den --force-Fallback aus.

Hostnamen und Pfade in der folgenden Ausgabe sind anonymisiert.

Reproduktion

Am 7. Oktober 2026 hat acme.sh erfolgreich ein RSA-Zertifikat ausgestellt. Seine ca.cer enthielt:

subject=C=US, O=Let's Encrypt, CN=YR2
issuer=C=US, O=ISRG, CN=Root YR

subject=C=US, O=ISRG, CN=Root YR
issuer=C=US, O=Internet Security Research Group, CN=ISRG Root X1

Der vom Template verwendete Befehl schlägt bei diesen tatsächlichen Dateien fehl:

$ openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
C=US, O=Let's Encrypt, CN=YR2
error 2 at 1 depth lookup: unable to get issuer certificate
error fullchain.cer: verification failed

Die Verwendung des vollständigen Aussteller-Bundles als Eingabe für den Aufbau der Kette ist erfolgreich:

$ openssl verify -show_chain -untrusted ca.cer fullchain.cer
fullchain.cer: OK
Chain:
depth=0: CN=forum.example.com (untrusted)
depth=1: C=US, O=Let's Encrypt, CN=YR2 (untrusted)
depth=2: C=US, O=ISRG, CN=Root YR (untrusted)
depth=3: C=US, O=Internet Security Research Group, CN=ISRG Root X1

openssl x509 -in ca.cer gibt nur das erste PEM-Zertifikat aus und verwirft das cross-signierte Root YR, das benötigt wird, um YR2 mit dem vertrauenswürdigen ISRG Root X1 zu verbinden.

Warum der vorhandene Check so geschrieben wurde

#576 / 8e2ccee führte diesen Check im Oktober 2021 ein, um den abgelaufenen Root in der alten Let’s Encrypt-Kette zu vermeiden. Der Bericht von 2021 beschreibt auch die erneute Ausstellung beim Rebuild.

Mit der aktuellen Hierarchie ist der erste Aussteller allein auf diesem Host nicht ausreichend. -untrusted stellt alle Zertifikate für den Kettenaufbau bereit, während die Vertrauensanker im System-Speicher verbleiben, was die Absicht der Korrektur von 2021 beibehält.

Auswirkungen

Das Template stellt RSA aus, prüft es und erzwingt dann die Ausstellung, wenn der Check fehlschlägt. Es wiederholt dieses Muster für ECDSA.

Dies war eine saubere Neuinstallation, die einen Hostnamen wiederverwendete, der ein oder zwei Tage zuvor installiert worden war, sodass sein Kontingent möglicherweise bereits teilweise aufgebraucht war. Das Protokoll und die Reihenfolge des Templates zeigen:

  1. Normales RSA: Erfolg um 10:30:55 UTC.
  2. Erzwingener RSA-Fallback: 429 um 10:30:56 UTC.
  3. Normales ECDSA: 429 um 10:30:58 UTC.
  4. Erzwingener ECDSA-Fallback: 429 um 10:31:01 UTC.

Die erzwungene RSA-Anfrage folgte einer erfolgreichen Ausstellung des gültigen Zertifikats, das in der obigen Reproduktion verwendet wurde. ECDSA wurde nicht ausgestellt, und HTTPS startete nicht, bis das gültige RSA-Paar auf die von nginx erwarteten _ecc-Dateinamen kopiert wurde.

Eine fehlgeschlagene Ausstellung, die eine unbrauchbare Zertifikatsdatei hinterlässt, kann auch zu dem nginx-Fehler führen, der in verwandten Berichten zu sehen ist:

PEM_read_bio_X509_AUX() failed
... no start line ...
Expecting: TRUSTED CERTIFICATE

Das exakte Set-Kontingent wird schrittweise aufgefüllt, etwa ein Zertifikat alle 34 Stunden. Aus der Reihenfolge des Templates kann eine unnötige erzwungene RSA-Ausstellung die zurückgegebene Kapazität vor ECDSA verbrauchen, sodass das Warten und Rebuilden ohne Korrektur des Checks ECDSA möglicherweise erneut blockiert.

Korrektur und Validierung

- openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
+ openssl verify -untrusted ca.cer fullchain.cer

Validierung abgeschlossen:

  • Tatsächliche YR2-RSA-Dateien: Vorhandener Check schlägt fehl; vorgeschlagener Befehl ist erfolgreich.

  • Tatsächliche RSA- (YR1 → Root YR → ISRG Root X1) und ECDSA- (YE1 → Root YE → ISRG Root X2) Dateien werden beide innerhalb des Discourse-Containers mit OpenSSL 3.5.7 validiert.

  • Ein vollständiger Rebuild generiert das korrigierte Skript und wiederverwendet beide Zertifikate: zwei Skip-Meldungen, beide Ketten installiert und nginx -t besteht den Test.

  • Synthetische Regressionsfälle der 2021-Kette mit vorhandenem und entferntem abgelaufenen alten Root bestehen beide mit -untrusted auf OpenSSL 3.0.13. Der alte vollständige CAfile-Befehl reproduziert die jeweiligen depth-3-Ablauf- und depth-2-Ausstellerfehler.

Verwandte Berichte

411097 führt seinen _ecc-Check-Fehler auf ein fehlendes Verzeichnis in der vom Autor eingefügten Diagnose zurück. Hier existieren die RSA-Dateien und der Fehler wird direkt durch den Validierungsbefehl reproduziert.

413078 enthält übereinstimmende YR1/YE2-Ausstellerfehler; der spätere Anwendungs-Ausfall wurde nicht mit diesem Problem in Verbindung gebracht.

PR: FIX: preserve the Let's Encrypt issuer chain in cert_exists - Pull Request #1136 - discourse/discourse_docker - GitHub