Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 11 min read

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, or SEC_ERROR_UNKNOWN_ISSUER.
  • error.log records SSL_do_handshake() failed with an OpenSSL error code.
  • NGINX refuses to start with cannot load certificate ... PEM_read_bio or key values mismatch.
  • openssl s_client reports verify 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_certificate to 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; monitor notAfter and alert well before expiry.
  • After issuing a cert, verify the cert/key modulus MD5s match before deploying, so a mismatched pair never reaches nginx -t in 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_ciphers deliberately (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:

  1. Missing intermediate chain (ssl_certificate set to the leaf, not fullchain).
  2. Certificate and key from different pairs (key values mismatch).
  3. An expired certificate.
  4. A protocol/cipher mismatch with a legacy client (unsupported protocol).
  5. A cert/key file that cannot be loaded (wrong path or permissions).
  6. 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.

Free download · 368-page PDF

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.