The Error
You hit an HTTPS endpoint and get back:
SSL: HANDSHAKE_FAILURE — SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
Or in curl:
curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to example.com:443
Or in Python:
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443):
Max retries exceeded ... caused by SSLError(SSLError(1, '[SSL: HANDSHAKE_FAILURE]
ssl handshake failure (_ssl.c:997)'))
The TLS handshake is where client and server agree on a cipher suite and exchange certificates. When it breaks, the connection drops immediately — nothing gets through.
What Causes This
Six things account for the vast majority of these failures:
- TLS version mismatch — server requires TLS 1.2+ but client offers only SSLv3/TLS 1.0
- No shared cipher suite — client's cipher list has zero overlap with the server's allowed ciphers
- Outdated OpenSSL — versions below 1.1.1 (EOL December 2019) lack support for ECDHE and AES-GCM suites that most servers now require
- Server requires SNI — client doesn't send Server Name Indication, so the server rejects the handshake outright
- Expired or broken certificate chain — cert is expired or the intermediate CA certificate is missing
- Firewall/proxy interference — a middlebox strips or mangles the ClientHello before it reaches the server
Quick Diagnosis
Don't guess — run these checks first to pinpoint where negotiation breaks.
Step 1 — Check what TLS versions the server supports
# Test specific TLS versions
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# Check if server still allows old versions (should fail on hardened servers)
openssl s_client -connect example.com:443 -ssl3
openssl s_client -connect example.com:443 -tls1
If -tls1_2 connects but -tls1 fails, the server has dropped TLS 1.0 — your client needs to catch up.
Step 2 — Check cipher suite negotiation
openssl s_client -connect example.com:443 -cipher 'ALL' 2>&1 | grep -E 'Cipher|Protocol|Alert'
Step 3 — Verbose curl output
curl -v --tlsv1.2 https://example.com 2>&1 | head -40
Watch for TLSv1.2, Handshake, Client hello followed immediately by alert handshake failure. That sequence confirms the server rejected your ClientHello.
Fix 1 — Force a Compatible TLS Version (Quick Fix)
Most of the time, forcing TLS 1.2 resolves it immediately.
curl
curl --tlsv1.2 https://example.com
# or for TLS 1.3
curl --tlsv1.3 https://example.com
Python requests
import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
class TLS12Adapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = create_urllib3_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount('https://', TLS12Adapter())
response = session.get('https://example.com')
Node.js (https module)
const https = require('https');
const options = {
hostname: 'example.com',
port: 443,
path: '/',
minVersion: 'TLSv1.2', // TLS 1.2 minimum; still allows TLS 1.3
};
https.get(options, (res) => console.log(res.statusCode));
Note: minVersion (Node 12+) is the modern approach. The older secureProtocol: 'TLSv1_2_method' pins exactly TLS 1.2 and blocks TLS 1.3 — avoid it for new code.
Fix 2 — Update OpenSSL and System Libraries
OpenSSL 1.0.2 hit end-of-life in December 2019. Anything below 1.1.1 is missing the ECDHE and AES-GCM cipher suites that modern servers demand.
# Check your current version
openssl version -a
# Ubuntu/Debian
sudo apt update && sudo apt upgrade openssl libssl-dev
# CentOS/RHEL
sudo yum update openssl
# macOS (via Homebrew)
brew upgrade openssl
After upgrading, reinstall or rebuild your Python/Node.js installations too. Both link against the system OpenSSL at compile time and won't pick up the new version automatically.
Fix 3 — Specify Cipher Suites Explicitly
When you control the client, explicitly list at least one cipher the server accepts.
curl with explicit ciphers
curl --tlsv1.2 \
--ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384' \
https://example.com
Python ssl context
import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
CIPHERS = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES128-GCM-SHA256'
class CipherAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
ctx = create_urllib3_context(ciphers=CIPHERS)
kwargs['ssl_context'] = ctx
return super().init_poolmanager(*args, **kwargs)
session = requests.Session()
session.mount('https://', CipherAdapter())
response = session.get('https://example.com')
Fix 4 — SNI Issues (Virtual Hosting)
When a server hosts multiple domains on one IP, it needs the SNI extension to pick the right certificate. Clients that skip SNI get rejected.
# Test with SNI explicitly
openssl s_client -connect example.com:443 -servername example.com
# Without SNI (simulates old clients)
openssl s_client -connect example.com:443 -noservername
First command succeeds, second fails? The server requires SNI. The good news: Python 2.7.9+, Node.js 0.12+, and Java 8u161+ all send SNI automatically. If you're on something older than that, upgrade the runtime — patching SNI support manually isn't worth it.
Fix 5 — Certificate Chain Problems
A missing intermediate certificate on the server is a surprisingly common trigger.
# Check the full cert chain
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -dates
# Validate expiry
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -enddate
If you see verify error:num=20:unable to get local issuer certificate, the server is not sending the intermediate CA cert. This is a server-side fix — contact the admin and ask them to install the full chain.
Verify the Fix
# Successful handshake output looks like:
openssl s_client -connect example.com:443 -tls1_2
# ...
# SSL-Session:
# Protocol : TLSv1.2
# Cipher : ECDHE-RSA-AES128-GCM-SHA256
# ...
# Verify return code: 0 (ok) ← this is what you want
# Or with curl
curl -v https://example.com 2>&1 | grep -E 'SSL|TLS|Connected'
# Should show: * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
Verify return code: 0 (ok) means the handshake succeeded and the certificate chain is valid. Anything else means you're not done yet.
Tips
Certificate debugging often involves checking file integrity. ToolCraft's Hash Generator lets you generate SHA-256 or MD5 hashes of cert files directly in the browser — nothing is uploaded, which matters when handling sensitive keys.
For ongoing prevention, keep these habits:
- Set TLS 1.2 as the minimum in all HTTP client configs — don't leave it at "auto"
- Add
openssl s_clientchecks to your CI pipeline to catch cert expiry before users do - Never set
ssl.SSLContext.check_hostname = Falsein Python — it disables hostname verification entirely - If you manage the server, test your TLS config with SSL Labs; aim for an A rating

