Nginx Error: 'upstream SSL certificate verify error: (20:unable to get local issuer certificate)' — Cause, Fix, and Troubleshooting Guide
Fix nginx 'upstream SSL certificate verify error: (20:unable to get local issuer certificate)' from proxy_pass https:// — missing CA bundle, SNI, self-signed.
- #nginx
- #web-server
- #troubleshooting
- #tls
Stuck on this NGINX error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
What this error means
Here nginx is acting as a TLS client: you have proxy_pass https://... and proxy_ssl_verify on;, so nginx validates the backend’s certificate the same way a browser validates a website. OpenSSL error 20 — unable to get local issuer certificate — means nginx received the backend’s leaf certificate but could not build a trust chain to a CA it trusts, because the issuing (intermediate/root) certificate was not found in the trust store nginx is using. The handshake to the upstream is aborted and the client request fails.
2026/07/12 14:22:07 [error] 4471#4471: *55129 upstream SSL certificate verify error: (20:unable to get local issuer certificate) while SSL handshaking to upstream, client: 203.0.113.7, server: api.example.com, request: "GET /v1/status HTTP/1.1", upstream: "https://10.0.5.30:8443/v1/status", host: "api.example.com"
Downstream clients typically see a 502 Bad Gateway, because nginx refuses to forward the request over a connection whose peer it could not verify.
Symptoms at the client
upstream SSL certificate verify error: (20:unable to get local issuer certificate) while SSL handshaking to upstreaminerror.log.- Clients get
502 Bad Gatewayfor routes that useproxy_pass https://; plainhttp://upstreams are unaffected. - The backend serves fine when you hit it directly with a browser or
curlthat has the full system CA bundle. - Related reason codes may appear:
(21:unable to verify the first certificate)(missing intermediate),(18:self signed certificate),(19:self signed certificate in certificate chain),(10:certificate has expired),(62:hostname mismatch). - The error started after enabling
proxy_ssl_verify on;or after the backend rotated to a new CA. openssl s_clientagainst the upstream showsverify error:num=20and an incomplete chain.
Trust and cipher causes
- No trust store configured for the client role —
proxy_ssl_verify on;is set but noproxy_ssl_trusted_certificateis given, so nginx has no CA bundle to validate against (it does not automatically use the OS store unless you point it there). - Backend does not send its intermediate — the upstream serves only its leaf cert, so nginx cannot chain to the root even though the root is trusted. This is the classic
20/21cause and mirrors the “incomplete chain” browser problem. - Private/internal CA not trusted — the backend uses an internal CA (or service mesh CA) whose root/intermediate is not in the bundle nginx loads.
- Self-signed backend certificate — there is no issuer to find at all; verification can never succeed against a public trust store.
- Missing SNI to the upstream — nginx does not send SNI by default when proxying, so the backend returns a default/wrong certificate whose chain does not validate. Fixed with
proxy_ssl_server_name on;. - Wrong or stale CA bundle —
proxy_ssl_trusted_certificatepoints at an old bundle that predates the backend’s CA rotation, or at a file that lacks the needed intermediates.
Inspecting the TLS handshake
Confirm the failing upstream and reason straight from the log:
sudo tail -n 50 /var/log/nginx/error.log | grep -E 'verify error|SSL handshaking to upstream'
See exactly what chain the backend actually presents, and whether it includes the intermediate. Send SNI, since nginx-with-proxy_ssl_server_name will:
# -showcerts prints the full chain the backend sends; -servername sets SNI
openssl s_client -connect 10.0.5.30:8443 -servername api-backend.example.com -showcerts </dev/null 2>/dev/null \
| grep -E 'verify (error|return)|subject=|issuer='
A verify error:num=20 with only ONE certificate in -showcerts means the backend is not sending its intermediate. Validate explicitly against the CA bundle you intend nginx to use:
openssl s_client -connect 10.0.5.30:8443 -servername api-backend.example.com \
-CAfile /etc/nginx/ssl/backend-ca.pem </dev/null 2>/dev/null | grep 'Verify return code'
Inspect the current proxy TLS directives nginx is running:
sudo nginx -T 2>/dev/null | grep -nE 'proxy_ssl_(verify|trusted_certificate|server_name|name|verify_depth)|proxy_pass\s+https'
Check the CA file resolves and contains the expected issuer:
ls -l /etc/nginx/ssl/backend-ca.pem
openssl crl2pkcs7 -nocrl -certfile /etc/nginx/ssl/backend-ca.pem | openssl pkcs7 -print_certs -noout | grep -E 'subject|issuer'
The fix
The correct fix is almost always: give nginx the right CA bundle and send SNI. Point proxy_ssl_trusted_certificate at a bundle that includes the backend’s issuing CA (public roots, or your internal CA), and enable SNI so the backend returns the correct certificate:
location / {
proxy_pass https://backend;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
proxy_ssl_trusted_certificate /etc/nginx/ssl/backend-ca.pem; # CA chain to trust
# Send SNI + verify against the right name (defaults are OFF when proxying)
proxy_ssl_server_name on;
proxy_ssl_name api-backend.example.com;
}
For a public backend, you can point the trusted-certificate at the system CA bundle instead of maintaining your own:
# Debian/Ubuntu path shown; RHEL uses /etc/pki/tls/certs/ca-bundle.crt
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
If the real cause is a missing intermediate on the backend, fix it at the source: have the upstream serve its full chain (leaf + intermediates), e.g. its own ssl_certificate should use fullchain.pem. That is the durable fix; trusting the intermediate on nginx’s side only masks a chain the backend should be sending.
For an internal/self-signed backend you control, add that CA (or the self-signed cert itself) to the trusted file rather than disabling verification:
sudo cat internal-ca.pem >> /etc/nginx/ssl/backend-ca.pem
Disabling verification (proxy_ssl_verify off;) makes the error vanish but leaves nginx open to a man-in-the-middle on the backend hop; treat it only as a temporary, deliberate exception, never the fix.
Validate and reload after editing:
sudo nginx -t && sudo systemctl reload nginx
Certificate lifecycle
proxy_ssl_verify,proxy_ssl_server_name, andproxy_ssl_nameall default off/unset — nginx will not send SNI or verify unless you say so, which surprises people migrating from acurl-tested backend.- Distinguish reason
20/21(missing intermediate — usually a backend misconfiguration) from18/19(self-signed — a trust decision) before choosing a fix. - When the backend rotates CAs, update
proxy_ssl_trusted_certificatein lockstep; a stale bundle produces exactly this error the moment the new chain goes live. - Set a sane
proxy_ssl_verify_depth(2 is typical for leaf+intermediate+root); too shallow a depth can fail an otherwise valid chain. - Prefer fixing the backend to serve
fullchain.pemover widening trust on nginx — it fixes every client, not just the proxy. - Alert on
upstream SSL certificate verify errorand correlate with502spikes so a CA rotation is caught immediately; wire it into your monitoring dashboard.
Related TLS errors
- Nginx Error: Ssl Handshake Certificate — the downstream (client-facing) counterpart of certificate/chain verification failures.
- Nginx Error: Ssl Do Handshake Failed — protocol/cipher negotiation failures on the upstream hop, distinct from certificate verification.
- Nginx Error: 502 Bad Gateway — the client-visible symptom when an upstream TLS verification aborts the request.
See the NGINX category for more guides.
Fixed it? Get 500 NGINX & DevOps AI prompts — free
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.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
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.