Skip to content

FIX: preserve the Let's Encrypt issuer chain in cert_exists - #1136

Open
jmoureaux wants to merge 1 commit into
discourse:mainfrom
surikai-engineering:fix/letsencrypt-cert-exists-untrusted-chain
Open

jmoureaux wants to merge 1 commit into
discourse:mainfrom
surikai-engineering:fix/letsencrypt-cert-exists-untrusted-chain

Conversation

@jmoureaux

@jmoureaux jmoureaux commented Oct 7, 2026 •

Copy link
Copy Markdown

cert_exists() can reject a valid Let's Encrypt certificate and trigger unnecessary forced issuance because openssl x509 -in ca.cer keeps only the first certificate from the issuer bundle.

On actual RSA files containing YR2 and cross-signed Root YR, the current check drops Root YR and fails:

$ 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

$ openssl verify -untrusted ca.cer fullchain.cer
fullchain.cer: OK

In the reinstall that exposed this, RSA issuance succeeded and was immediately followed by a forced RSA fallback, then two ECDSA attempts. The last three requests received rate-limit responses. The hostname had been used recently, so the quota could already have been partly consumed.

Preserve the intent of #576

#576 / 8e2ccee changed this check in 2021 to avoid the expired root in the old Let's Encrypt chain.

The current hierarchy needs the cross-signed Root YR to reach ISRG Root X1 on this host. Supplying the complete bundle with -untrusted preserves the chain-building certificates and uses the system trust store for trust anchors.

Validation

  • Reproduced the existing failure and successful proposed command on actual YR2 RSA files.
  • Verified actual RSA YR1 and ECDSA YE1 chains inside the Discourse container with OpenSSL 3.5.7, reaching ISRG Root X1 and X2 respectively.
  • Completed a full rebuild: the generated script contains -untrusted, both normal issuance calls report Skip, both existing chains are installed, and nginx -t passes.
  • Replayed synthetic 2021-chain cases on OpenSSL 3.0.13 with the expired old root present and removed: full-CAfile reproduces the historical error types, while -untrusted succeeds in both cases.

Meta: https://meta.discourse.org/t/lets-encrypt-cert-exists-truncates-the-ca-chain-causing-forced-reissuance-and-rate-limit-failures/414167

cert_exists() keeps only the first certificate from ca.cer. On the
observed YR2 chain, this removes the cross-signed Root YR certificate
and rejects a valid certificate, triggering the --force fallback.

Supply the full issuer bundle with -untrusted and use the system
trust store for trust anchors, preserving the intent of discourse#576.
@discoursebot

Copy link
Copy Markdown

This pull request has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/lets-encrypt-cert-exists-truncates-the-ca-chain-causing-forced-reissuance-and-rate-limit-failures/414167/1

@discoursebot

Copy link
Copy Markdown

This pull request has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/the-too-many-certificates-error-ports-80-443-open-then-close/413078/10

@discoursebot

Copy link
Copy Markdown

This pull request has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/need-help-with-dual-container-issue-with-letsencrypt-for-a-few-days-now/411097/7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants