NGINX Error Guide: 'SSL_do_handshake() failed' Protocol and Cipher Mismatch
Fix NGINX SSL_do_handshake() failed errors caused by TLS protocol version and cipher mismatches, no shared cipher, and wrong version number handshakes.
- #nginx
- #troubleshooting
- #errors
- #tls
Exact Error Message
NGINX logs handshake negotiation failures to error.log at the [info] level. They are noisy and easy to miss, but they all share the SSL_do_handshake() failed prefix. Here are the most common variants you will see:
2026/06/27 11:47:03 [info] 2841#2841: *881 SSL_do_handshake() failed (SSL: error:0A00006C:SSL routines::bad key share) while SSL handshaking, client: 203.0.113.7, server: 0.0.0.0:443
2026/06/27 11:48:19 [info] 2841#2842: *902 SSL_do_handshake() failed (SSL: error:0A000102:SSL routines::unsupported protocol) while SSL handshaking, client: 198.51.100.22, server: 0.0.0.0:443
2026/06/27 11:51:44 [info] 2841#2843: *934 SSL_do_handshake() failed (SSL: error:0A0000C1:SSL routines::no shared cipher) while SSL handshaking, client: 192.0.2.45, server: 0.0.0.0:443
2026/06/27 11:53:02 [info] 2841#2844: *961 SSL_do_handshake() failed (SSL: error:0A00010B:SSL routines::wrong version number) while SSL handshaking, client: 203.0.113.88, server: 0.0.0.0:443
What the Error Means
SSL_do_handshake() is the OpenSSL call NGINX uses to negotiate a TLS session before any HTTP request is exchanged. When that call returns an error, the handshake never completed and the connection is dropped. The client typically sees ERR_SSL_PROTOCOL_ERROR, handshake failure, or a connection reset.
Crucially, these errors happen at the negotiation layer, not the certificate-validation layer. The client and server are arguing about which TLS protocol version and cipher suite to use (or one side is not speaking TLS at all). This is distinct from certificate expiry, an incomplete chain, or hostname mismatch, which produce different errors (certificate verify failed, unable to get local issuer certificate). If your error string mentions protocol, version, cipher, or key share, you are in the right guide. If it mentions a certificate, see the cert guide linked under Related Errors.
Common Causes
- Protocol version mismatch. Your
ssl_protocolsdirective excludes the version the client offers. A modern hardened server set toTLSv1.2 TLSv1.3will reject a legacy TLS 1.0 / 1.1 client (old Java, embedded devices, ancient curl), producingunsupported protocolortlsv1 alert protocol version. - No shared cipher. Your
ssl_cipherslist is so strict that it shares no cipher suite with the client. Common after copy-pasting an aggressive “A+” cipher string that drops everything the client supports. Yieldsno shared cipher. - Wrong version number. A client (or health checker, or load balancer) sends plain HTTP to a port expecting TLS. OpenSSL reads
GET / HTTP/1.1as a malformed TLS record and reportswrong version number. Port scanners and misconfiguredproxy_pass http://to an HTTPS upstream do this constantly. - Bad key share (TLS 1.3). The client offered a key-share group the server does not support, or an interception/middlebox mangled the ClientHello.
- SNI issues. A client connecting without SNI (or with the wrong SNI) lands on the default
serverblock, which may have an incompatible protocol/cipher policy. - Upstream vs. downstream confusion. The same error appears when NGINX is the TLS client proxying to an HTTPS backend (
proxy_pass https://) and the backend rejects NGINX’s protocol/cipher offer. The log will reference anupstream:field instead ofclient:.
How to Reproduce the Error
Force an old protocol against a modern server to trigger unsupported protocol:
# Server set to TLSv1.2+ only; force TLS 1.0 from the client
openssl s_client -connect app.example.com:443 -tls1
Force a cipher the server does not allow to trigger no shared cipher:
# Offer only a legacy cipher the hardened server has dropped
openssl s_client -connect app.example.com:443 -cipher 'DES-CBC3-SHA'
Send plain HTTP to the TLS port to trigger wrong version number:
# curl over HTTP to an HTTPS-only listener
curl -v http://app.example.com:443/
Each command leaves a matching SSL_do_handshake() failed line in error.log.
Diagnostic Commands
Start by confirming the active TLS policy NGINX is actually running (not just what a single file says):
# Validate config syntax
sudo nginx -t
# Dump the FULLY rendered config and inspect TLS policy
sudo nginx -T 2>/dev/null | grep -nE 'ssl_protocols|ssl_ciphers|server_name|listen'
Test what the server will negotiate, per protocol version:
# Does the server accept TLS 1.2?
openssl s_client -connect app.example.com:443 -tls1_2 </dev/null
# Always pass SNI; many vhosts only respond correctly with it
openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null
# Probe a specific cipher to confirm overlap
openssl s_client -connect app.example.com:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256' </dev/null
Confirm a client tool can complete the handshake at a given version:
# -I = headers only; -v shows the negotiated protocol/cipher line
curl -Iv --tlsv1.2 https://app.example.com/
Enumerate every protocol and cipher the server offers (read-only scan):
nmap --script ssl-enum-ciphers -p 443 app.example.com
Watch the live error stream while you reproduce:
sudo tail -f /var/log/nginx/error.log
journalctl -u nginx --since "5 min ago" --no-pager
The nmap output is the fastest way to see the gap: compare the protocols/ciphers the server lists against what the failing client supports.
Step-by-Step Resolution
1. Read the error string first. unsupported protocol / tlsv1 alert means a protocol gap. no shared cipher means a cipher gap. wrong version number means someone is speaking plain HTTP to a TLS port, not a TLS problem at all.
2. Decide whether to widen the policy or fix the client. Security best practice is to keep TLS 1.2 and 1.3 only. If the failing client is a legitimate, un-upgradable system, you may need to re-enable an older protocol for that vhost. Prefer upgrading the client.
3. Adjust ssl_protocols to include the client’s version. In your server or http block, ensure the directive lists the versions you intend to support, for example ssl_protocols TLSv1.2 TLSv1.3;. Add TLSv1.1 only if a real legacy client requires it. Then validate and reload:
sudo nginx -t && sudo systemctl reload nginx
4. Loosen ssl_ciphers only enough to create overlap. If nmap shows zero shared suites, replace an overly aggressive cipher string with a known-good Mozilla “intermediate” list, then re-test with the openssl s_client -cipher ... command above. Reload after editing:
sudo nginx -t && sudo systemctl reload nginx
5. For wrong version number, fix the caller, not NGINX. Point health checks and load balancers at https:// instead of http://, or expose a plain-HTTP listener on a separate port. If your upstream block uses proxy_pass http://backend but the backend is HTTPS, switch it to https:// and set proxy_ssl_protocols / proxy_ssl_ciphers to match the backend.
6. Always test with SNI. Re-run openssl s_client -servername app.example.com to confirm the correct vhost answers. A handshake that succeeds with SNI but fails without it points to a default-server policy gap.
7. Re-run the reproduction command until it completes cleanly and the error.log line stops appearing.
Prevention and Best Practices
- Pin
ssl_protocols TLSv1.2 TLSv1.3;in thehttpblock so every vhost inherits a consistent, modern baseline. - Use a vetted cipher list (Mozilla SSL Configuration Generator, “intermediate” profile) rather than hand-rolled strings that accidentally exclude all clients.
- Monitor
error.logforSSL_do_handshake() failedrates; a spike often means a deploy changed the TLS policy or a client population you forgot about. - Keep
ssl_prefer_server_ciphers off;for TLS 1.3 and let the client choose; enforce server order only for TLS 1.2 if you have a strong reason. - Run
nmap --script ssl-enum-ciphersin CI against staging to catch protocol/cipher regressions before they reach production. - Separate plain-HTTP and HTTPS listeners onto distinct ports so probes never produce
wrong version numbernoise on your TLS port. - See the NGINX category for related listener and proxy guides.
Related Errors
certificate verify failed/unable to get local issuer certificate— certificate chain or expiry problems, covered in the separate NGINX certificate guide. These are validation failures, not negotiation failures.SSL_do_handshake() failed ... upstream:— the same call failing when NGINX is the TLS client to an HTTPS backend; fix withproxy_ssl_protocols/proxy_ssl_ciphers.502 Bad Gateway— frequently the downstream symptom when an upstream TLS handshake fails.tlsv1 alert protocol version— a specific alert form of the protocol-version mismatch described here.
Frequently Asked Questions
Is SSL_do_handshake() failed a certificate problem?
No. This error family is about protocol-version and cipher negotiation (or plain HTTP hitting a TLS port). Certificate problems produce different strings such as certificate verify failed. If you see unsupported protocol, no shared cipher, wrong version number, or bad key share, your certificates are almost certainly fine.
Why does wrong version number keep appearing even though TLS works in the browser?
Because something is sending plain HTTP to your HTTPS port. The usual culprits are health checkers, monitoring probes, port scanners, or an upstream block using proxy_pass http:// against an HTTPS backend. Browsers negotiate correctly, so the noise comes from non-browser clients. Point them at https:// or a dedicated HTTP port.
Should I just enable TLS 1.0 and 1.1 to make the errors stop?
Only as a last resort for a specific, un-upgradable client, and ideally scoped to one vhost. TLS 1.0/1.1 are deprecated and fail most compliance baselines. Prefer upgrading the client. Widening ssl_protocols globally to silence a single legacy device weakens every other connection.
How do I tell which side rejected the handshake?
Check the log fields: a client: field means a downstream client failed against NGINX, while an upstream: field means NGINX failed as a client against your backend. Then reproduce with openssl s_client (for downstream) or inspect proxy_ssl_* directives (for upstream) to confirm where the protocol or cipher gap lives.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.