You renewed your SSL certificate, updated Nginx configuration, and restarted the server. But browsers show "Your connection is not secure" or API calls fail with "certificate verify failed." The certificate files exist, Nginx starts without errors, yet something is still broken.
SSL certificate issues in Nginx are often subtle—a missing intermediate certificate, wrong file order, or expired chain. Let's diagnose and fix them.
Common SSL Error Messages
The errors show up in different places:
Browser:
``
NET::ERR_CERT_AUTHORITY_INVALID
Your connection is not private
**Error log (/var/log/nginx/error.log):**
``
2026/04/04 12:00:00 [error] 1234#1234: *5678 SSL_do_handshake() failed (SSL: error:1416A0BF:SSL routines:tls_process_client_certificate:certificate verify failed) while SSL handshaking
cURL:
``
curl: (60) SSL certificate problem: unable to get local issuer certificate
curl: (60) SSL certificate problem: certificate has expired
curl: (60) SSL certificate problem: self signed certificate
Step 1: Verify Certificate Files Exist and Are Readable
First, confirm Nginx can read the certificate files:
```bash # Check file existence and permissions ls -la /etc/nginx/ssl/
# Test if nginx user can read them sudo -u nginx cat /etc/nginx/ssl/example.com.crt > /dev/null && echo "Readable" || echo "Permission denied" ```
If permission denied:
chmod 644 /etc/nginx/ssl/*.crt
chmod 600 /etc/nginx/ssl/*.key # Keys should be more restrictive
chown nginx:nginx /etc/nginx/ssl/*.keyStep 2: Verify Certificate Validity
Check if the certificate is actually valid and not expired:
```bash # Check certificate dates openssl x509 -in /etc/nginx/ssl/example.com.crt -noout -dates
# Check full certificate details openssl x509 -in /etc/nginx/ssl/example.com.crt -noout -text
# Check if private key matches certificate openssl x509 -noout -modulus -in /etc/nginx/ssl/example.com.crt | openssl md5 openssl rsa -noout -modulus -in /etc/nginx/ssl/example.com.key | openssl md5 ```
The MD5 hashes should match. If they don't, you have a mismatched key/certificate pair.
Step 3: Check Certificate Chain Completeness
This is the most common issue. Modern browsers and clients require the full certificate chain, but many certificate authorities only provide the server certificate. You need to include intermediate certificates.
Check your current certificate:
# Count certificates in the file
grep -c "BEGIN CERTIFICATE" /etc/nginx/ssl/example.com.crtIf it shows only 1, you're missing the intermediate chain.
Verify with OpenSSL:
# Test the chain
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | grep -A2 "Certificate chain"Output with missing chain:
``
Certificate chain
0 s:CN = example.com
Should show something like:
``
Certificate chain
0 s:CN = example.com
i:CN = R3
1 s:CN = R3
i:CN = ISRG Root X1
Fix: Create a full chain certificate file
For Let's Encrypt, the files are usually in /etc/letsencrypt/live/example.com/:
# Create full chain (certificate + intermediate)
cat /etc/letsencrypt/live/example.com/cert.pem \
/etc/letsencrypt/live/example.com/chain.pem > /etc/nginx/ssl/example.com-fullchain.crtFor other CAs, download the intermediate certificate and concatenate:
# Order matters: server cert first, then intermediates
cat your-server.crt intermediate-ca.crt root-ca.crt > fullchain.crtStep 4: Update Nginx Configuration
Make sure your Nginx config uses the correct files:
```nginx server { listen 443 ssl http2; server_name example.com;
# Full chain certificate ssl_certificate /etc/nginx/ssl/example.com-fullchain.crt;
# Private key ssl_certificate_key /etc/nginx/ssl/example.com.key;
# Modern SSL configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; } ```
Test and reload:
nginx -t && systemctl reload nginxStep 5: Fix Let's Encrypt Renewal Issues
Let's Encrypt certificates expire every 90 days. Check renewal status:
```bash # Check certificate expiration certbot certificates
# Force renewal (dry run) certbot renew --dry-run
# Check renewal cron/timer systemctl list-timers | grep certbot ```
If renewal fails, check the challenges directory:
# Ensure this location block exists
location /.well-known/acme-challenge/ {
root /var/www/certbot;
allow all;
}For certbot standalone mode:
# Stop Nginx temporarily
systemctl stop nginx
certbot certonly --standalone -d example.com -d www.example.com
systemctl start nginxStep 6: Handle Self-Signed Certificates
For development environments with self-signed certificates:
# Generate self-signed certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/selfsigned.key \
-out /etc/nginx/ssl/selfsigned.crt \
-subj "/CN=localhost"Nginx configuration:
```nginx server { listen 443 ssl; server_name localhost;
ssl_certificate /etc/nginx/ssl/selfsigned.crt; ssl_certificate_key /etc/nginx/ssl/selfsigned.key;
# For development only ssl_verify_client off; } ```
For clients to accept self-signed certificates:
```bash # Import CA into system trust store (Linux) cp /etc/nginx/ssl/selfsigned.crt /usr/local/share/ca-certificates/ update-ca-certificates
# Or for curl curl --cacert /etc/nginx/ssl/selfsigned.crt https://localhost/ ```
Step 7: Fix Client Certificate Verification
If you're using mutual TLS (mTLS) and getting verify errors:
```nginx server { listen 443 ssl; server_name api.example.com;
ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key;
# Client certificate verification ssl_client_certificate /etc/nginx/ssl/ca.crt; ssl_verify_client on; ssl_verify_depth 2; } ```
Common issues:
```bash # Verify the CA certificate openssl verify -CAfile /etc/nginx/ssl/ca.crt /etc/nginx/ssl/client.crt
# Check if client cert is signed by your CA openssl verify -purpose sslclient -CAfile /etc/nginx/ssl/ca.crt client.crt ```
Step 8: Debug SSL Handshake Issues
For deeper troubleshooting, enable SSL debug logging:
error_log /var/log/nginx/error.log debug;Or use OpenSSL directly:
```bash # Test SSL connection openssl s_client -connect example.com:443 -servername example.com -showcerts
# Test specific protocol openssl s_client -connect example.com:443 -servername example.com -tls1_2
# Test with specific cipher openssl s_client -connect example.com:443 -servername example.com -cipher ECDHE-RSA-AES128-GCM-SHA256 ```
Step 9: Check OCSP Stapling
OCSP stapling improves SSL handshake performance but can cause issues:
```nginx server { # ... other SSL config ...
# OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/fullchain.crt; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; } ```
Test OCSP:
# Check if OCSP stapling works
openssl s_client -connect example.com:443 -servername example.com -status < /dev/null 2>&1 | grep -A 10 "OCSP response"If you see "OCSP Response Status: successful", it's working. Errors often stem from:
- Missing
ssl_trusted_certificate(should be full chain) - DNS resolver issues
- OCSP responder unreachable
Step 10: Verify Complete SSL Setup
After making changes, verify everything works:
```bash # Test Nginx config nginx -t
# Reload Nginx systemctl reload nginx
# Test with SSL Labs (comprehensive) # Visit: https://www.ssllabs.com/ssltest/analyze.html?d=example.com
# Quick local test openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 | grep "Verify return code"
# Should show: Verify return code: 0 (ok) ```
Test with curl:
```bash # Test certificate chain curl -vI https://example.com/ 2>&1 | grep -E "(SSL certificate verify|subject:|issuer:)"
# Test with specific CA curl --cacert /etc/nginx/ssl/fullchain.crt https://example.com/ ```
Quick SSL Configuration Template
A secure, modern SSL configuration:
```nginx server { listen 443 ssl http2; server_name example.com;
ssl_certificate /etc/nginx/ssl/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key;
# Modern configuration (Mozilla guideline) 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;
# Session configuration ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; ssl_session_tickets off;
# OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/nginx/ssl/fullchain.crt; resolver 8.8.8.8 8.8.4.4 valid=300s;
# HSTS add_header Strict-Transport-Security "max-age=63072000" always; }
# Redirect HTTP to HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; } ```
SSL issues are almost always about chain completeness and correct file paths. When in doubt, test with OpenSSL first—it gives clearer error messages than browsers.