Fix: Nginx SSL: error:0A00006C:SSL routines::bad key / SSL handshake failed
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Nginx SSL handshake failed and certificate errors caused by mismatched keys, wrong certificate chain, expired certs, TLS version issues, and permission problems.
The Error
Nginx fails to start or clients cannot connect with SSL errors:
nginx: [emerg] SSL_CTX_use_PrivateKey_file("/etc/nginx/ssl/server.key") failed
(SSL: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch)Or variations in the error log:
SSL_do_handshake() failed (SSL: error:0A00006C:SSL routines::bad key)*1 SSL_do_handshake() failed (SSL: error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure)*1 no "ssl_certificate" is defined in server listening on SSL port while SSL handshakingcurl: (35) error:0A000086:SSL routines::certificate verify failedupstream SSL certificate verify error: (10:certificate has expired)Nginx cannot complete the SSL/TLS handshake. The certificate, private key, or SSL configuration has a problem.
Why This Happens
An SSL/TLS handshake requires:
- A valid certificate that matches the domain name.
- The matching private key that was used to generate the certificate.
- The full certificate chain (intermediate certificates) so clients can verify trust.
- Compatible TLS versions and ciphers between client and server.
- Correct file permissions so Nginx can read the certificate and key files.
Common causes:
- Certificate and key mismatch. The private key does not correspond to the certificate.
- Missing intermediate certificate. The chain is incomplete, causing trust verification failure.
- Expired certificate. The certificate’s validity period has ended.
- Wrong file format. The certificate or key is in the wrong format (DER vs PEM).
- Permission denied. Nginx cannot read the certificate or key file.
- TLS version mismatch. Client requires TLS 1.2 but server only offers TLS 1.0.
- Wrong certificate for the domain. The certificate does not match the requested hostname (SNI mismatch).
- Expired root CA. A previously trusted root certificate (DST Root CA X3 in 2021) reached its expiry, breaking trust on older OpenSSL versions.
- Cipher suite removal. A server upgrade dropped legacy ciphers that long-lived client devices still require.
Each of these failure modes produces a different error string, but the operational consequence is the same: TLS handshakes stop succeeding and every request to that vhost returns a transport-layer error before any HTTP processing happens. This is why SSL incidents tend to look like full outages — the load balancer, CDN, and monitoring agents all fail at the same layer.
In Production: Incident Lens
SSL handshake failures are some of the highest-blast-radius incidents you can have. Unlike an application bug that touches one endpoint, a broken cert kills every HTTPS connection to the affected vhost — APIs, the marketing site, webhooks, OAuth callbacks, even your own monitoring agents if they call back over TLS. Customer-facing pages return browser warnings, mobile apps refuse to connect, and any downstream service that pins the certificate goes silent.
How it surfaces: the canonical version is “the cert expired at 00:00 UTC and the on-call woke up to PagerDuty at 00:02.” The synthetic check fails the next pulse, error rates spike to 100% on the HTTPS listener, and any external monitoring (Pingdom, UptimeRobot, StatusCake) lights up. A subtler variant: a CA rotated its intermediate certificate, you only deployed the leaf, and clients with old trust stores (Android < 7.1.1, Java 8 default trust, old curl) start failing while modern browsers still work because they cross-sign. The 2021 DST Root CA X3 expiry was the classic example — anything pinned to that root broke worldwide while browsers were fine.
Blast radius: typically every HTTPS request to the affected server block. If the cert is shared across multiple vhosts via SAN, all of them go down. If it is the wildcard cert behind a CDN origin shield, every subdomain stops responding. Worst case: the cert protects the API your mobile apps depend on, and you cannot push an emergency app update fast enough to recover. Plan for the assumption that an SSL incident equals a full outage of the affected hostname for the duration.
Monitoring signal: the high-value alerts are layered. First, certificate expiry days remaining — alert at 30, 14, 7, 3, and 1 day(s) with escalating severity. Second, TLS handshake error rate — Nginx exposes this via stub_status extensions or via the error log; ship those logs to your aggregator and alert on a sustained SSL_do_handshake() failed rate above baseline. Third, OCSP responder availability for stapled responses. Fourth, an external probe (Datadog, Checkly, Vercel) that performs a real TLS handshake from outside your network — your internal checks may use a different trust store than your customers.
Recovery sequence: the first move is rotation. If you have a previous valid cert on disk, swap the ssl_certificate and ssl_certificate_key paths back, run nginx -t, and systemctl reload nginx. If the cert is fresh but lacks the intermediate, regenerate fullchain.crt by concatenating leaf + intermediate (in that order) and reload. If you cannot recover within minutes, push a CDN-level fallback (Cloudflare’s Universal SSL, Fastly’s certificate) to terminate TLS upstream of your Nginx. Never disable TLS verification on the client side as a workaround — it normalizes an insecure state and clients rarely re-enable it.
Postmortem preventive: automate everything. Run cert-manager in Kubernetes or certbot with --deploy-hook on bare metal, with renewal attempted at 30 days remaining. Add expiry alerts at 14, 7, and 3 days as a second line of defense. Always reference the full chain in ssl_certificate, not the leaf only — Nginx does not fetch intermediates the way browsers do, and clients that lack AIA fetching will fail. Run an SSL Labs scan in CI against staging after every Nginx config change and fail the pipeline if the grade drops below A. Keep the previous cert on disk for at least 30 days after rotation so rollback is a one-line config edit.
Fix 1: Fix Certificate and Key Mismatch
Verify the certificate and key match:
# Get the modulus hash of the certificate
openssl x509 -noout -modulus -in /etc/nginx/ssl/server.crt | openssl md5
# Get the modulus hash of the private key
openssl rsa -noout -modulus -in /etc/nginx/ssl/server.key | openssl md5If the two MD5 hashes are different, the certificate and key do not match.
Fix: Generate a new key and CSR, then request a new certificate:
# Generate a new private key and CSR
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
-subj "/CN=example.com"
# Submit server.csr to your CA for a new certificateFix: Find the matching key. If you have multiple key files, check each one:
for keyfile in /etc/nginx/ssl/*.key; do
echo "$keyfile: $(openssl rsa -noout -modulus -in "$keyfile" | openssl md5)"
done
echo "Certificate: $(openssl x509 -noout -modulus -in /etc/nginx/ssl/server.crt | openssl md5)"The key file whose hash matches the certificate hash is the correct one.
Pro Tip: When you generate a key and CSR, immediately associate them. Name the files consistently:
example.com.key,example.com.csr,example.com.crt. This prevents mixing up keys from different certificate requests.
Fix 2: Fix the Certificate Chain
Clients need the full certificate chain (your certificate + intermediate certificates) to verify trust:
Check the chain:
# Show the full certificate chain
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
openssl x509 -noout -subject -issuerBuild the correct chain file:
# Combine your certificate with the intermediate certificate(s)
cat server.crt intermediate.crt > fullchain.crt
# Order matters: your cert first, then intermediate(s), then root (optional)Nginx configuration:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.crt; # Full chain, not just your cert
ssl_certificate_key /etc/nginx/ssl/server.key;
}Verify the chain is complete:
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.crt
# Should output: fullchain.crt: OKDownload missing intermediate certificates:
# Extract the issuer URL from your certificate
openssl x509 -noout -text -in server.crt | grep "CA Issuers"
# CA Issuers - URI:http://crt.example.com/intermediate.crt
# Download the intermediate cert
curl -o intermediate.crt http://crt.example.com/intermediate.crt
# Convert from DER to PEM if needed
openssl x509 -inform DER -in intermediate.crt -out intermediate.pemCommon Mistake: Using only the leaf certificate without intermediates in
ssl_certificate. Browsers might still work (they can fetch intermediates themselves), but API clients, curl, and other tools will fail with “unable to verify the first certificate.”
Fix 3: Fix Expired Certificates
Check certificate expiration:
# Check expiry date
openssl x509 -noout -dates -in /etc/nginx/ssl/server.crt
# notBefore=Jan 15 00:00:00 2024 GMT
# notAfter=Jan 15 23:59:59 2025 GMT
# Check from the live server
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -datesFix: Renew the certificate.
For Let’s Encrypt / Certbot:
# Renew all certificates
sudo certbot renew
# Force renewal of a specific certificate
sudo certbot renew --cert-name example.com --force-renewal
# Reload Nginx after renewal
sudo systemctl reload nginxSet up automatic renewal:
# Certbot usually installs a cron job or systemd timer
sudo systemctl status certbot.timer
# If not, add a cron job
echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'" | sudo crontab -For commercial certificates: Contact your CA to reissue or renew the certificate.
Fix 4: Fix File Format Issues
Nginx requires PEM format for certificates and keys:
Check the format:
# PEM files start with -----BEGIN CERTIFICATE-----
head -1 server.crt
# If it looks like binary data, it's DER format
file server.crtConvert DER to PEM:
# Certificate
openssl x509 -inform DER -in server.crt -out server.pem
# Private key
openssl rsa -inform DER -in server.key -out server.pemConvert PKCS#12 (.pfx/.p12) to PEM:
# Extract certificate
openssl pkcs12 -in server.pfx -clcerts -nokeys -out server.crt
# Extract private key
openssl pkcs12 -in server.pfx -nocerts -nodes -out server.keyFix passphrase-protected keys:
# Option 1: Specify the passphrase file
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_password_file /etc/nginx/ssl/key_passphrase;# Option 2: Remove the passphrase from the key
openssl rsa -in encrypted.key -out decrypted.keyFix 5: Fix File Permissions
Nginx must be able to read the certificate and key files:
# Check permissions
ls -la /etc/nginx/ssl/
# Fix ownership and permissions
sudo chown root:root /etc/nginx/ssl/server.key
sudo chmod 600 /etc/nginx/ssl/server.key # Only root can read the key
sudo chown root:root /etc/nginx/ssl/server.crt
sudo chmod 644 /etc/nginx/ssl/server.crt # Certificate can be world-readableCheck SELinux (RHEL/CentOS):
# Check for SELinux denials
sudo ausearch -m avc -ts recent | grep nginx
# Restore correct context
sudo restorecon -Rv /etc/nginx/ssl/Test the Nginx configuration:
sudo nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successfulFix 6: Fix TLS Version and Cipher Configuration
Modern clients require TLS 1.2 or 1.3:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (optional but recommended)
add_header Strict-Transport-Security "max-age=63072000" always;
}Test which TLS versions and ciphers the server supports:
# Test TLS 1.2
openssl s_client -connect example.com:443 -tls1_2
# Test TLS 1.3
openssl s_client -connect example.com:443 -tls1_3
# List supported ciphers
nmap --script ssl-enum-ciphers -p 443 example.comFix 7: Fix SNI (Server Name Indication) Issues
If you host multiple domains on one IP, SNI configuration must be correct:
# Each domain needs its own server block with the correct certificate
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/example.com/server.key;
}
server {
listen 443 ssl;
server_name other.com;
ssl_certificate /etc/nginx/ssl/other.com/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/other.com/server.key;
}Set a default SSL server for unknown hostnames:
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/default/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/default/server.key;
return 444; # Close connection
}Fix 8: Fix Upstream SSL (Reverse Proxy)
When Nginx proxies to an HTTPS backend:
location / {
proxy_pass https://backend-server:8443;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/ssl/backend-ca.crt;
proxy_ssl_server_name on;
}If the backend has a self-signed certificate:
# Option 1: Trust the self-signed cert
proxy_ssl_trusted_certificate /etc/nginx/ssl/backend-self-signed.crt;
proxy_ssl_verify on;
# Option 2: Disable verification (development only!)
proxy_ssl_verify off;Still Not Working?
Check the Nginx error log for details:
sudo tail -50 /var/log/nginx/error.logTest the certificate from a client:
curl -v https://example.com 2>&1 | grep -E "SSL|TLS|certificate"Check for OCSP stapling issues:
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;If the OCSP responder is unreachable, connections may fail. Disable stapling temporarily to test.
Use SSL Labs to audit your configuration: Submit your domain to SSL Labs Server Test for a comprehensive analysis.
Check for a chain rebuild after a CA rotation. When a CA rotates its intermediate (this happens every few years for major CAs), the new leaf certificate is signed by a new intermediate that your fullchain file does not include. Browsers cross-sign and recover automatically; older clients and pinned applications do not. After every renewal, diff fullchain.crt against the previous one and confirm the intermediate chain still validates against your client’s trust store. Run openssl s_client -connect host:443 -showcerts and verify every certificate in the output has a corresponding issuer.
Check for OCSP stapling timeouts in the error log. A line like ssl_stapling_responder_timeout means Nginx tried to fetch a fresh OCSP response and the CA’s OCSP server was slow. The first handshake after Nginx starts can hang for several seconds while it warms the cache. Pre-fetch the response with ssl_stapling_file pointing to a regularly refreshed DER file, or rely on the CA’s must-staple extension being optional.
Check for client-side intermediate trust gaps. Some clients (especially JVM 8 with the default cacerts, older Android, embedded devices) have a fixed trust store that does not auto-update. Even a correct full chain on the server will fail there if the root has rotated. Either pin the older root in those clients or migrate them to a CA whose roots they trust.
Check for protocol downgrade due to load balancer mismatch. If a load balancer in front of Nginx negotiates TLS 1.3 with the client and TLS 1.2 with Nginx, cipher and SNI behavior can diverge. Verify the LB’s TLS settings match Nginx’s expectations.
For Nginx 502 errors, see Fix: Nginx 502 Bad Gateway. For upstream timeout issues, see Fix: Nginx upstream timed out. For general SSL certificate errors, see Fix: SSL certificate problem: unable to get local issuer certificate. For renewal automation failures, see Fix: Nginx Certbot renewal failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Certbot Certificate Renewal Failed (Let's Encrypt)
How to fix Certbot certificate renewal failures — domain validation errors, port 80 blocked, nginx config issues, permissions, and automating renewals with systemd or cron.
Fix: nginx Upstream Load Balancing Not Working — All Traffic Hitting One Server
How to fix nginx load balancing issues — upstream block configuration, health checks, least_conn vs round-robin, sticky sessions, upstream timeouts, and SSL termination.
Fix: Kubernetes Ingress Not Working (404, 502, or Traffic Not Routing)
How to fix Kubernetes Ingress not routing traffic — why Ingress returns 404 or 502, how to configure annotations correctly, debug ingress-nginx and AWS ALB Ingress Controller, and verify backend service health.
Fix: Nginx WebSocket Proxy Not Working (101 Switching Protocols Failed)
How to fix Nginx WebSocket proxying not working — 101 Switching Protocols fails, connections drop after 60 seconds, missing Upgrade headers, and SSL WebSocket configuration.