“SSL handshake failed” means the client and server couldn’t establish a TLS connection. The handshake — where they negotiate cipher suites, exchange certificates, and derive session keys — broke at some point. The connection is refused before any application data flows.
This is one of the most common TLS errors, and it has dozens of possible causes. Here’s a systematic approach to diagnosing and fixing it.
Quick Diagnosis
# Step 1: Get the actual error details
openssl s_client -connect yourserver.com:443 -servername yourserver.com
# Look for: "SSL handshake has read 0 bytes" or specific alert codes
# Step 2: Check what the server supports
nmap --script ssl-enum-ciphers -p 443 yourserver.com
# Step 3: Check certificate validity
echo | openssl s_client -connect yourserver.com:443 2>/dev/null | openssl x509 -noout -dates -subject -issuer
Cause 1: Certificate Expired
Error: certificate has expired or CERT_HAS_EXPIRED
Diagnosis:
echo | openssl s_client -connect server:443 2>/dev/null | openssl x509 -noout -enddate
# notAfter=Mar 15 00:00:00 2026 GMT ← if this date is in the past, cert is expired
Fix:
# Renew the certificate
certbot renew --force-renewal
# Or request a new one from your CA
# Reload the service
systemctl reload nginx
Prevention: Automated renewal (ACME/cert-manager) + expiry monitoring with alerts at 30/14/7 days.
Cause 2: Incomplete Certificate Chain
Error: unable to verify the first certificate or unknown CA
Diagnosis:
openssl s_client -connect server:443 -servername server
# Look at "Certificate chain" section
# If only depth=0 is shown (no intermediate), chain is incomplete
Fix (Nginx):
# Combine leaf + intermediate into fullchain
cat server.crt intermediate.crt > fullchain.pem
# Update Nginx config
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/server.key;
# Reload
nginx -t && nginx -s reload
Why Chrome works but curl/Java doesn’t: Chrome fetches missing intermediates via AIA (Authority Information Access). Other clients don’t — they require the server to send the full chain.
Cause 3: Cipher Suite Mismatch
Error: handshake_failure or no shared cipher
Diagnosis:
# Check what server offers
nmap --script ssl-enum-ciphers -p 443 server
# Try connecting with a specific cipher
openssl s_client -connect server:443 -cipher ECDHE-RSA-AES256-GCM-SHA384
Common scenario: Server configured for TLS 1.3 only, but client only supports TLS 1.2. Or server only allows ECDSA ciphers but certificate uses RSA key.
Fix (Nginx):
# Support both TLS 1.2 and 1.3 with broad cipher coverage
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
Cause 4: TLS Version Mismatch
Error: protocol_version alert or unsupported protocol
Diagnosis:
# Test specific TLS versions
openssl s_client -connect server:443 -tls1_2 # Try TLS 1.2
openssl s_client -connect server:443 -tls1_3 # Try TLS 1.3
openssl s_client -connect server:443 -tls1_1 # Try TLS 1.1 (should fail)
Common scenario: Server only allows TLS 1.3, client is Java 8 (no TLS 1.3 support).
Fix: Enable TLS 1.2 as fallback:
ssl_protocols TLSv1.2 TLSv1.3; # Not just TLSv1.3
Cause 5: Hostname Mismatch (SNI)
Error: handshake failure when connecting to a server hosting multiple domains
Diagnosis:
# Connect without SNI — gets default cert
openssl s_client -connect server:443
# Connect with SNI — gets correct cert
openssl s_client -connect server:443 -servername yourdomain.com
# If these return different certificates, SNI is the issue
Common scenario: Client doesn’t send SNI (very old clients, some libraries with incorrect configuration). Server can’t determine which certificate to present.
Fix (client-side): Ensure your HTTP client sends the hostname:
# Python requests — sends SNI automatically
requests.get("https://yourdomain.com")
# If using IP directly, specify SNI:
import ssl
context = ssl.create_default_context()
conn = context.wrap_socket(sock, server_hostname="yourdomain.com")
Cause 6: Client Certificate Required (mTLS)
Error: certificate required or bad certificate
Diagnosis:
openssl s_client -connect server:443
# Look for: "Acceptable client certificate CA names"
# If present, server requires a client certificate
Fix: Provide the client certificate:
openssl s_client -connect server:443 \
-cert client.pem -key client-key.pem
# Or in curl:
curl --cert client.pem --key client-key.pem https://server/api
Cause 7: Certificate Revoked
Error: certificate revoked or OCSP response shows “revoked”
Diagnosis:
# Check OCSP status
openssl ocsp -issuer intermediate.pem -cert server.pem \
-url http://ocsp.ca.com -resp_text
Fix: Request a new certificate from your CA. A revoked certificate cannot be un-revoked.
Cause 8: System Clock Wrong
Error: certificate is not yet valid or certificate has expired (but cert is actually valid)
Diagnosis:
# Check system time
date
# If significantly wrong (hours/days off), certificates appear invalid
Fix:
# Sync system clock
timedatectl set-ntp true
systemctl restart systemd-timesyncd
# Or: ntpdate pool.ntp.org
Cause 9: Firewall/Middlebox Interference
Error: Intermittent handshake failures, works from some networks but not others
Diagnosis:
# Test from different network paths
# If it works directly but fails through corporate proxy/firewall:
# The middlebox is interfering with TLS
# Check if a proxy is intercepting
openssl s_client -connect server:443 | grep "issuer"
# If issuer is your corporate proxy CA (not the real CA), traffic is being intercepted
Fix: Work with your network team to:
- Update middlebox firmware (TLS 1.3 support)
- Whitelist the destination from TLS inspection
- Ensure the middlebox’s CA certificate is in client trust stores
Systematic Debugging Checklist
□ Can you reach the server at all? (telnet server 443)
□ What TLS versions does the server support? (nmap ssl-enum-ciphers)
□ Is the certificate valid? (not expired, correct hostname, complete chain)
□ Does the client support the server's cipher suites?
□ Is SNI being sent correctly?
□ Is a client certificate required?
□ Is the system clock correct on both sides?
□ Is there a proxy/firewall in the path?
□ Does it work from a different network/machine?
FAQ
Q: “SSL handshake failed” vs “TLS handshake failed” — is there a difference? A: No. Same error, different naming. The actual protocol is TLS (SSL is dead). Error messages use both terms interchangeably.
Q: Why does it work in my browser but fail in curl/Java/Python? A: Browsers are more forgiving: they fetch missing intermediates (AIA), try multiple TLS versions, and have larger cipher suite lists. Programmatic clients are strict — they need the full chain, correct SNI, and matching cipher suites.
Q: The handshake was working yesterday. What changed? A: Most likely: certificate expired (check dates), intermediate CA certificate expired (check the full chain), or a configuration change (server update, firewall rule change, cipher suite modification).
Q: How do I fix this for a service I don’t control? A: If the remote server has the problem (expired cert, incomplete chain), you can’t fix it — contact their admin. If it’s a client-side issue (your client doesn’t support their TLS version), update your client libraries or configure compatible cipher suites.