NGINX Error Guide: 'SSL_do_handshake() failed' and certificate errors
Fix NGINX SSL_do_handshake() failed and certificate errors: diagnose missing chains, wrong cert/key pairs, protocol mismatches, SNI issues, and expired certificates.
- #nginx
- #troubleshooting
- #errors
- #tls
Overview
SSL_do_handshake() failed and related certificate errors mean NGINX could not complete the TLS handshake with a client (or, for an HTTPS upstream, with the backend). The handshake can fail for many reasons: a client speaking a protocol NGINX disabled, a missing intermediate chain, a cert that does not match its key, an expired certificate, or a client without SNI hitting a server block that requires it.
Typical error.log lines:
2026/06/23 18:03:55 [crit] 7044#7044: *50122 SSL_do_handshake() failed (SSL: error:0A00006C:SSL routines::bad key share) while SSL handshaking, client: 203.0.113.21, server: 0.0.0.0:443
2026/06/23 18:04:10 [info] 7044#7044: *50130 SSL_do_handshake() failed (SSL: error:0A000102:SSL routines::unsupported protocol) while SSL handshaking
Some of these are fatal config errors that stop NGINX from starting (cannot load certificate); others are per-connection failures logged while serving. The fix depends entirely on the exact SSL error string — read it before changing anything.
Symptoms
- Browsers show
ERR_SSL_PROTOCOL_ERROR,NET::ERR_CERT_AUTHORITY_INVALID, orSEC_ERROR_UNKNOWN_ISSUER. - error.log records
SSL_do_handshake() failedwith an OpenSSL error code. - NGINX refuses to start with
cannot load certificate ... PEM_read_bioorkey values mismatch. openssl s_clientreportsverify error:num=20(unable to get local issuer) or an expired cert.
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 | grep -E 'verify|depth'
verify error:num=20:unable to get local issuer certificate
verify return:1
sudo grep "SSL_do_handshake" /var/log/nginx/error.log | tail -5
[crit] 7044#7044: *50122 SSL_do_handshake() failed (SSL: error:0A000102:SSL routines::unsupported protocol) while SSL handshaking
Common Root Causes
1. Missing intermediate certificate chain
ssl_certificate points at the leaf cert only, not the full chain. Browsers with a cached intermediate may work, but clients that need the chain fail with “unable to get local issuer”.
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 \
| grep -E 'verify error|Certificate chain' -A2
verify error:num=20:unable to get local issuer certificate
Certificate chain
0 s:CN = app.example.com i:CN = R3
Only one cert is presented (depth 0). ssl_certificate must point at fullchain.pem (leaf + intermediates), not cert.pem.
2. Certificate and private key do not match
The configured ssl_certificate and ssl_certificate_key are from different key pairs, so NGINX refuses to start.
sudo openssl x509 -noout -modulus -in /etc/nginx/ssl/app.crt | openssl md5
sudo openssl rsa -noout -modulus -in /etc/nginx/ssl/app.key | openssl md5
(stdin)= 4f9a... # cert modulus
(stdin)= b71c... # key modulus -- DIFFERENT
[emerg] 7044#7044: SSL_CTX_use_PrivateKey("/etc/nginx/ssl/app.key") failed (SSL: error:05800074:x509 certificate routines::key values mismatch)
The MD5s differ, so cert and key are not a pair. Re-pair them (the key that generated the CSR).
3. Expired certificate
The cert’s validity window has passed; clients reject it and NGINX may log handshake failures.
sudo openssl x509 -noout -enddate -in /etc/nginx/ssl/fullchain.pem
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 | openssl x509 -noout -dates
notAfter=May 20 23:59:59 2026 GMT
verify error:num=10:certificate has expired
notAfter is in the past. Renew (e.g. certbot renew) and reload NGINX.
4. Protocol or cipher mismatch
A client speaks a TLS version (or ciphers) that NGINX no longer allows via ssl_protocols/ssl_ciphers, causing “unsupported protocol”.
grep -RnE 'ssl_protocols|ssl_ciphers' /etc/nginx/nginx.conf /etc/nginx/conf.d/
echo | openssl s_client -connect app.example.com:443 -tls1_1 2>&1 | grep -E 'protocol|alert'
ssl_protocols TLSv1.2 TLSv1.3;
[info] 7044#7044: *50130 SSL_do_handshake() failed (SSL: error:0A000102:SSL routines::unsupported protocol) while SSL handshaking
A client offering only TLS 1.1 is refused because only 1.2/1.3 are enabled. This is usually correct security policy, not a bug — but it explains the failures for legacy clients.
5. Cert file cannot be loaded (path/permissions/format)
Wrong path, unreadable file, or a key with a passphrase NGINX cannot supply — the master aborts on start.
grep -RnE 'ssl_certificate|ssl_certificate_key' /etc/nginx/conf.d/app.conf
sudo ls -l /etc/nginx/ssl/
sudo nginx -t
[emerg] 7044#7044: cannot load certificate "/etc/nginx/ssl/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory)
The path is wrong or the file is missing/unreadable by the NGINX user. Fix the path and permissions (key should be 600, owned by root).
6. SNI mismatch — wrong server block / default cert served
A client that sends no SNI (or the wrong host) gets the default server’s certificate, which may not cover the hostname, causing a name-mismatch.
echo | openssl s_client -connect app.example.com:443 2>&1 | grep -E 'subject=|CN'
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 | grep -E 'subject='
subject=CN = default.example.com # without SNI -> default server cert
subject=CN = app.example.com # with SNI -> correct cert
Without SNI the client receives the default block’s cert. Ensure the right server_name/ssl_certificate covers the host, or make the default server present a valid cert.
Diagnostic Workflow
Step 1: Capture the exact OpenSSL error
sudo grep -E "SSL_do_handshake|cannot load certificate|key values mismatch" /var/log/nginx/error.log | tail -10
The OpenSSL string (unsupported protocol, bad key share, key values mismatch, certificate has expired) tells you which root cause to chase. [emerg] = config/startup; [crit]/[info] = per-connection.
Step 2: Inspect the served chain and validity from outside
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 \
| grep -E 'verify|Certificate chain|subject=|issuer='
verify error:num=20 = missing chain; num=10 = expired; a wrong subject= = SNI/server-block issue.
Step 3: Verify cert/key pairing and dates locally
sudo openssl x509 -noout -enddate -subject -in /etc/nginx/ssl/fullchain.pem
sudo openssl x509 -noout -modulus -in /etc/nginx/ssl/fullchain.pem | openssl md5
sudo openssl rsa -noout -modulus -in /etc/nginx/ssl/app.key | openssl md5
Matching modulus MD5s confirm cert and key belong together; the notAfter date confirms it has not expired.
Step 4: Check protocols, ciphers, and file references
grep -RnE 'ssl_protocols|ssl_ciphers|ssl_certificate|ssl_certificate_key' \
/etc/nginx/nginx.conf /etc/nginx/conf.d/ /etc/nginx/sites-enabled/
sudo ls -l /etc/nginx/ssl/
Confirm ssl_certificate is the fullchain file, the key path is correct and readable, and the enabled protocols include what your clients need.
Step 5: Validate and reload
sudo nginx -t
sudo systemctl reload nginx
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 | grep -E 'verify return code'
nginx -t catches load/pairing errors; after reload, verify return code: 0 (ok) confirms the chain validates.
Example Root Cause Analysis
Users on certain browsers report NET::ERR_CERT_AUTHORITY_INVALID for app.example.com, while others see no problem.
Checking the served chain from outside:
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 \
| grep -E 'verify error|Certificate chain' -A2
verify error:num=20:unable to get local issuer certificate
Certificate chain
0 s:CN = app.example.com i:CN = R3
Only the leaf cert (depth 0) is presented — the intermediate is missing. Browsers that had the intermediate cached succeeded; everyone else failed to build a path to the root. The config confirms it:
grep -RnE 'ssl_certificate ' /etc/nginx/conf.d/app.conf
ssl_certificate /etc/nginx/ssl/cert.pem; # leaf only, not fullchain
Fix: point ssl_certificate at the full chain, validate, and reload:
# change ssl_certificate /etc/nginx/ssl/cert.pem; -> /etc/nginx/ssl/fullchain.pem; then:
sudo nginx -t
sudo systemctl reload nginx
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 | grep 'verify return code'
verify return code: 0 (ok)
Every client can now build the chain.
Prevention Best Practices
- Always set
ssl_certificateto the full chain (fullchain.pem), never the leaf alone, so clients without a cached intermediate can build a path to the root. - Automate renewal with
certbot renew(or ACME of choice) on a timer and reload NGINX on success; monitornotAfterand alert well before expiry. - After issuing a cert, verify the cert/key modulus MD5s match before deploying, so a mismatched pair never reaches
nginx -tin production. - Keep private keys
chmod 600, owned by root, outside the web root; a missing or unreadable key file is a fatal[emerg]at start. - Choose
ssl_protocols/ssl_ciphersdeliberately (TLS 1.2/1.3) and document which legacy clients you are intentionally cutting off, so “unsupported protocol” failures are expected, not surprises. - For triage of a handshake incident, the free incident assistant can map the OpenSSL error string to the likely cert problem. More fixes live in the NGINX guides.
Quick Command Reference
# Capture the exact SSL error
sudo grep -E "SSL_do_handshake|cannot load certificate|key values mismatch" /var/log/nginx/error.log | tail -10
# Inspect served chain, validity, subject from outside
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>&1 \
| grep -E 'verify|Certificate chain|subject=|issuer='
# Confirm cert/key pairing and expiry locally
sudo openssl x509 -noout -enddate -subject -in /etc/nginx/ssl/fullchain.pem
sudo openssl x509 -noout -modulus -in /etc/nginx/ssl/fullchain.pem | openssl md5
sudo openssl rsa -noout -modulus -in /etc/nginx/ssl/app.key | openssl md5
# Check protocols and file references
grep -RnE 'ssl_protocols|ssl_ciphers|ssl_certificate|ssl_certificate_key' /etc/nginx/conf.d/
# Validate and reload
sudo nginx -t && sudo systemctl reload nginx
Conclusion
SSL_do_handshake() failed and certificate errors are decided by the exact OpenSSL string, not guesswork. The usual root causes:
- Missing intermediate chain (
ssl_certificateset to the leaf, not fullchain). - Certificate and key from different pairs (
key values mismatch). - An expired certificate.
- A protocol/cipher mismatch with a legacy client (
unsupported protocol). - A cert/key file that cannot be loaded (wrong path or permissions).
- An SNI mismatch serving the default block’s wrong certificate.
Read the OpenSSL error, verify the served chain with openssl s_client, confirm the cert/key pair and expiry locally, then nginx -t and reload. Most production handshake failures come down to a missing fullchain or an expired cert.
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.