SSL certificate chain incomplete: what it is, how to see it, and how to fix it
Corrections: support@domainvane.com
The chain the client has to build
A public certificate for your hostname is a leaf. A browser trusts a small set of root certificates that shipped with the browser or the operating system. Public issuers commonly sign leaves through one or more intermediates, although a leaf can be signed directly by a trusted root.
During the handshake the server sends a list of certificates. RFC 5246, section 7.4.2, is the TLS 1.2 rule, and it is the rule operators still implement:
- the sender's own certificate comes first
- each certificate after that must directly certify the one before it
- the self-signed root may be left off, because the client is expected to already have the root
If the server sends only the leaf, the client has to find the intermediate some other way. Some clients have it cached. Some will chase an Authority Information Access URL on the leaf and download the issuer. Some will not, or will time out when they try. The result is a site that loads in one browser and fails in another, or that fails only on a phone, or only from a monitoring check that does not do the extra download.
Nginx's HTTPS documentation describes the same symptom: one browser complains about a certificate signed by a well-known authority, and another accepts it, because the issuing intermediate is not in that browser's store. The fix they document is to send the intermediates yourself.
Domainvane's CHAIN_INCOMPLETE status means chain validation failed against Node's default CA store after higher-precedence errors were excluded. A missing intermediate is the common case, not the definition of the code. An untrusted or private issuer, a bad signature, or a path-constraint failure can produce the same status even when the server sent every certificate in its intended chain. Inspect the served chain and the verification error before changing files.
How to see what the server sent
From a machine that can reach port 443:
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts -verify_hostname www.example.com -verify_return_error
Nginx's own docs point at openssl s_client -connect as the way to see the chain the server presents. -servername sets SNI only; it does not verify the hostname. -verify_hostname performs that name check, and -verify_return_error stops on a verification failure. Read the Certificate chain section and the final verification error.
- Certificate
0should be the leaf. Inspect its Subject Alternative Name extension rather than treating its Subject or Common Name as the hostname:
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName
- Certificate
1(and2, if present) should be intermediates. - You do not need to see the root. RFC 5246 allows it to be omitted.
- A chain section that contains only certificate
0is the usual incomplete-chain case. verify error:num=21:unable to verify the first certificatemeans the client that just connected could not build a path. Confirm it against the served certificates; a private or untrusted issuer, bad signature, or path constraint can also prevent validation without a missing certificate.
Then check order. The leaf's issuer name should be the subject of the next certificate. If the file on disk is intermediate-then-leaf, some servers reject it on startup rather than serve a confusing chain. Nginx documents a startup failure (key values mismatch) when the server certificate and the bundle were concatenated in the wrong order. Fix the order before you reload.
Three neighboring failures
These get the same screenshot in a browser and different fixes on the server.
| What you observe | What is actually wrong |
|---|---|
| Leaf dates are in the past | The certificate is expired. Install a current leaf. The chain files may be fine. |
| Leaf dates are in the future | The certificate is not yet valid, or the server clock is wrong. |
| The names on the certificate do not include this hostname | Hostname mismatch. A wildcard covers one label (*.example.com covers www.example.com and does not cover example.com). |
| The certificate is signed by itself | Self-signed. There is no public intermediate to append. Clients that do not trust this certificate on purpose will keep failing. |
| The verification path fails after earlier errors are excluded | Inspect the served chain and verification error. A missing intermediate is common, but it is not the only cause. |
Domainvane's TLS check uses that distinction, with a fixed order. The first match wins: NOT_YET_VALID, then EXPIRED, then SELF_SIGNED, then CHAIN_INCOMPLETE, then HOSTNAME_MISMATCH, after connection-level failures (DNS_FAIL, CONN_REFUSED, TIMEOUT, TLS_PROTOCOL). CHAIN_INCOMPLETE means chain validation failed against Node's default CA store after higher-precedence errors were excluded. A self-signed certificate is reported as SELF_SIGNED, so you do not "complete the chain" by appending a public intermediate to a certificate you minted yourself.
The check stores the error code, dates, issuer, subject, and whether the chain validated. It does not store the raw chain. Use openssl s_client when you need the PEM bytes.
Fix it on nginx
Nginx has one directive for the certificate file. Intermediates go in that same file, leaf first. The ssl_certificate documentation says: primary certificate first, then the intermediate certificates.
cat leaf.pem intermediate.pem > fullchain.pem
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
Test, then reload:
nginx -t && systemctl reload nginx
If the certificate came from Certbot, the files already exist under /etc/letsencrypt/live/<name>/. Certbot's compatibility notes name cert.pem, chain.pem, fullchain.pem, and privkey.pem. fullchain.pem is the leaf plus the intermediates. cert.pem is the leaf alone. When the issuer supplied intermediates, nginx normally needs fullchain.pem; first confirm the served chain and verification error rather than assuming the file choice is the whole explanation.
Fix it on Apache
Current Apache (2.4.8 and later) can take the same concatenated file in SSLCertificateFile: leaf, then intermediates, toward the root. The Apache mod_ssl documentation says this support is what made SSLCertificateChainFile obsolete. You may still see SSLCertificateChainFile in older examples. On a current 2.4 server, prefer the single file in SSLCertificateFile, leaf first, and SSLCertificateKeyFile for the key.
SSLCertificateFile /etc/ssl/certs/fullchain.pem
SSLCertificateKeyFile /etc/ssl/private/privkey.pem
Reload Apache after the config test succeeds. A chain file that is not referenced, or a cert.pem left over from a partial paste, will keep serving the leaf alone.
Fix it on IIS
IIS builds the list it sends from the certificate stores on the server, in the local computer account. Microsoft's IIS guidance says the complete chain except the root is what gets sent, and that intermediates belong in the Intermediate Certification Authorities store of the local computer. Importing the leaf into the personal store, and leaving the intermediate only inside a ZIP you downloaded, produces the same incomplete handshake.
Using Microsoft Management Console for the local computer account, import the relevant intermediate certificates into Intermediate Certification Authorities → Certificates. Confirm the leaf in the personal store has its private key, then bind that certificate on the site. Re-test from a fresh client. Restart IIS or Windows only if the documentation for that installed version, or observed behavior after the documented import, specifically requires it. IIS chooses the chain from the stores; it does not read a fullchain.pem line from a config file the way nginx does.
After the reload
Run openssl s_client again. Certificate 0 is the leaf; when an intermediate is served next, its subject matches the leaf's issuer; and the verify error you were chasing is gone. Then hit the hostname from a client that does not share your laptop's certificate cache. A browser that cached the intermediate yesterday will hide the bug you just fixed, and it will also hide the bug if the fix did not stick.
If you run a lot of client hostnames, a one-off openssl session will not be there the day someone deploys cert.pem by habit. A scheduled check that reports CHAIN_INCOMPLETE is the durable version of the same observation.
Domainvane checks port 443 on the hostnames you add and will raise that status when the chain does not validate. It will not edit nginx for you. Use the served chain and verification error to identify the actual fix.
Sources
Checked 2026-09-25.
- RFC 5246, section 7.4.2 (sender's certificate first; each following certificate certifies the previous; root may be omitted). https://www.rfc-editor.org/rfc/rfc5246#section-7.4.2
- nginx, Configuring HTTPS servers (certificate chains, concatenation order,
openssl s_client). https://nginx.org/en/docs/http/configuring_https_servers.html - nginx,
ssl_certificate(primary certificate, then intermediates, in one file). https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate - Apache
mod_ssl,SSLCertificateFile(intermediates in the server certificate file, leaf toward root, supported in 2.4.8 and later;SSLCertificateChainFileobsolete). https://httpd.apache.org/docs/trunk/mod/mod_ssl.html#sslcertificatefile - Certbot, Backwards Compatibility (stable names:
cert.pem,chain.pem,fullchain.pem,privkey.pem). https://certbot.eff.org/docs/compatibility.html - Microsoft, Configure intermediate certificates in IIS (intermediates in the local computer Intermediate Certification Authorities store; server sends the chain except the root). https://learn.microsoft.com/en-us/troubleshoot/developer/webapps/iis/www-authentication-authorization/configure-intermediate-certificates