Bridge SSL

SSL handshake failed: causes and how to fix it

11 min readUpdated

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_MISMATCH and 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]:

What the error means
Alert (number)Meaning in RFC 8446OpenSSL prints
handshake_failure (40)the sender could not negotiate an acceptable set of security parameterssslv3 alert handshake failure
protocol_version (70)the version the peer tried is recognized but not supportedtlsv1 alert protocol version
unrecognized_name (112)the server has nothing for the name the client sent in server_nametlsv1 unrecognized name
unknown_ca (48)the certificate chain did not lead to an authority the receiver knowstlsv1 alert unknown ca
bad_certificate (42)a certificate was corrupt or its signatures did not verifysslv3 alert bad certificate
certificate_required (116)the server wanted a client certificate and got nonetlsv13 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]:

If you see it in a browser
Server faultChromeFirefox, on the Secure Connection Failed page
no shared cipherERR_SSL_VERSION_OR_CIPHER_MISMATCHSSL_ERROR_NO_CYPHER_OVERLAP
client certificate required, TLS 1.2ERR_BAD_SSL_CLIENT_AUTH_CERTSSL_ERROR_HANDSHAKE_FAILURE_ALERT
client certificate required, TLS 1.3—SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT
unknown hostnameERR_SSL_UNRECOGNIZED_NAME_ALERTSSL_ERROR_UNRECOGNIZED_NAME_ALERT

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, from openssl s_client -tls1_2 and 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, from openssl s_client and 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 send handshake_failure instead (RFC 5246, section 7.4.6) [2], and our test server did.

  • Client: tlsv13 alert certificate required, or sslv3 alert handshake failure under 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

  1. IETF, RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3, sections 2, 4.4.2.4 and 6 (alerts).

  2. IETF, RFC 5246, The TLS Protocol Version 1.2, section 7.4.6.

  3. IETF, RFC 6066, TLS Extensions, section 3 (server_name).

  4. OpenSSL 3.0, ssl/ssl_err.c and crypto/err/openssl.txt (error messages); openssl-s_client manual page (-servername, -noservername, -msg, -showcerts).

  5. Chromium, net/base/net_error_list.h; Mozilla NSS, lib/ssl/SSLerrs.h.

  6. OpenJDK, sun/security/ssl/Alert.java; Go, crypto/tls/alert.go and net/http/transport.go.

  7. Our test setup: OpenSSL 3.0 servers and clients, curl, Python, Chrome and Firefox, with certificates from a test authority of our own.

  8. curl, libcurl error codes: CURLE_SSL_CONNECT_ERROR (35), CURLE_RECV_ERROR (56).