Skip to content
API2026-10-11Begin4 min read

Incomplete HTTPS Certificate Chain: Browser Distrusts a “Valid” Leaf

Symptom: decode looks fine, browsers disagree

A common TLS time sink:

  • Paste the leaf into a certificate decoder — Not Before / Not After look healthy
  • SAN contains your hostname
  • Mobile Safari, strict corporate proxies, or default curl still report unable to get local issuer certificate or “not trusted”

The leaf is not “fake”. The trust path is broken: the client never receives (or cannot match) the intermediate that signed that leaf.

How many layers matter

For day-to-day debugging, three tiers are enough:

TierSigned byTrusted by default?
Leaf (end-entity)Intermediate CANo — must be proven by the chain
IntermediateRoot (or another intermediate)Usually not fully preloaded
RootSelf-signedYes — in the OS/browser trust store

The server must send: leaf + enough intermediates to reach a trusted root. Do not stuff the root into your nginx ssl_certificate chain in most setups (unnecessary; some clients are picky).

A five-minute triage

1. Pull the chain with SNI

openssl s_client -connect www.example.com:443 \
  -servername www.example.com -showcerts </dev/null

Without -servername, a multi-cert IP may return a different certificate and send you on a wild goose chase.

Count BEGIN CERTIFICATE blocks:

  • Only one: highly suspicious — leaf-only
  • Two or more: save them as leaf.pem, int1.pem, …

2. Check linkage: Issuer must equal the next Subject

openssl x509 -in leaf.pem -noout -issuer -subject
openssl x509 -in int1.pem -noout -issuer -subject

Expect: leaf Issuer == int1 Subject; int1 Issuer lands on a trusted root (or another intermediate). Mismatch means wrong files or a scrambled bundle.

The on-site decoder can show the same PEM fields — see the certificate decoder guide. It does not build a PKIX path; use local OpenSSL for trust decisions.

3. Verify on your machine

# Trust store path varies by distro; a CA bundle file also works
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
  -untrusted int1.pem leaf.pem

OK means the path closes under this machine’s trust model. unable to get local issuer almost always means a missing intermediate or a bad -untrusted file.

Three deployment root causes

A. Leaf uploaded, fullchain ignored

ACME clients typically emit:

  • cert.pem — leaf only
  • fullchain.pem — leaf + intermediate(s)

Point nginx / Caddy / most cloud LBs at fullchain. Leaf-only often “works on my Chrome” because desktop browsers cached the intermediate, while mobile and strict clients fail immediately.

B. Wrong concatenation order

Correct order: leaf first, intermediates after. Reversed bundles fail hard on some stacks and flake on others. Issuer/Subject linkage beats eyeballing PEM.

C. Intermediate expired or rotated

“Leaf has 60 days left” does not mean the chain is alive. Intermediate rotations (for example Let’s Encrypt’s past cutovers) broke sites that never touched their leaf. Check every tier’s Not After.

Guomi / dual-certificate note

TLCP uses signature + encryption certificates. Chain completeness still applies; you simply have more files and stricter ordering. See the Guomi TLS nginx guide. For RSA key context on the international stack, see the RSA guide.

CDN and reverse-proxy twists

Fixing files on the origin is not enough if TLS terminates at a CDN or cloud load balancer. Confirm which hop presents the certificate to the browser:

  1. Browser → CDN edge (what users see)
  2. CDN → origin (often a different cert, or HTTP on a private link)

openssl s_client against the public hostname checks the edge. If you only updated origin disk and forgot to replace the cert object in the CDN console, decode results on the origin PEM will look perfect while the public site stays broken. Always re-pull with s_client against the hostname users type after every change.

Implementer's note

ToolVault’s certificate tool only parses the PEM you paste. It does not fetch remote certificates and does not perform full chain validation — SSRF/privacy for the former, WebCrypto limits for the latter. Separating “read fields” from “decide trust” avoids fake confidence from online tools. Need a trust verdict: local openssl verify or SSL Labs. Need SAN/dates/Issuer clarity: use the certificate decoder.

Checklist

  1. Does s_client -servername show at least two certificates?
  2. Does the leaf Issuer equal the first intermediate’s Subject?
  3. Does the server config point at fullchain, not leaf-only?
  4. Is every tier’s Not After still in the future?
  5. After fixing files, did you reload the process that actually terminates TLS (container / reverse proxy / CDN origin)?

This article is brought to you by ToolVault. More developer tools at the homepage.