SSL handshake failed: causes and how to fix it
SSL handshake failed means a client and a server started to set up an encrypted connection and one of them gave up before it was finished. The side that gives up can send an alert, a short message that names the reason [1]. Read that alert in the error text of curl or another client, or with openssl s_client, and read the server log for the same moment: between them they name the cause. Browsers translate the alert into their own codes, which the SSL errors section explains.
Quick facts
- Where it appears
- server logs, curl, OpenSSL, Python, Java, Go and other non-browser clients
- Typical text
sslv3 alert handshake failure(OpenSSL, curl, Python),Received fatal alert: handshake_failure(Java) [4][6]- Browser equivalents
ERR_SSL_PROTOCOL_ERROR,ERR_SSL_VERSION_OR_CIPHER_MISMATCHand other codes in the table below [5]- What failed
- the TLS handshake, before any application data was sent
- Who fixes it
- whoever controls the side the alert points at: the server for versions, ciphers, hostnames and its own certificate; the client for its trust store, its own certificate and its TLS library
What the error means
Every HTTPS connection begins with a TLS handshake. The client lists the versions and ciphers it supports, the server chooses, sends its certificate and proves it holds the matching key, and both sides derive the keys for the session. If either side cannot continue, it sends an alert and closes the connection.
The alerts are defined in RFC 8446, section 6, each with a name and a number [1]. OpenSSL prints the name in its own wording and adds the number, as in SSL alert number 40 [4][7]:
| Alert (number) | Meaning in RFC 8446 | OpenSSL prints |
|---|---|---|
handshake_failure (40) | the sender could not negotiate an acceptable set of security parameters | sslv3 alert handshake failure |
protocol_version (70) | the version the peer tried is recognized but not supported | tlsv1 alert protocol version |
unrecognized_name (112) | the server has nothing for the name the client sent in server_name | tlsv1 unrecognized name |
unknown_ca (48) | the certificate chain did not lead to an authority the receiver knows | tlsv1 alert unknown ca |
bad_certificate (42) | a certificate was corrupt or its signatures did not verify | sslv3 alert bad certificate |
certificate_required (116) | the server wanted a client certificate and got none | tlsv13 alert certificate required |
The sslv3, tlsv1 and tlsv13 prefixes are part of OpenSSL's message names, not the protocol version in use: in our test, openssl s_client printed sslv3 alert handshake failure in a TLS 1.2 session [7].
If you see it in a browser
A browser does not show the alert name. It shows a code, and the same server faults produced these in our test [5][7]:
| Server fault | Chrome | Firefox, on the Secure Connection Failed page |
|---|---|---|
| no shared cipher | ERR_ | SSL_ |
| client certificate required, TLS 1.2 | ERR_ | SSL_ |
| client certificate required, TLS 1.3 | — | SSL_ |
| unknown hostname | ERR_ | SSL_ |
Chrome's result for the TLS 1.3 case was not conclusive in our setup, so the table leaves it out. Firefox puts every one of these under Secure Connection Failed, with the code underneath. As a visitor, open the site on another network and in another browser: if it fails everywhere, the cause is on the server. Visitor checks for this family of errors are on ERR_SSL_PROTOCOL_ERROR.
Causes on the server, and how to fix each
For each cause: the alert, what the client printed and what the server log said in our test, with an OpenSSL 3.0 server and OpenSSL-based clients [7].
No shared protocol version
The client offers only versions the server has turned off, or the other way round, and the server rejects the ClientHello.
Alert:
protocol_version(70).Client:
tlsv1 alert protocol version, fromopenssl s_client -tls1_2and from curl limited to TLS 1.2.Server log:
unsupported protocol, from a server limited to TLS 1.3.
Enable TLS 1.2 and TLS 1.3 on the server. In Chrome this fault falls under ERR_SSL_VERSION_OR_CIPHER_MISMATCH, defined as no common protocol version or cipher suite [5].
No shared cipher
Both sides support a common version but no common cipher suite.
Alert:
handshake_failure(40).Client:
sslv3 alert handshake failure.Server log:
no shared cipher.
Compare the server's cipher list with what the failing client offers, then adjust the server's list.
The hostname is missing or unmatched
When several sites share an IP address, the server picks the configuration from the hostname the client sends, called SNI. A server that does not recognize the name should either abort with unrecognized_name or carry on without it (RFC 6066, section 3) [3]; our test server was set to abort.
Alert:
unrecognized_name(112).Client:
tlsv1 unrecognized name, fromopenssl s_clientand from curl.Server log:
callback failed, in the step that handles the server name.
Add the hostname to the server's configuration, or make the client send it. openssl s_client fills in the hostname from -connect by default, so testing without it needs -noservername [4].
The certificate is not accepted
The client rejects the server's certificate: it is expired, issued for another name, or does not chain to a root the client trusts. The client states the reason in its own words and alerts the server.
Alert:
unknown_ca(48), for a certificate from an authority the client did not trust.Client:
SSL certificate problem: unable to get local issuer certificate, from curl.Server log:
tlsv1 alert unknown ca.
That client message has its own page, unable to get local issuer certificate, and Chrome reports an untrusted issuer as ERR_CERT_AUTHORITY_INVALID [5].
A client certificate is required
The server asks the client to authenticate with its own certificate, a setup called mutual TLS. A client with no certificate, or one the server does not accept, fails by design, and the alert depends on the version.
Alert:
certificate_required(116) in TLS 1.3 [1]. Under TLS 1.2 the server may sendhandshake_failureinstead (RFC 5246, section 7.4.6) [2], and our test server did.Client:
tlsv13 alert certificate required, orsslv3 alert handshake failureunder TLS 1.2.Server log:
peer did not return a certificate.
In TLS 1.3 the client's certificate travels in its last handshake flight, after the server's [1], so the server's answer arrives once the client has sent everything. curl reported it with exit code 56, a receive error, rather than 35, a handshake error [7][8]. Give the client a certificate the server accepts, or check whether the endpoint should ask for one.
The certificate does not match the private key
A certificate works only with the private key it was issued for. In our test, an OpenSSL server given a certificate with an unrelated private key refused to start, with X509_check_private_key:key values mismatch in its log, and Python refused to load the pair, [X509: KEY_VALUES_MISMATCH] key values mismatch [7]. Both checked the pair at load time, so look for the error after a restart or reload, for example after a reissue that left the old key in place. The certificate and key match check answers in one step.
Something in between
A proxy, firewall or antivirus product that inspects TLS has to end the client's connection and open its own to the server. The client's handshake is then with that device, and the device's versions, ciphers and certificate apply. If the failure follows the network rather than the client or the server, look there.
How to diagnose
Run the handshake from outside the server and read where it stops:
$ openssl s_client -connect example.com:443 -servername example.com -msg </dev/null
$ curl -v https://example.com/The -msg option prints each handshake message and any alert, which shows how far the exchange got and who ended it [4]. In our test, a refused ClientHello came back as <<< TLS 1.2, Alert [length 0002], fatal handshake_failure: >>> marks what the client sent, <<< what it received, and the alert keeps its RFC name [7]. To test versions, add -tls1_2 or -tls1_3 to the first command. To test SNI, run it once with -noservername in place of -servername: if it fails only then, the server depends on the hostname and the failing client is not sending one.
Then narrow it down with three questions, in order. Does every client fail, or only some? Total failure points at the server's configuration or its certificate and key. Does a browser succeed where a script fails? That points at the chain or the client's trust store. Does the failure follow the network? Then something in between is terminating connections.
When you have access to both ends, read both logs. The alert name alone does not always settle it: in our test, handshake_failure reached the client both for a missing shared cipher suite and for a missing client certificate under TLS 1.2, and only the server log, no shared cipher or peer did not return a certificate, told them apart [7].
TLS handshake timeout
A timeout is a different failure: the handshake never finished because messages stopped arriving. Go's HTTP client reports it as net/http: TLS handshake timeout [6]. In our test, a listener that accepted TCP connections and never answered gave curl: (28) SSL connection timeout and, in Python, The handshake operation timed out [7]. Look at the network path, a firewall or a proxy before TLS settings.
How to prevent it
Test from outside after every certificate, server or proxy change, and include a non-browser client such as openssl s_client or curl. Endpoints used by other systems need this test most, because their clients are libraries.
Keep protocol versions and ciphers current, and check them again after either side's TLS library is upgraded. A handshake works only where the two lists overlap, and an upgrade on either side can change its list.
FAQ
What does "SSL handshake failed" mean?
The client and server could not complete the exchange that sets up an encrypted connection. They found no common protocol version or cipher, one side rejected the other's certificate, or something between them interrupted the exchange.
How do I fix "SSL handshake failed"?
Find where it stops with openssl s_client -msg or curl -v, read the alert, and read the server's log. Then fix the matching cause: enable TLS 1.2 and 1.3, adjust the cipher list, serve the full certificate chain, configure the hostname, or supply a client certificate if the server requires one.
What does "sslv3 alert handshake failure" mean?
It is OpenSSL's message for the handshake_failure alert, number 40. The sslv3 prefix is part of the message name and does not mean SSL 3.0 was used. The alert says the sender could not agree on parameters. In our test both a missing shared cipher and, under TLS 1.2, a missing client certificate produced it, and only the server log told which.
Why does it work in the browser but fail from my script?
Check the two things the script depends on: the chain the server sends and the CA file the script trusts. Run openssl s_client -showcerts against the site to see the chain as the server sends it. The page on unable to get local issuer certificate covers the browser-versus-script case in full.
Can a client certificate cause a handshake failure?
Yes. If the server requires mutual TLS, a client that connects without an accepted certificate fails the handshake. In TLS 1.3 the alert is certificate_required; under TLS 1.2 the server may send handshake_failure instead.
What causes a TLS handshake timeout?
Messages stopped arriving before the TLS handshake finished. Check the network path before the TLS settings, looking for a firewall, proxy or load balancer that accepts the connection and passes nothing back. In our test, a listener that accepted the TCP connection and never answered produced it.
Sources
IETF, RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3, sections 2, 4.4.2.4 and 6 (alerts).
IETF, RFC 5246, The TLS Protocol Version 1.2, section 7.4.6.
IETF, RFC 6066, TLS Extensions, section 3 (server_name).
OpenSSL 3.0, ssl/ssl_err.c and crypto/err/openssl.txt (error messages); openssl-s_client manual page (-servername, -noservername, -msg, -showcerts).
Chromium, net/base/net_error_list.h; Mozilla NSS, lib/ssl/SSLerrs.h.
OpenJDK, sun/security/ssl/Alert.java; Go, crypto/tls/alert.go and net/http/transport.go.
Our test setup: OpenSSL 3.0 servers and clients, curl, Python, Chrome and Firefox, with certificates from a test authority of our own.
curl, libcurl error codes: CURLE_SSL_CONNECT_ERROR (35), CURLE_RECV_ERROR (56).