← Back to Blog
troubleshooting

SSL Handshake Failed: Causes, Diagnosis, and Fixes (2026)

An SSL handshake failure occurs when the client (browser or API consumer) and server can't agree on the parameters needed to establish an encrypted connection. The handshake itself is a multi-step negotiation — protocol version, cipher suite, certificate verification — and it can fail at any of those steps. The exact cause determines the fix.

🔍

TLS Scanner

Scan your server for TLS version support and cipher suite issues.

Try It Free →

Step 1: Diagnose with OpenSSL

OpenSSL is the fastest way to pinpoint the cause. Run these commands and compare output:

Basic Handshake Test

openssl s_client -connect yourdomain.com:443 -servername yourdomain.com # Key lines to look at in output: SSL-Session: Protocol : TLSv1.3 ← good Cipher : TLS_AES_256_GCM_SHA384 ← negotiated cipher Verify return code: 0 (ok) ← certificate chain is valid Verify return code: 20 (unable to get local issuer certificate) ← chain incomplete Verify return code: 10 (certificate has expired) ← cert expired

Test Specific TLS Versions

# Test if TLS 1.3 works openssl s_client -connect yourdomain.com:443 -tls1_3 -servername yourdomain.com # Test if TLS 1.2 works openssl s_client -connect yourdomain.com:443 -tls1_2 -servername yourdomain.com # If TLS 1.3 fails but 1.2 works: server doesn't support TLS 1.3 yet # If both fail: cipher or certificate issue

Test Specific Cipher Suites

# List what ciphers the server supports nmap --script ssl-enum-ciphers -p 443 yourdomain.com # Or test a specific cipher: openssl s_client -connect yourdomain.com:443 -cipher "ECDHE-RSA-AES256-GCM-SHA384"
# Example output when cipher mismatch causes handshake failure: $ openssl s_client -connect yourdomain.com:443 -tls1_3 140584647214912:error:1409442E:SSL routines:ssl3_read_bytes: tlsv1 alert protocol version:../ssl/record/rec_layer_s3.c:1544: SSL alert number 70 # "alert protocol version" = server and client have no TLS version in common

Fix #1: Protocol Mismatch (Server Enables Old TLS Only)

If your server is still on TLS 1.0 or 1.1 only, modern browsers will refuse to connect. The fix is enabling TLS 1.2 and TLS 1.3 in your server config.

Apache — Enable TLS 1.2 + 1.3

# In /etc/apache2/sites-available/yourdomain.conf (inside VirtualHost *:443): SSLProtocol -all +TLSv1.2 +TLSv1.3 SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:\ ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:\ TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 SSLHonorCipherOrder off # Let client choose; TLS 1.3 ignores this anyway # Test config then reload: sudo apachectl configtest && sudo systemctl reload apache2

Nginx — Enable TLS 1.2 + 1.3

# In /etc/nginx/sites-available/yourdomain.conf (inside server block): ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:\ ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # Reload: sudo nginx -t && sudo systemctl reload nginx

Fix #2: Incomplete Certificate Chain

The server sends your certificate but not the intermediate certificate the CA issued it from. The client can't build a trusted path to a root CA.

# Check chain with OpenSSL: openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>&1 | grep -E "Chain|issuer|subject" # If chain is incomplete, you see: CONNECTED(00000003) depth=0 CN=yourdomain.com ← only server cert, no intermediate # Correct output should show depth=1 (intermediate) and depth=2 (root): depth=2 CN=ISRG Root X1 depth=1 CN=R3, O=Let's Encrypt depth=0 CN=yourdomain.com

Fix: Create a Combined Certificate File

# Concatenate your cert + intermediate (CA chain) into one file: cat yourdomain.crt intermediate.crt > yourdomain-bundle.crt # Apache: point SSLCertificateFile to the bundle SSLCertificateFile /etc/ssl/yourdomain-bundle.crt # Nginx: same — ssl_certificate should include the full chain ssl_certificate /etc/ssl/yourdomain-bundle.crt;

Fix #3: Clock Skew

If the client's system clock is more than a few minutes off, the TLS library will reject the certificate as expired or not yet valid. This is common on servers without NTP configured or VMs that lost time sync after a suspend/resume.

# Check current time vs certificate validity: openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates notBefore=Jun 15 00:00:00 2026 GMT notAfter=Sep 13 23:59:59 2026 GMT # Enable NTP on your server (Ubuntu/Debian): sudo timedatectl set-ntp true timedatectl status # should show "synchronized: yes"

Fix #4: Cipher Mismatch Between Client and Server

If your server only allows old cipher suites (like RC4 or 3DES) and the client has deprecated them, no common cipher is available. The solution is updating to modern AEAD ciphers.

# Verify what ciphers are currently accepted (nmap approach): nmap --script ssl-enum-ciphers -p 443 yourdomain.com 2>/dev/null | grep -E "TLS_|ciphers" # Recommended modern cipher list (Apache/Nginx): # TLS_AES_256_GCM_SHA384 (TLS 1.3 only) # TLS_CHACHA20_POLY1305_SHA256 (TLS 1.3 only) # ECDHE-RSA-AES256-GCM-SHA384 (TLS 1.2) # ECDHE-RSA-AES128-GCM-SHA256 (TLS 1.2) # Remove these deprecated ciphers if present: # RC4-SHA, DES-CBC3-SHA, AES128-SHA (no ECDHE = no forward secrecy)

Verify the Fix

After any configuration change, confirm the handshake now completes successfully:

$ openssl s_client -connect yourdomain.com:443 -servername yourdomain.com < /dev/null 2>&1 | tail -6 New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 Server public key is 2048 bit Post-Handshake Auth: No Compression: NONE Expansion: NONE Verify return code: 0 (ok)
Tip: Run your domain through the SSL Checker after making changes. It tests TLS protocol support, cipher strength, certificate validity, and chain completeness in one pass without needing shell access.

FAQs

  • The handshake fails only for some users, not all. Why?
    Different client environments support different TLS versions and ciphers. If old iOS or Android devices fail but Chrome on desktop works, your server likely supports TLS 1.2 but is missing some cipher suites that older mobile TLS stacks require. Enabling ECDHE-RSA-AES128-GCM-SHA256 and ECDHE-RSA-AES256-GCM-SHA384 covers the vast majority of clients.
  • The SSL Checker shows everything is fine but my Java application still fails the handshake. Why?
    Java maintains its own TLS trust store separate from the OS. Older JDK versions (before JDK 11) don't trust Let's Encrypt's intermediate CA (R3). Update your JDK to 11+, or manually import the Let's Encrypt Root CA into your Java keystore with keytool -import.
  • Handshake fails on port 443 but works on port 8443. What does that mean?
    The two ports likely have different SSL configurations on your server. Check that port 443's VirtualHost or server block has the same SSL directives as 8443, including the SSLProtocol, SSLCipherSuite, and certificate path directives.

Diagnosing SSL Handshake Failures on Windows

Windows Server IIS / PowerShell

SSL handshake failures on Windows generate Event ID 36888 (fatal alert) or 36874 (protocol version) in the Windows Event Log. Check these first before running network-level diagnostics.

Check Windows Event Log for TLS Errors

# Filter Event Log for Schannel errors (run in elevated PowerShell) Get-EventLog -LogName System -Source "Schannel" -Newest 20 | ` Where-Object {$_.EntryType -eq "Error"} | ` Select-Object TimeGenerated, EventID, Message | ` Format-List # Key Event IDs: # 36888 — Fatal SSL alert received (e.g., handshake failure, protocol version) # 36874 — TLS protocol version not supported by the client # 36882 — Certificate not found / certificate mismatch

Test TLS Handshake with PowerShell

# Test TLS handshake to a remote server $domain = "yourdomain.com" $port = 443 try { $tcp = New-Object System.Net.Sockets.TcpClient($domain, $port) $ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, ({param($s,$c,$ch,$e) $true})) # Accept any cert for diagnostics $ssl.AuthenticateAsClient($domain) Write-Host "Handshake succeeded" Write-Host "TLS version: $($ssl.SslProtocol)" Write-Host "Cipher: $($ssl.CipherAlgorithm)" Write-Host "Cert subject: $($ssl.RemoteCertificate.Subject)" $ssl.Close() } catch { Write-Host "Handshake FAILED: $_" } $tcp.Close()

Test with OpenSSL on Windows

:: Install OpenSSL first: winget install ShiningLight.OpenSSL :: Full handshake diagnostics — shows TLS version, cipher, cert chain openssl s_client -connect yourdomain.com:443 -servername yourdomain.com :: Test specific TLS versions openssl s_client -connect yourdomain.com:443 -tls1_2 :: Test TLS 1.2 openssl s_client -connect yourdomain.com:443 -tls1_3 :: Test TLS 1.3 :: Check if server accepts any cipher at all openssl s_client -connect yourdomain.com:443 -cipher "ALL:COMPLEMENTOFALL"

Fix: Cipher Suite Mismatch on Windows

Windows clients negotiate ciphers using the Schannel cipher list. If the server requires a cipher that the Windows client does not support, the handshake fails with "No suitable cipher suites."

# List Windows cipher suites (PowerShell) Get-TlsCipherSuite | Select-Object Name, Exchange, Cipher, Hash | Format-Table -AutoSize # Enable a specific cipher suite if it's missing Enable-TlsCipherSuite -Name "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" # Disable a weak cipher suite Disable-TlsCipherSuite -Name "TLS_RSA_WITH_3DES_EDE_CBC_SHA"

Fix: Certificate Binding Issue in IIS

If the certificate exists in the store but IIS shows a handshake error, the binding may be corrupt. Rebuild it:

# Remove and re-add the HTTPS binding Import-Module WebAdministration $siteName = "Default Web Site" Remove-WebBinding -Name $siteName -Protocol https -Port 443 New-WebBinding -Name $siteName -Protocol https -Port 443 -HostHeader "yourdomain.com" # Re-assign certificate $thumbprint = "YOUR_CERT_THUMBPRINT" $binding = Get-WebBinding -Name $siteName -Protocol https $binding.AddSslCertificate($thumbprint, "My") # If that fails, reset via netsh: netsh http delete sslcert ipport=0.0.0.0:443 $guid = [guid]::NewGuid().ToString("B") netsh http add sslcert ipport=0.0.0.0:443 certhash=$thumbprint appid="$guid" certstorename=MY

Diagnosing SSL Handshake Failures on macOS

macOS Terminal
# Diagnose SSL handshake with verbose output openssl s_client -connect yourdomain.com:443 -servername yourdomain.com -debug 2>&1 | head -50 # Test specific TLS versions (use Homebrew openssl for -tls1_3 support) brew install openssl /opt/homebrew/opt/openssl/bin/openssl s_client -connect yourdomain.com:443 -tls1_2 /opt/homebrew/opt/openssl/bin/openssl s_client -connect yourdomain.com:443 -tls1_3 # Check curl TLS handshake (macOS curl uses LibreSSL/Secure Transport) curl -v https://yourdomain.com 2>&1 | grep -E "TLS|SSL|cipher|error" # Check if a specific cipher is accepted openssl s_client -connect yourdomain.com:443 -cipher "ECDHE-RSA-AES256-GCM-SHA384"
# For Homebrew Apache/Nginx server-side errors, check logs: # Apache error log: tail -f /opt/homebrew/var/log/httpd/error_log # Nginx error log: tail -f /opt/homebrew/var/log/nginx/error.log

macOS handles TLS at the OS level (Secure Transport on older macOS, Network.framework on macOS 11+). Certificate trust issues also appear in the Console app: open Console.app → search for "TLS" or "SSL" to see system-level TLS diagnostics.


Check your TLS configuration now

The SSL Checker tests protocol support, cipher suites, certificate chain, and handshake success from outside your network.