SSL handshake failed — diagnose and fix certificate errors
Quick answer: An SSL/TLS handshake failure means the client and server couldn't establish a secure connection. The usual causes are an expired certificate, a missing intermediate chain, a hostname that doesn't match the cert, or a protocol/cipher mismatch. Diagnose it precisely with openssl s_client -connect host:443 -servername host — it shows the chain, expiry, and exactly where the handshake breaks.
What the error looks like
It surfaces differently per client, but all mean the secure channel never formed:
curl: (35) error:0A000126:SSL routines::unexpected eof
curl: (60) SSL certificate problem: unable to get local issuer certificate
# browser:
ERR_SSL_PROTOCOL_ERROR
# node:
Error: certificate has expired (code: CERT_HAS_EXPIRED)
The handshake is where the cert, chain, hostname, and protocol are all checked. Each failure mode has a distinct fingerprint — so read the exact message rather than guessing.
Why it happens
Expired certificate
The cert's validity window has passed. The fix is renewal, often an automation that lapsed.
Missing intermediate chain
The server sends only the leaf cert; clients can't build a path to a trusted root.
Hostname mismatch
The cert's SAN doesn't include the hostname being requested.
Protocol or cipher mismatch
The client requires a TLS version or cipher the server disabled (e.g. old TLS 1.0/1.1).
Diagnose it in three steps
Inspect the live handshake
openssl s_client -connect example.com:443 -servername example.com
# shows the cert chain, 'Verify return code', and where it stopsCheck expiry and chain
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -issuerConfirm hostname and protocol
# Does the SAN list include the host? Is TLS 1.2/1.3 offered?
openssl s_client -connect example.com:443 -tls1_2Serve the full chain, valid and matched
Most handshake failures come down to the certificate you serve. Use the full-chain file (leaf + intermediates), keep renewal automated so it never expires, make sure the hostname is in the SAN, and offer modern TLS:
# nginx: serve fullchain (not just the leaf cert)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
You own TLS — we sign what we send
Your proxy and certificates are yours to run — Infraveil doesn't terminate your TLS. What it does, on your own servers, is sign and verify the traffic between its agent and the control plane (the agent even verifies Infraveil's own signatures), so the channel it controls is provably authentic. Trust by inspection, not assertion.
Frequently asked questions
What causes an SSL handshake to fail?
The client and server couldn't agree on a secure connection — commonly an expired certificate, a missing intermediate chain, a hostname that doesn't match the cert, or a TLS version/cipher mismatch.
How do I debug a TLS handshake?
Use openssl s_client -connect host:443 -servername host to see the certificate chain, validity dates, and the verify return code that pinpoints the failure.
What is a missing intermediate certificate?
The server sent only its leaf certificate, so clients can't build a trust path to a root CA. Serve the full chain (fullchain.pem) including intermediates.
Why does it work in my browser but fail in curl or Node?
Browsers sometimes cache intermediates from prior sites, masking a missing chain. curl and Node don't, so they fail — which means the server really is misconfigured. Serve the full chain.