SSL handshake failed: the step it died on names the cause
Handshake failures get diagnosed badly because the error message is generic and the advice online is a list. Try updating your certificate, try enabling TLS 1.2, try a different cipher suite, try disabling your antivirus. One of those is right and there is no way to tell which from the message.
There is a way to tell from the sequence. A handshake happens in a fixed order, and the place where it stops is a diagnosis, because each step depends on different things.
The order, and what each step needs
| Step | Depends on | Failure looks like |
|---|---|---|
| TCP connect | Routing, firewall, something listening on 443 | Refused, or timed out. No TLS involved at all |
| ClientHello sent | Nothing yet | Silence, or a reset immediately after |
| ServerHello | A shared protocol version and a shared cipher | handshake failure, protocol version, no protocols available |
| Certificate sent | SNI matching a configured site | Wrong certificate, or unrecognized name |
| Certificate verified | Chain completeness, dates, trust store | certificate verify failed, and a reason |
| Client certificate requested | The client having one | bad certificate, or a stall |
| Finished | Everything above | Success |
Two consequences that are worth stating on their own, because they eliminate whole categories of guessing.
A failure before ServerHello has nothing to do with your certificate. The server has not sent one yet. Renewing it, reissuing it, or checking its expiry date is time spent on a component that never entered the conversation.
A failure at verification has nothing to do with ciphers. The cryptography agreed fine. The client does not like the paperwork.
Finding out where it stopped
openssl s_client -connect example.com:443 -servername example.com < /dev/null
Read the output from the top rather than jumping to the error. If you see a certificate chain printed, you got past ServerHello and the problem is verification. If the output stops at a bare alert with no certificate above it, negotiation failed and the certificate is irrelevant.
To see the messages themselves rather than openssl's summary:
openssl s_client -connect example.com:443 -servername example.com -msg < /dev/null 2>&1 | head -30
Then bisect. Each of these changes exactly one variable, and the first one that succeeds names the cause:
openssl s_client -connect example.com:443 -servername example.com -tls1_2 < /dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_3 < /dev/null
openssl s_client -connect example.com:443 < /dev/null
The third one drops SNI. If it succeeds where the others failed, or hands you a certificate for a different domain, the server has no virtual host for the name you asked for and is falling back to a default. That is a configuration problem on the server, not a certificate problem, and it is the case most often misread as a certificate problem.
The two that show up on client work
A hardening pass broke an old client. Somebody disabled TLS 1.0 and 1.1 on the server, correctly, because a compliance scan asked for it. Everything keeps working. Three weeks later a payment provider's callback stops arriving, or a client reports that their office printer can no longer submit scans to the site, or a Windows service that has run for six years fails. The change and the symptom are far enough apart that nobody connects them.
What makes this hard is that the failing party is usually the one you cannot inspect. The tell is that everything modern works and one specific old thing does not, and that the old thing is the same one every time. Ask what the failing client is and how old it is before you touch the server config again.
Your own server is the old client. The mirror image, and just as common on shared hosting. A site on an ageing PHP stack calls an API that has raised its minimum protocol version, and the outbound request fails while the site itself is perfectly healthy. Nothing in your monitoring sees this: the site answers 200, the failure is in a request the site makes to somebody else, and it usually surfaces as a form that silently stops delivering.
curl -v https://api.example.com/ 2>&1 | grep -iE 'SSL connection|TLS|alert|version'
Run that from the client's server, not from your laptop. The whole point is that the two have different libraries.
When it is not TLS at all
Two impostors, both frequent.
A middlebox on the visitor's network, a corporate proxy or an antivirus that inspects traffic, terminates TLS itself and re-signs with its own authority. From the browser's point of view the certificate is issued by something it may or may not trust, and the failure follows the visitor rather than the site. One person cannot reach the site, everyone else can, and nothing you change on the server will help.
And a firewall that resets the connection partway through the handshake produces a message that reads like a protocol failure and is not. The tell is that it happens at the same point every time regardless of which protocol version you force, and that plain HTTP on port 80 behaves normally.
Both belong to the same family as a browser warning that only one user sees, which is one of the eight distinct faults sitting behind the same red interstitial in read the second line. And once you have narrowed it to something about the certificate itself rather than the negotiation, the offline checks that prove a key, a certificate and a chain belong together are in three openssl commands.