Skip to content
SiteCheckTools

Articles/SSL Checker

Fixing an Incomplete Certificate Chain

It works in your browser. It fails in curl, on older Androids, and for a slice of your visitors you will never hear from.

·7 min read

The tool this explains

SSL Checker

Read a site's certificate chain: issuer, validity dates, days until expiry, and whether the chain is complete.

The site loads perfectly in your browser. It loads for everyone in the office. Then a customer says it will not open on their phone, your monitoring service reports a certificate error, and curl refuses to connect.

An incomplete certificate chain is the most persistent TLS misconfiguration there is, precisely because the person who set it up is the least likely to see it. This is why it hides, and how to fix it properly.

What a chain is

Browsers do not trust your certificate directly. They trust a small set of root certificates shipped with the operating system or browser — a few hundred, from a few dozen authorities.

Your certificate is not signed by one of those roots. Roots are kept offline and used sparingly, because a compromised root is a catastrophe. Instead, a root signs an intermediate certificate, and the intermediate signs yours.

So there is a chain:

your certificate  ->  intermediate  ->  root (already trusted)

To verify you, a browser must be able to follow every link. It has the root. It has your certificate, because you sent it. It does not have the intermediate unless you send it too.

That is the entire issue. Your server is responsible for sending its own certificate plus every intermediate above it — but not the root, which the client already has.

Why it works for you and fails for others

If the intermediate is missing, some clients recover and some do not. That inconsistency is the whole problem.

Browsers often paper over it. Desktop browsers cache intermediates they have seen on other sites. Once your browser has encountered that intermediate anywhere, it has it, and your site verifies fine — for you, permanently. Chrome and Firefox will also fetch a missing intermediate over the network using the AIA extension in your certificate, so they can often repair the chain silently.

Other clients will not. curl, wget, OpenSSL, Java, Python's requests, Go's HTTP client, older Android devices, some payment gateways, and most monitoring services do exactly what the specification says: if the chain does not verify with what was sent, reject the connection.

The result is a site that is fine in every browser you personally test, and broken for API clients, webhook deliveries, older mobile devices and anything server-to-server. The people affected rarely tell you. They assume your site is down.

This is also why the problem survives so long. Every time someone checks, it works.

Confirming it

Run the domain through the SSL Checker. It connects without a browser's cached intermediates, so it sees what a fresh client sees. Two results together confirm the diagnosis:

  • Certificate chain reports 1 certificate
  • Browser trust fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE

A correctly configured server typically presents two or three certificates. One is almost always this problem.

From a terminal, the equivalent is:

openssl s_client -connect example.com:443 -servername example.com

Look at the certificate chain OpenSSL prints at the top. One entry means the intermediate is missing. You will also see verify error:num=20:unable to get local issuer certificate, which is the same finding in OpenSSL's wording.

Testing from a machine that has never visited the site is a reasonable sanity check, but a tool that connects fresh is more reliable than trying to find an uncontaminated browser.

Fixing it

The fix is always the same: install the full chain rather than the certificate alone.

Certificate authorities issue several files, and the names vary — which is exactly how the mistake happens. Typically you receive:

  • cert.pem or example.com.crt — your certificate alone
  • chain.pem or intermediate.crt — the intermediates alone
  • fullchain.pem or bundle.crt — your certificate followed by the intermediates

You want the third one. Someone installs the first because it has their domain name on it and looks like the obvious choice, and everything appears to work.

Nginx takes the combined file in ssl_certificate. It does not have a separate directive for intermediates, so the file must contain both:

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

Using cert.pem here instead of fullchain.pem is the single most common cause of this problem on Nginx.

Apache historically used SSLCertificateChainFile for the intermediates, separate from SSLCertificateFile. Since Apache 2.4.8 that directive is deprecated and SSLCertificateFile accepts the combined file, which is now the recommended approach.

Order matters. The file must be your certificate first, then each intermediate in order up the chain. Concatenating them backwards produces a file that some clients accept and others reject — the same intermittent behaviour you were trying to fix.

If you are assembling it by hand:

cat cert.pem chain.pem > fullchain.pem

Reload the server afterwards. A certificate change requires a reload; editing the file alone changes nothing.

Then re-run the SSL Checker and confirm the chain reports two or more certificates and Browser trust passes.

While you are looking at the chain, check the expiry dates of the intermediates as well as your own certificate.

An intermediate that expires before your certificate does will break the site on the intermediate's date — even though your certificate is still valid and nothing in your renewal process looks wrong. Renewing your certificate alone does not fix it, because the stale intermediate is still in your chain file.

The SSL Checker flags this specifically: its Chain result compares every certificate's expiry against the leaf's and warns when something above expires sooner. The fix is to update the chain file with the current intermediate from your authority, which usually happens automatically on your next renewal if your client is configured to write fullchain.pem.

Preventing it coming back

Point your automation at the right file. Most incomplete chains are introduced once and then faithfully preserved by an automated renewal that keeps writing the same wrong file. Check what your ACME client is configured to deploy.

Monitor from outside a browser. Any uptime or certificate monitor that connects fresh will catch this. A browser check will not.

Test after every certificate change, not just after the first install. A renewal can change the intermediate — authorities do rotate them — and a process that worked last time can produce a broken chain this time.

The short version

Your server must send its certificate and the intermediates. Browsers hide the mistake by caching or fetching what is missing, so it looks fine to you while failing for curl, older Androids and every server-to-server client. Install fullchain.pem, not cert.pem, reload, and confirm with a tool that connects without a browser's cache.

Check this on your own domain with the SSL Checker. Free, no signup, and every result shows the raw data behind it.