Skip to content

TLS certificates: always issue RSA with the ISRG Root X1 chain

Every certificate on this server must be issued with these two flags:

--key-type rsa --rsa-key-size 2048 --preferred-chain "ISRG Root X1"

Leave them off and certbot uses its own defaults, which breaks the site for a meaningful slice of visitors. This is not theoretical — it happened.

What went wrong (2026-08-18)

Visitors on some corporate networks could not reach franzvoid.is, and an ISP reported the block was "because of the certificate".

Certbot 2.x defaults to ECDSA keys. Every one of the 15 certificates on this box had key_type = ecdsa in /etc/letsencrypt/renewal/*.conf and no preferred_chain, so each served this chain:

leaf (ECDSA P-256)
  └─ Let's Encrypt YE1 / YE2
       └─ ISRG Root YE
            └─ ISRG Root X2
                 └─ (issued by ISRG Root X1)

Two separate problems:

  1. RSA-only clients could not complete the handshake at all. An ECDSA-only certificate means a client that does not support ECDSA has nothing to negotiate. Plenty of corporate TLS-inspection appliances and ISP filtering boxes are still RSA-only. They report this to the user as a certificate error, which is why the symptom came back as "blocked because of the cert".
  2. The chain reached a trusted root only through two cross-signatures. Older trust stores do not carry ISRG Root YE or ISRG Root X2, and not every verifier builds a path across stacked cross-signs.

Reproduce the first problem against any ECDSA-only host:

# fails on an ECDSA-only cert, succeeds on an RSA one
echo | openssl s_client -connect <host>:443 -servername <host> -tls1_2 \
  -cipher 'ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256'

The fix

All 15 certificates were reissued as RSA with --preferred-chain "ISRG Root X1", giving a 3-certificate chain that is one hop from the most widely trusted root there is:

leaf (RSA 2048)
  └─ Let's Encrypt YR1
       └─ ISRG Root YR  ── issued by ISRG Root X1

Certbot writes the flags into the renewal config, so certbot.timer keeps them on auto-renewal. Confirm any cert with:

grep -E 'key_type|preferred_chain' /etc/letsencrypt/renewal/<name>.conf

Reissuing an existing certificate

--cert-name with no -d flags reuses the lineage's existing SAN list, which matters for the multi-name certs (franzvoid.is+www, reynsi.is+www, and wiki.mus.is which covers four names):

sudo certbot certonly --apache --cert-name <name> \
  --key-type rsa --rsa-key-size 2048 --preferred-chain "ISRG Root X1" \
  --force-renewal --non-interactive
sudo systemctl reload apache2

Mind Let's Encrypt's limits: 50 certificates per registered domain per week, and 5 duplicate certificates per week. Get the flags right on one host before batching.

Verifying

# chain should be 3 certs and end at ISRG Root X1 — no "Root YE" anywhere
echo | openssl s_client -connect <host>:443 -servername <host> -showcerts 2>/dev/null \
  | grep -E 's:|i:'

# and it should still validate
echo | openssl s_client -connect <host>:443 -servername <host> 2>&1 | grep 'Verify return code'

https://www.ssllabs.com/ssltest/ is the external check worth running; it reports trust-store coverage per client and flags unusual chains.

New sites

~/.claude/skills/new-wp/scripts/new-wp.sh (issue_certificate()) already passes these flags. Any other path that issues a certificate must pass them too.