cert_exists() di Let's Encrypt tronca la catena CA, causando emissione forzata e errori di rate limit

Ho trovato un bug riproducibile in web.letsencrypt.ssl.template.yml: cert_exists() rifiuta un certificato valido perché conserva solo il primo certificato dal bundle dell’emittente memorizzato in ca.cer. Questo attiva inutilmente il fallback --force.

I nomi host e i percorsi nell’output sottostante sono anonimizzati.

Riproduzione

Il 7 ottobre 2026, acme.sh ha emesso con successo un certificato RSA. Il suo ca.cer conteneva:

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

Il comando utilizzato dal template fallisce con questi file effettivi:

$ 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

L’uso del bundle completo dell’emittente come input per la costruzione della catena ha successo:

$ 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 restituisce solo il primo certificato PEM, scartando il Root YR cross-firmato necessario per collegare YR2 al fidato ISRG Root X1.

Perché il controllo esistente è stato scritto in questo modo

#576 / 8e2ccee ha introdotto questo controllo nell’ottobre 2021 per evitare la radice scaduta nella vecchia catena di Let’s Encrypt. Anche la relazione del 2021 descrive la riemissione al rebuild.

Con la gerarchia attuale, il solo primo emittente non è sufficiente su questo host. -untrusted fornisce tutti i certificati per la costruzione della catena mantenendo gli ancori di fiducia nello store di sistema, preservando l’intento della correzione del 2021.

Impatto

Il template emette RSA, lo verifica e poi forza l’emissione se il controllo fallisce. Ripete questo schema per ECDSA.

Si è trattato di una reinstallazione pulita che riutilizzava un nome host installato uno o due giorni prima, quindi la sua quota potrebbe già essere stata parzialmente consumata. Il log e l’ordine del template mostrano:

  1. RSA normale: successo alle 10:30:55 UTC.
  2. Fallback RSA forzato: 429 alle 10:30:56 UTC.
  3. ECDSA normale: 429 alle 10:30:58 UTC.
  4. Fallback ECDSA forzato: 429 alle 10:31:01 UTC.

La richiesta RSA forzata ha seguito un’emissione riuscita del certificato valido utilizzato nella riproduzione sopra. ECDSA non è stato emesso e HTTPS non è partito finché la coppia RSA valida non è stata copiata nei nomi file _ecc attesi da nginx.

Un’emissione fallita che lascia un file di certificato inutilizzabile può anche portare all’errore nginx visto in relazioni correlate:

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

La quota per set esatto si ricarica gradualmente, circa un certificato ogni 34 ore. Dall’ordine del template, un’emissione RSA forzata non necessaria può utilizzare la capacità restituita prima di ECDSA, quindi attendere e ricostruire senza correggere il controllo potrebbe lasciare ECDSA bloccato di nuovo.

Correzione e validazione

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

Validazione completata:

  • File RSA YR2 effettivi: il controllo esistente fallisce; il comando proposto ha successo.

  • I file RSA effettivi (YR1 → Root YR → ISRG Root X1) e ECDSA (YE1 → Root YE → ISRG Root X2) vengono entrambi verificati all’interno del container Discourse con OpenSSL 3.5.7.

  • Un rebuild completo genera lo script corretto e riutilizza entrambi i certificati: due messaggi Skip, entrambe le catene installate e nginx -t passa.

  • I casi di regressione sintetici della catena 2021 con la vecchia radice scaduta presente e rimossa passano entrambi con -untrusted su OpenSSL 3.0.13. Il vecchio comando completo CAfile riproduce rispettivamente gli errori di scadenza a profondità 3 e di emittente a profondità 2.

Relazioni correlate

411097 attribuisce il fallimento del controllo _ecc a una directory mancante nella diagnosi incollata dall’autore. Qui, i file RSA esistono e il fallimento è riprodotto direttamente dal comando di verifica.

413078 contiene errori di emittente YR1/YE2 corrispondenti; il successivo interruzione dell’applicazione non è stato collegato a questo problema.

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