"unable to get local issuer certificate" means the client checked the server's certificate and couldn't find the certificate that signed it, so it couldn't connect it to a certificate authority it trusts. Most of the time the cause is on the server: it sends its own certificate but not the intermediate between it and the root. Browsers often hide this by filling the gap themselves, which is why the site can look fine in Chrome while curl, Python, Node or Git fail. Less often the client is at fault: an outdated CA bundle, or a corporate proxy re-signing traffic with a root the client doesn't trust.
What It Looks Like
The message comes from OpenSSL, so it reads almost the same everywhere OpenSSL is underneath:
# curl
curl: (60) SSL certificate problem: unable to get local issuer certificate
# Python (requests / urllib)
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: unable to get local issuer certificate (_ssl.c:1006)
# Git
fatal: unable to access 'https://git.example.com/repo.git/': SSL certificate
problem: unable to get local issuer certificate
# Node.js
Error: unable to verify the first certificate
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'Node words it differently, but UNABLE_TO_VERIFY_LEAF_SIGNATURE is the same failure: the server's certificate couldn't be linked to a trusted CA.
How a Chain Is Verified
A server certificate is signed by an intermediate certificate, which is signed by a root. Clients trust only the roots in their trust store, so the server must send its own certificate and the intermediates, letting the client walk from one to the next until it reaches a root it has. If a link is missing, the walk stops and the client reports that it can't get the issuer.
What the server should send What the client already has
--------------------------- ---------------------------
0 www.example.com (leaf)
1 R11 (intermediate) --> signed by ISRG Root X1 (in trust store)Why It Happens
1. The server sends only its own certificate
The most common cause. The server is configured with the leaf certificate alone, for example Certbot's cert.pem instead of fullchain.pem, or a CA download where the intermediate came in a separate file that was never installed.
2. The wrong or an outdated intermediate
The chain file holds an intermediate that didn't sign this certificate, often left over from an older certificate after the CA changed intermediates at renewal.
3. The client's CA bundle is out of date
The server is fine, but the client's trust store doesn't include the root, typically on old operating systems, minimal Docker images without ca-certificates, or Python on macOS before its certificate installer has been run.
4. A proxy is re-signing the traffic
Corporate firewalls and some antivirus products decrypt HTTPS and re-sign it with their own root. Every HTTPS request from that network fails in tools that don't trust that root.
5. A private or self-signed CA
Internal services signed by a company CA fail on any machine that hasn't had that CA's root installed.
How to Diagnose It
Ask the server what it sends. -showcerts prints every certificate in the chain; a healthy server sends at least two:
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null
# Incomplete chain — only the leaf is sent:
# Certificate chain
# 0 s:CN = www.example.com
# i:C = US, O = Let's Encrypt, CN = R11
# ...
# Verify return code: 21 (unable to verify the first certificate)
# Complete chain:
# 0 s:CN = www.example.com i:CN = R11
# 1 s:CN = R11 i:CN = ISRG Root X1
# Verify return code: 0 (ok)How to Fix It — Server Side
Fix A — Serve the full chain
Point the server at a file containing your certificate followed by every intermediate, leaf first. With Certbot that file is fullchain.pem:
# Nginx: the leaf and intermediates in one file
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Apache 2.4.8+: the same full-chain file
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# Older Apache: leaf and chain in separate directives
SSLCertificateFile /etc/ssl/example.com.crt
SSLCertificateChainFile /etc/ssl/example.com.chain.crtFix B — Build the chain file by hand when the CA sent separate files
Concatenate your certificate and the intermediate your CA provided, in that order, then reload the server:
cat example.com.crt intermediate.crt > fullchain.pem
# Check the intermediate really signed your certificate:
openssl verify -untrusted intermediate.crt example.com.crt
# example.com.crt: OK
sudo nginx -t && sudo systemctl reload nginxHow to Fix It — Client Side
Fix C — Update the CA bundle
# Debian / Ubuntu (and Docker images based on them)
sudo apt-get install --reinstall ca-certificates && sudo update-ca-certificates
# Alpine
apk add --no-cache ca-certificates
# Python: refresh the bundle requests uses
pip install --upgrade certifi
# macOS python.org installs: run "Install Certificates.command" in the Python folderFix D — Trust your proxy's or company's root
Add the root certificate to the system store, and to tools that keep their own list. Don't disable verification to work around it.
# System store (Debian / Ubuntu)
sudo cp company-root.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates
# Node.js
export NODE_EXTRA_CA_CERTS=/path/to/company-root.crt
# Git
git config --global http.sslCAInfo /path/to/company-root.crtVerify the Fix
From a machine that was failing, connect again without any workaround. OpenSSL should end with Verify return code: 0 (ok) and curl should complete the request:
openssl s_client -connect www.example.com:443 -servername www.example.com </dev/null 2>/dev/null \
| grep "Verify return code"
# Verify return code: 0 (ok)
curl -sS -o /dev/null -w "%{http_code}\n" https://www.example.com/
# 200Frequently Asked Questions
1. The site works in my browser. Why does curl or my app fail?
Browsers quietly repair an incomplete chain: they cache intermediates from sites you visited earlier and can download a missing one using the address inside the certificate (AIA fetching). curl, OpenSSL, Python, Node, Java and most API clients don’t, so they fail on the same server. A working browser doesn’t prove the chain is complete; the Certificate Chain Checker does.
2. Should I include the root certificate in my chain file?
No need. Clients already have the root in their trust store, and it must come from there to be trusted at all, so sending it only adds bytes to every handshake. Send the leaf certificate followed by every intermediate, in order.
3. Is it safe to use curl -k or verify=False to get past it?
Only for a one-off test against a server you control. Turning verification off accepts any certificate, including an attacker’s, so leaving it in scripts or production code removes the protection TLS exists for. Fix the chain or the client’s CA bundle instead.
4. Why did this start after a renewal when nothing else changed?
The renewal probably installed cert.pem (the leaf only) where fullchain.pem was used before, or the CA moved to a new intermediate and your configuration still points at a chain file containing the old one. Point the server at the full chain the CA issued with this certificate.
5. What is the difference between verify error 20 and error 21?
OpenSSL error 20 (unable to get local issuer certificate) means it couldn’t find the certificate that signed the one it was checking. Error 21 (unable to verify the first certificate) usually follows it and means even the server’s own certificate couldn’t be linked to anything. Together they almost always mean the server sent only its leaf certificate.
6. We’re behind a corporate proxy and every HTTPS call fails this way.
The proxy is decrypting traffic and re-signing it with the company’s own root certificate, which your tools don’t trust. Get that root from IT and add it to the system trust store (and to tools that keep their own, such as Python’s certifi, Node’s NODE_EXTRA_CA_CERTS or the Java keystore).