Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 9 min read Last reviewed Jul 2026

Nginx Error: 'upstream SSL certificate verify error: (20:unable to get local issuer certificate)' — Cause, Fix, and Troubleshooting Guide

Quick answer

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
Free toolkit

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 20unable 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 upstream in error.log.
  • Clients get 502 Bad Gateway for routes that use proxy_pass https://; plain http:// upstreams are unaffected.
  • The backend serves fine when you hit it directly with a browser or curl that 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_client against the upstream shows verify error:num=20 and an incomplete chain.

Trust and cipher causes

  • No trust store configured for the client roleproxy_ssl_verify on; is set but no proxy_ssl_trusted_certificate is 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/21 cause 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 bundleproxy_ssl_trusted_certificate points 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, and proxy_ssl_name all default off/unset — nginx will not send SNI or verify unless you say so, which surprises people migrating from a curl-tested backend.
  • Distinguish reason 20/21 (missing intermediate — usually a backend misconfiguration) from 18/19 (self-signed — a trust decision) before choosing a fix.
  • When the backend rotates CAs, update proxy_ssl_trusted_certificate in 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.pem over widening trust on nginx — it fixes every client, not just the proxy.
  • Alert on upstream SSL certificate verify error and correlate with 502 spikes so a CA rotation is caught immediately; wire it into your monitoring dashboard.

See the NGINX category for more guides.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.