Fix: Nginx SSL: error:0A00006C:SSL routines::bad key / SSL handshake failed
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).
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.
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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Nginx 413 Request Entity Too Large
How to fix the Nginx 413 Request Entity Too Large error when uploading files by adjusting client_max_body_size, PHP limits, Node.js body parser, proxy buffers, Docker ingress, and more.
Fix: Docker container health status unhealthy
How to fix Docker container health check failing with unhealthy status, including HEALTHCHECK syntax, timing issues, missing curl/wget, endpoint problems, and Compose healthcheck configuration.
Fix: AWS CloudFormation stack in ROLLBACK_COMPLETE or CREATE_FAILED state
How to fix AWS CloudFormation ROLLBACK_COMPLETE and CREATE_FAILED errors caused by IAM permissions, resource limits, invalid parameters, and dependency failures.
Fix: Docker build sending large build context / slow Docker build
How to fix Docker build sending large build context caused by missing .dockerignore, node_modules in context, large files, and inefficient Dockerfile layers.