Fixes
Web Application Security · Updated 2026-05-02
Certificate Chain Completeness
Server must serve the full intermediate chain, not just the leaf cert. Tied to SAN Hostname Match via the same verified-handshake flag.
A trusted certificate is a chain: your server's leaf cert is signed by an intermediate CA, which is signed by a root CA in the client's trust store. The leaf alone is not enough; the server must also send the intermediate certs so the client can build the chain to a trusted root. When the chain is incomplete, modern browsers may recover by fetching missing intermediates via AIA (Authority Information Access), but strict clients (Python ssl, older mobile, IoT, server-to-server callers) refuse to validate.
Important coupling: this sub-weight and SAN Hostname Match share a single underlying signal (verified_chain_and_san_ok). If either fails, both fail in the same scan. To know which one is actually broken, inspect the cert manually with openssl.
How the check works
Per primary HTTPS host, RedScore performs a strict TLS handshake using the Python ssl module's default context (system trust store + hostname verification + chain validation). If the chain validates end-to-end, both Chain Completeness and SAN Hostname Match pass (5 + 8 pts). If the chain cannot be built (missing intermediate, untrusted root, expired cross-sign, self-signed cert, etc.), both fail.
How the verdict maps to evidence
- Pass (5/5 per host): strict TLS handshake succeeded; full chain to a trusted root validated.
- Fail (0/5 per host): strict handshake failed for any reason. The reason could be missing intermediates, untrusted root, expired cross-sign, or other; the score does not differentiate.
Diagnose: inspect what the server is sending
Three diagnostic angles:
Count the certs the server sends
openssl s_client -connect yourdomain.tld:443 -servername yourdomain.tld -showcerts < /dev/null 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
# Expected for Let's Encrypt: 2 (leaf + ISRG intermediate)
# Expected for most commercial CAs: 2 to 3 (leaf + intermediate(s))
# A result of 1 means only the leaf is being sent. Chain is incomplete.Strict verification end-to-end
openssl s_client -connect yourdomain.tld:443 \
-servername yourdomain.tld \
-verify_return_error < /dev/null
# Look for "Verify return code: 0 (ok)" at the bottom.
# Anything else is the verification error you need to fix.Identify each cert in the chain
openssl s_client -connect yourdomain.tld:443 -servername yourdomain.tld -showcerts < /dev/null 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/' \
| openssl crl2pkcs7 -nocrl -certfile /dev/stdin \
| openssl pkcs7 -print_certs -text \
| grep -E "^Certificate|Issuer:|Subject:"
# Lists each cert's Subject and Issuer. The chain should follow:
# leaf Subject: CN=yourdomain.tld Issuer: CN=R10 (LE intermediate)
# intermed Subject: CN=R10 Issuer: CN=ISRG Root X1
# (root is in the client trust store; the server should not send it).Fix: send the full chain
Let's Encrypt with certbot
certbot writes both cert.pem (leaf only) and fullchain.pem (leaf + intermediates) into /etc/letsencrypt/live/yourdomain.tld/. Always point your web server at fullchain.pem, never cert.pem.
nginx
ssl_certificate /etc/letsencrypt/live/yourdomain.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.tld/privkey.pem;Apache
SSLCertificateFile /etc/letsencrypt/live/yourdomain.tld/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.tld/privkey.pem
# On older Apache (<2.4.8) you may also need:
# SSLCertificateChainFile /etc/letsencrypt/live/yourdomain.tld/chain.pemCommercial CAs
Most commercial CAs deliver a bundle file with the leaf and intermediates. If they ship them as separate files, concatenate them into a single fullchain file (leaf first, then each intermediate, root excluded):
Build the bundle
cat yourdomain.tld.crt intermediate1.crt intermediate2.crt > fullchain.pemOrder matters: leaf must come first. Do not include the root cert; clients have it in their trust store.
Caddy
Caddy serves the full chain by default for ACME-issued certs. No configuration needed. If you provide a custom cert via tls directive, point it at a fullchain bundle, not the leaf.
Cloudflare and CDNs
Cloudflare, AWS CloudFront, Fastly, Akamai all serve the full chain automatically when you use their managed TLS. If you upload a custom cert (Cloudflare "Upload Custom SSL", CloudFront imported ACM cert), upload the fullchain bundle. Confirm with the openssl count test above after uploading.
AWS ACM
ACM-issued certs include the chain automatically. ACM-imported certs require you to provide the chain in the certificate-chain field at import time; if you imported leaf-only by mistake, re-import with the full chain.
Verify the fix
- Re-run the openssl chain count and verify command above.
- ssllabs.com/ssltest reports chain completeness explicitly. Look for "Chain issues: None".
- whatsmychaincert.com walks the chain you serve and tells you which intermediates are missing.
- Test from a strict client: curl --cacert /etc/ssl/certs/ca-certificates.crt https://yourdomain.tld. If curl fails with "unable to get local issuer certificate", the chain is incomplete.
- Re-run the RedScore lookup. Pass requires strict-verify success on every primary HTTPS host.
Common pitfalls
- Pointing nginx or Apache at cert.pem instead of fullchain.pem. The most common cause of incomplete chains. Switch the config to fullchain.pem and reload.
- Stale chain after CA renewal. CA roots and intermediates rotate. The Let's Encrypt 2021 DST Root CA X3 expiry broke many older clients overnight; certbot's new chains use ISRG Root X1. Update certbot regularly so renewals pull the current chain.
- Wrong intermediate. Some CAs offer multiple intermediates (cross-signed for backwards compatibility, native for modern clients). Pick the one that matches your client base; cross-signed older intermediates may produce smaller chains but do not validate everywhere.
- Browser passes via AIA fetching, strict clients fail. Modern browsers can fetch missing intermediates from URLs in the leaf cert's AIA extension. Strict clients (Python ssl, Go, Java without certain flags, IoT, mobile WebViews) usually do not. The score reflects strict verification; if browsers say "locked" but RedScore fails, AIA fetching is masking your bad chain.
- Order of certs in the bundle. Leaf must be first; intermediates follow in order from issuing CA up. Reversed or shuffled bundles work in some servers but break others.
- Including the root cert in the bundle. Roots live in the client trust store, not on your server. Including the root makes the bundle larger and can confuse some validators. Strip it.
- Tied to SAN Hostname Match. If your handshake fails strict verification because of a SAN mismatch rather than chain issues, this sub-weight still fails. See the SAN Hostname Match fix to confirm which one is actually broken.
What to do next
See how these recommendations apply to your site's current scan results.
Scan domain