The Problem
If you have ever checked your Nginx error.log only to find a cryptic handshake failure, you are likely dealing with a certificate trust issue. This specific error usually surfaces when Nginx acts as a reverse proxy for a backend server that requires HTTPS. The log entry typically looks like this:
[error] 12345#0: *678 SSL_do_handshake() failed (SSL: error:14090086:SSL routines:ssl3_get_server_certificate:certificate verify failed) while SSL handshaking to upstream, client: 1.2.3.4, server: example.com, request: "GET /api HTTP/1.1", upstream: "https://10.0.0.5:443/api", host: "example.com"
The root of the issue is that Nginx behaves like a web browser when it talks to your backend (upstream). If you tell Nginx to verify the backend's identity but it cannot validate the certificate against its list of trusted authorities, it kills the connection immediately to keep data secure.
TL;DR: Quick Fix
For local development or internal testing where security isn't the top priority, you can tell Nginx to stop being so picky. Adding proxy_ssl_verify off; to your location block will bypass the check:
location / {
proxy_pass https://backend_upstream;
proxy_ssl_verify off;
}
Warning: Never use this in production. Bypassing SSL verification makes your internal traffic vulnerable to man-in-the-middle attacks.
Why Handshakes Fail
- **Untrusted Certificates:** Your backend uses a self-signed certificate or a private CA (like a corporate root) that Nginx doesn't recognize.
- **Missing CA Chain:** Nginx lacks the specific Root CA bundle needed to verify the full certificate path.
- **SNI Mismatch:** Modern backends—especially those on AWS, Cloudflare, or Kubernetes—expect a Server Name Indication (SNI). If Nginx doesn't send the hostname, the backend serves the wrong certificate.
- **Expired Certs:** The backend certificate passed its expiration date, or the system clock on your Nginx server is out of sync.
Step-by-Step Fixes
Method 1: Trust the Backend Certificate (Best Practice)
If your backend uses a private CA, Nginx needs to see the credentials. First, export your backend's CA certificate as a .crt or .pem file. Then, explicitly tell Nginx to trust that specific file.
location / {
proxy_pass https://backend_server;
proxy_ssl_trusted_certificate /etc/nginx/certs/internal-ca.crt;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
}
Method 2: Enable SNI Support
By default, Nginx does not pass the hostname during the SSL handshake with the backend. This often causes failures when proxying to services like an AWS Application Load Balancer (ALB) or a Kubernetes Ingress controller. You can fix this by enabling proxy_ssl_server_name.
location / {
proxy_pass https://api.internal.service;
# This sends the hostname to the backend during the handshake
proxy_ssl_server_name on;
proxy_ssl_name api.internal.service;
}
Method 3: Update the System CA Store
Sometimes the problem isn't your config; it's your OS. If your backend uses a standard certificate (like Let's Encrypt) but your Nginx server is running an old OS, the root certificates might be expired. This happened famously with the DST Root CA X3 expiration in 2021.
# For Ubuntu 22.04 or Debian
sudo apt-get update && sudo apt-get install --only-upgrade ca-certificates
# For CentOS 7 or RHEL 8
sudo yum update ca-certificates
Verifying the Fix
Don't just restart Nginx and hope for the best. Always validate your syntax first to avoid downtime:
nginx -t
If the syntax is valid, reload the service to apply the changes without dropping active connections:
systemctl reload nginx
Monitor your logs using tail -f /var/log/nginx/error.log while you refresh your browser. You should see the 14090086 errors disappear, replaced by successful 200 OK entries in your access logs.
Pro-Tip: Check Your File Integrity
When moving certificate files between servers via SCP or copy-paste, files can occasionally get corrupted or truncated. A single missing character in a PEM file will break the entire handshake process.
I recommend verifying the integrity of your .crt files by comparing hashes. Using a tool like the Hash Generator at ToolCraft allows you to generate SHA-256 hashes locally in your browser. If the hash on your local machine matches the hash of the file on the Nginx server, you can be 100% sure the file is intact. This saves hours of troubleshooting phantom configuration issues.

