Skip to main content

    SSL Handshake Failed: Find the Cause Before You Change Anything

    A failed TLS handshake names its own cause. Read the alert code or your client's error string, find the real fault, and fix it without weakening TLS.

    MS
    My-SSL Security Team
    ·
    13 min read
    ·
    Published September 21, 2026
    ·
    Last updated September 21, 2026

    The short answer

    SSL handshake failed means the two sides gave up before agreeing how to encrypt the connection, so no request was ever sent. The failure is not generic: the side that gave up almost always sends a numbered TLS alert, and that number names the fault — 40 handshake_failure, 48 unknown_ca, 70 protocol_version, 71 insufficient_security, 112 unrecognized_name. Read the alert or your client's error string before touching a setting. The fixes that skip that step — disabling verification, turning old protocols back on — remove the protection instead of the fault.

    The four stages of a TLS handshake and the alerts raised at each oneFour stages run left to right. Stage one is TCP and ClientHello, where the client offers versions, ciphers and a server name. Stage two is ServerHello, where the server picks a version and a cipher suite. Stage three is the certificate, where the server sends its chain and the client verifies it. Stage four is key exchange, where signatures are checked and the handshake finishes. Beneath each stage is the set of failures seen there. Stage one shows no TLS on the port, connection reset, timeout or firewall, and a CDN error 525. Stage two shows alert 40 handshake failure, alert 70 protocol version, alert 71 insufficient security and alert 112 unrecognized name. Stage three shows alert 48 unknown CA, alert 45 certificate expired, alert 42 bad certificate and alert 46 certificate unknown. Stage four shows alert 51 decrypt error, alert 49 access denied and alert 44 certificate revoked. A band across the bottom states that the stage the handshake dies at narrows the cause before any setting is changed, and that an alert arriving at you was sent by the other side about what it received from you.A handshake dies at exactly one stage. The alert number tells you which.1. TCP + ClientHelloclient offers versions,ciphers and a name2. ServerHelloserver picks a versionand a cipher suite3. Certificateserver sends its chain,client verifies it4. Key exchangesignatures checked,handshake finishesNEVER REACHED TLSno TLS on the portconnection resettimeout or firewallCDN: error 525NO COMMON GROUND40 handshake_failure70 protocol_version71 insufficient_security112 unrecognized_nameno certificate involvedCHAIN REJECTED48 unknown_ca45 certificate_expired42 bad_certificate46 certificate_unknownPROOF FAILED51 decrypt_error49 access_denied44 certificate_revokedusually mutual TLSAn alert that arrives at you was sent by the other side, about what it received from you.So an unknown_ca you receive means the peer distrusts your chain — not that yourtrust store is the thing that needs changing.
    Half the time lost on handshake failures goes to inspecting certificates for a failure that happened one stage earlier, where no certificate had been sent yet.

    What a handshake failure actually is

    A TLS handshake is the negotiation that happens before any request is sent: the client proposes protocol versions, cipher suites and a hostname, the server picks from what it was offered, sends a certificate chain, and both sides prove they hold the keys they claim to. A handshake failure means that negotiation was abandoned. Nothing was encrypted badly, because nothing was encrypted at all.

    That distinction is the reason so much debugging time goes nowhere. “SSL handshake failed” reads like a certificate error, so the certificate is the first thing anyone looks at — and in a large share of cases the connection died before the certificate was ever sent. A protocol version mismatch, a cipher suite mismatch and an unreachable port all surface under the same sentence.

    What makes this tractable is that TLS is unusually talkative about its own failures. When one side gives up it is supposed to send an alert record carrying a numbered description, and those numbers are standardised in RFC 8446 for TLS 1.3 and RFC 5246 for TLS 1.2. Your client almost certainly printed one. It is usually buried in the middle of a long error string, which is why it gets skipped.

    Read the alert code before you change anything

    Every fatal TLS alert carries a number defined in the RFC 8446 alert registry, and that number narrows the cause to a single handshake stage. Alerts 40, 70 and 71 mean the two sides found no common version or cipher. Alerts 42 through 48 mean a certificate was received and rejected. Alerts 49 and 51 mean key or access checks failed, usually under mutual TLS.

    One rule governs how to read any of them, and it is the rule most often got backwards: an alert that arrives at you was sent by the other side, about what it received from you. If your server logs an incoming unknown_ca, the client distrusts the chain your server sent. Changing your server's trust store will not help. The same alert logged by your client means the opposite, and the fix lives somewhere else entirely.

    AlertWhat the sender meansWhere to look
    40 handshake_failureCould not negotiate an acceptable set of security parametersVersion or cipher overlap; a server with no certificate usable for what was asked
    42 bad_certificateA certificate was corrupt, or its signature did not verifyA truncated or wrongly concatenated PEM file; in mutual TLS, the client certificate
    44 certificate_revokedThe certificate was revoked by its issuerReissue is the only fix; check why revocation happened before reordering
    45 certificate_expiredA certificate in the chain is outside its validity windowCheck the intermediate too, not only the leaf; check the verifier's clock
    46 certificate_unknownSome other problem made the certificate unacceptableOften a name mismatch or a missing key usage; the catch-all of the certificate alerts
    48 unknown_caA chain was received but could not be matched to a trusted issuerA missing intermediate, a private CA, or a stale trust store
    49 access_deniedThe certificate was valid but the peer declined to proceedClient-certificate access rules; a valid identity that is not authorised
    51 decrypt_errorA signature or key-exchange verification failedIn mutual TLS, a client key that does not match the client certificate
    70 protocol_versionThe offered protocol version is recognised but not supportedA client stuck on TLS 1.0/1.1, or a server that has not enabled TLS 1.2+
    71 insufficient_securityThe server requires stronger parameters than the client offeredA hardened server policy meeting a legacy client; more specific than alert 40
    112 unrecognized_nameThe server did not recognise the name sent in SNI (RFC 6066)A missing or wrong SNI value; a virtual host that is not configured

    Two alerts on that list are worth treating as good news. Alert 70 and alert 71 say the same thing as alert 40 but with more precision, so a peer that sends one of them has saved you a round of guessing. Alert 40 is the least specific of the three, and it is also by far the most common, which is why the next step is to reproduce the failure somewhere you can see both sides.

    Diagnosing it in four commands

    Four openssl s_client runs isolate almost every handshake failure. The first establishes whether TLS works at all from your network. The second pins a protocol version to test for a version mismatch. The third shows exactly which certificates the server sends. The fourth turns a quiet verification warning into a hard error you cannot overlook.

    bash
    # 1. Does it negotiate at all, and with what?
    openssl s_client -connect example.com:443 -servername example.com </dev/null
    
    # 2. Pin a version to test for a version mismatch
    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
    
    # 3. What does the server actually send? (chain order and completeness)
    openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
    
    # 4. Make verification failures fatal instead of advisory
    openssl s_client -connect example.com:443 -servername example.com \
      -verify_return_error </dev/null

    Three lines in that output carry most of the answer. Protocol and Cipher tell you what was agreed, if anything. Verify return code tells you how the chain fared: 0 (ok) is a pass, 21 (unable to verify the first certificate) is the signature of a missing intermediate, and 10 (certificate has expired) needs no translation. The list of certificates under Certificate chain shows what the server sent, in order.

    The most useful thing this gives you is a bisection. If openssl s_client completes a handshake from the same machine where your application fails, the server is negotiating correctly and the fault is in your application — its trust store, its pinned protocol versions, its cipher list, or a proxy it is configured to use. If s_client fails too, the fault is on the server side or in the network between you. That one comparison eliminates an entire half of the search space, and it takes about ten seconds.

    When you want the same picture from outside your own network — which matters, because a corporate proxy that intercepts TLS will change the answer — you can check what chain your server serves to the public internet and compare it against what you saw locally.

    What your client's error string is telling you

    Every TLS stack words the same failures differently, which is why the same server misconfiguration is filed as four unrelated bugs by four teams. The strings below map back to two causes between them: a peer that sent a fatal alert, or a chain that could not be verified locally. Recognising which one you have decides whether you look at the server or at the client.

    ClientWhat you seeWhat it means
    curl(35) ... alert handshake failureThe peer sent alert 40 — version, cipher or certificate-selection mismatch
    curl(60) unable to get local issuer certificateVerification failed locally — usually a missing intermediate on the server
    JavaPKIX path building failed ... unable to find valid certification pathSame missing intermediate, or an issuer absent from the JDK trust store
    JavaReceived fatal alert: handshake_failureThe peer sent alert 40; on older JDKs, often an unsupported modern cipher
    PythonCERTIFICATE_VERIFY_FAILEDLocal verification failed; the trailing text names the specific reason
    Gox509: certificate signed by unknown authorityChain could not be completed; Go performs no AIA fetching
    ChromeERR_SSL_VERSION_OR_CIPHER_MISMATCHNo shared version or cipher — the browser wording for alert 40 / 70 / 71
    CloudflareError 525 / Error 526The edge-to-origin handshake failed (525) or the origin certificate was rejected (526)

    The intermediate that only browsers forgive

    If a site loads in Chrome but fails from curl, Java, Python or a mobile app, the server is almost certainly sending only its leaf certificate and omitting the intermediate that links it to a trusted root. Browsers paper over this. Desktop Chrome and Edge read the Authority Information Access extension and fetch the missing intermediate from the CA, and Firefox ships a preloaded set of intermediates through Mozilla's Remote Settings rather than fetching them.

    Command-line tools and application runtimes do neither. curl, Java, Python and Go verify only the certificates the server actually presented, so an incomplete chain is a hard failure every time. The result is a server that has been quietly misconfigured for months and looks healthy to everyone who checks it the obvious way.

    Why a missing intermediate certificate fails everywhere except a browserAt the top, three certificates in a chain. The leaf certificate is sent by the server. The intermediate certificate is drawn with a dashed outline and marked as not sent. The root certificate sits in the client's trust store. Below, four rows show what different clients do with that incomplete chain. Desktop Chrome and Edge fetch the missing intermediate through the Authority Information Access extension and the page loads. Firefox uses its preloaded intermediate set and usually loads too. curl, Python and Go fail with an error about being unable to get the local issuer certificate. Java fails with a PKIX path building error. A gold band at the bottom states that the server is misconfigured in all four rows, and that a working browser is not evidence of a working chain.One misconfiguration, four different verdicts.LEAFserver sends thisINTERMEDIATENOT SENTROOTin the trust storethe gap in themiddle is the bugChrome / Edgefetches the intermediate via AIA — page loadsFirefoxuses its preloaded intermediates — usually loadscurl / Python / Gofails: unable to get local issuer certificateJavafails: PKIX path building failedThe server is wrong in all four rows. A working browser is not evidence of a working chain.
    This is the single most common reason a certificate that “works fine” breaks a payment callback, a monitoring probe or a mobile app the week after it is installed.

    The fix is to install the full chain file your CA provides: the leaf first, then each intermediate in order. The root does not need to be sent and adds a wasted round of bytes to every connection, since a client that does not already hold the root has no reason to trust a copy the server hands it. How certificate chains are built and validated covers the ordering rules and the common ways bundles get assembled wrong.

    One habit prevents the whole class of problem: after installing or renewing any certificate, verify it with something that is not a browser. A single curl -v https://yourhost/ catches an incomplete chain in the minute after deployment rather than in a support ticket six weeks later.

    Version and cipher suite mismatches

    A version or cipher mismatch is an empty intersection between two lists, and neither side is malfunctioning. The client offers the versions and cipher suites it is willing to use, the server compares them against its own list, and if nothing appears in both it sends alert 40, 70 or 71 and closes the connection. No certificate is involved, which is why reissuing one changes nothing.

    A version and cipher mismatch is an empty intersection, not a faultOn the left, a panel lists what a modern client offers: TLS 1.3, TLS 1.2, and cipher suites based on ECDHE with AES-GCM. On the right, a panel lists what an old server still accepts: TLS 1.0, TLS 1.1, and cipher suites based on RSA key exchange with CBC and 3DES. Between them a dark box states that the intersection is empty and that this is reported as alert 40, alert 70 or alert 71. A band across the bottom contrasts two responses: re-enabling TLS 1.0 on the client creates the overlap by removing the protection, while upgrading the server's configuration creates the overlap by fixing the fault.Nothing is broken. There is simply no overlap.CLIENT OFFERSTLS 1.3TLS 1.2ECDHE key exchangeAES-GCM, ChaCha20a 2026 defaultSERVER ACCEPTSTLS 1.0TLS 1.1RSA key exchangeCBC, 3DESuntouched since 2016INTERSECTION: EMPTYreported as alert 40,70 or 71Two ways to create an overlap:Re-enable TLS 1.0 on the client — the connection succeeds and the protection is gone.Update the server's TLS configuration — the connection succeeds and the fault is gone.
    Both columns are internally consistent, which is why neither side logs anything that looks like an error beyond the alert itself.

    The two directions this arrives from look identical in the logs and call for opposite fixes. An old client against a modern server — an embedded device, a legacy Java service, a payment terminal — fails because the server dropped TLS 1.0 and 1.1 and the RSA key-exchange ciphers. A modern client against an old server fails for the mirror-image reason. Running the second command in the previous section against each protocol version tells you which situation you are in within a minute.

    Where the old component is the server, the work is a configuration update. Where the old component is a client you do not control, the honest answer is sometimes that it cannot connect to a properly configured endpoint, and the options are to update it or to give it a separate endpoint with a deliberately weaker policy, kept away from anything sensitive. Cipher suites explained sets out what a current suite list should contain and what has been removed from it.

    When the server doesn't recognise the name

    Server Name Indication is how a client tells a server which hostname it wants, before any certificate is chosen. On a host serving many sites from one address, a missing or wrong SNI value means the server picks its default virtual host, which usually holds a certificate for some other name. RFC 6066 says a server that does not recognise the name should either abort with a fatal unrecognized_name(112) alert or continue the handshake.

    That choice is why the symptom is inconsistent. A server that aborts produces a clean alert 112 and an obvious diagnosis. A server that continues hands over the wrong certificate, and the client rejects it a moment later for a name mismatch — the same root cause, reported as a certificate problem. RFC 6066 also notes that sending unrecognized_name at warning level is not recommended, because client behaviour in response to warnings is unpredictable.

    The clients that hit this are the ones that do not send SNI at all: very old runtimes, some monitoring probes, and anything connecting to a bare IP address rather than a hostname. You can reproduce it exactly by dropping -servername from the openssl commands above and watching a different certificate come back. What SNI does and why shared hosting depends on it covers the mechanism in full.

    Cloudflare 525 and the origin leg

    Behind a CDN there are two separate TLS connections: the visitor to the edge, and the edge to your origin server. Cloudflare error 525 refers only to the second. It means the handshake between the edge and your origin never completed, which can only happen in Full or Full (Strict) mode, where the edge is required to speak TLS to the origin. Error 526 is the neighbouring case: the origin did present a certificate and the edge rejected it.

    The two TLS connections behind a CDN, and which one error 525 refers toThree boxes run left to right: the visitor's browser, the CDN edge, and the origin server. The first leg, browser to CDN edge, is marked as normally healthy because the edge presents its own certificate. The second leg, CDN edge to origin, is the one that fails. Two labelled boxes distinguish the failures on that second leg: error 525 means the handshake with the origin never completed, and error 526 means the origin did present a certificate and the edge rejected it. A band across the bottom says to test the second leg directly against the origin address, because the browser never sees that connection.Behind a CDN there are two handshakes. Only one of them is failing.BROWSERsees only leg 1CDN EDGEown certificateORIGINwhere it breaksleg 1leg 2525handshake never completed526certificate was rejectedBoth codes are generated by the edge, about a connection your browser never makes.Test leg 2 yourself, against the origin address, with the real hostname in SNI.
    Turning the proxy off makes a 525 disappear without fixing anything — the origin is still unable to complete a handshake, and now it is serving traffic directly.

    The causes on that leg are the ordinary ones, just somewhere you cannot see from a browser: nothing listening on port 443 at the origin, a firewall that allows the edge on port 80 but not 443, an origin certificate that does not cover the hostname being requested, or no shared protocol version between the edge and an old origin stack. Each is diagnosable with the same openssl commands, pointed at the origin address with the real hostname supplied through -servername.

    Switching the proxy to a mode that does not verify the origin will clear the error and leave the origin exactly as broken as it was, now serving traffic over a connection nobody is checking. What each Cloudflare SSL mode actually does sets out what changes between Flexible, Full and Full (Strict), and why the last one is the only setting that verifies the origin.

    The causes that are new in 2026

    Two industry changes are producing handshake failures this year in setups that worked without modification for years. Both are worth checking early, because neither shows up as anything unusual in the alert code: they present as ordinary certificate rejections and expiry failures, and the troubleshooting guides written before 2026 do not mention either.

    The first is the removal of the clientAuth extended key usage from publicly trusted TLS certificates. Organisations that used a public TLS certificate as the client certificate in a mutual-TLS setup find that the server now rejects it, typically as alert 42, 46 or 49. Certum issued its last SSL/TLS certificates carrying clientAuth on 15 May 2026, DigiCert stopped on 1 May 2026 and Sectigo on 15 May 2026, so the failure arrives at renewal rather than all at once — which makes it look like a broken renewal instead of a policy change. Why public TLS certificates no longer authenticate clients covers how to confirm it and where mutual TLS should move instead.

    The second is shorter certificate lifetimes. Certificates issued on or after 15 March 2026 cap at 200 days under the CA/Browser Forum Baseline Requirements, dropping to 100 days in March 2027 and 47 days in March 2029. An expiry that used to arrive once a year now arrives roughly twice, and any renewal process that depended on somebody remembering will fail sooner and more often. Expiry-driven handshake failures are the most avoidable item on this page and the one most likely to grow.

    If a certificate on the failing host does need replacing rather than reconfiguring, it is worth checking what validation level it holds before reordering, because a DV certificate can be reissued in minutes while an OV or EV certificate with stale organisation data cannot. Compare what DV, OV and EV certificates need at reissue before you are doing it under time pressure.

    The fixes that make it worse

    Every handshake failure has a fix that makes the error message disappear in under a minute, and each one works by removing the check rather than the fault. They are worth naming, because they are the top answers on most search results for this error and they all leave the connection measurably weaker than the developer believes it to be.

    The quick fixWhat it actually doesInstead
    curl -k, verify=FalseAccepts any certificate from anyone, including an interceptorFix the chain on the server, or point the client at the right CA bundle
    A trust-all TrustManagerDisables verification for the whole JVM process, often permanentlyImport the actual issuing CA into a keystore the application points at
    Re-enabling TLS 1.0 / 1.1Restores protocols deprecated across browsers since 2020 and disallowed under PCI DSSUpdate the component that cannot do TLS 1.2, or isolate it behind its own endpoint
    Importing the leaf into the trust storeWorks until the next renewal, then fails again with no obvious link to the changeServe the missing intermediate from the server, where the problem is
    Switching the CDN off verificationHides a broken origin and leaves the edge-to-origin hop unauthenticatedFix the origin handshake, then return the proxy to Full (Strict)

    There is one legitimate use for the first row, and it is diagnostic. If curl -k succeeds where curl fails, you have proved the failure is verification rather than negotiation, which is genuinely useful information. The mistake is leaving the flag in the script that goes to production.

    Frequently Asked Questions

    Get instant answers to common questions about SSL certificates and our services.

    Still Have Questions?

    Our SSL experts are available 24/7 to help with any questions about certificates, installation, or technical issues.

    Worth checking while this is fresh

    An incomplete chain and an approaching expiry both look fine in a browser right up to the moment something that is not a browser tries to connect. Both take about thirty seconds to rule out. Check what your server actually serves — the chain it sends, the names it covers and the date it expires — for the host that just failed, and for the one you renewed last month and have not tested since.

    Related reading