unable to get local issuer certificate: what it actually means.

The most-pasted TLS error on the internet, and the one with the worst advice attached. It has two causes that look identical from the error and are fixed in opposite places. Here is how to tell which one you have in under a minute, and what to do about each.

Written by · Published 2026-09-23 · 7 min read

What the words mean

The message is OpenSSL's, error 20 in its verification code, and it surfaces through everything built on OpenSSL: curl, wget, Python's ssl module and therefore requests and pip, Ruby, PHP, Perl, Git, and most Linux command-line tools. Read it literally. "Local issuer certificate" means the certificate that signed the one it is looking at. "Unable to get" means it looked in the two places it is allowed to look and found nothing.

Those two places are: the certificates the server sent in the handshake, and the client's own trust store. The verifier takes the leaf, reads who issued it, and looks for that issuer first among the served certificates, then among the local roots. If the issuer is an intermediate the server did not send, and it is not a root (intermediates never are), the search fails at the first link and you get error 20. If the served chain is complete but the root it ends in is not in the local store, you get the same error one link higher.

So there are exactly two causes: the server did not send enough, or the client does not have enough. Everything else about this error is working out which.

Server fault or client fault: the one-minute test

Check the same host from a client you did not configure. The SSL certificate chain checker is one: it connects from this server, keeps what yours sent, and tries to build a path with a clean copy of the Mozilla root store.

Chain checker saysMeaningFix where
Incomplete, browsers pass, the rest failThe server is missing an intermediate. Your client is right to refuse it. Every strict client on earth is failing too, you are just the one who noticed.Server
Complete, all clients passThe server is fine. Your client's trust store, environment or network path is the problem.Client
Untrusted root or dead endA private CA, or a certificate from a CA no longer trusted. The server is doing what it was told; the question is whether it should be.Depends

The same test from a shell, if you would rather not leave the terminal:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -c 'BEGIN CERT'
# 1 = the server sends only the leaf: server fault
# 2 or more = the server sends intermediates: probably client fault (read on)

If it is the server

The server sends only the leaf certificate, or the leaf plus the wrong intermediate. This is far and away the most common cause when the error appears for a site you or your company runs, and it hides during testing because browsers repair the chain on the fly. Why your certificate works in Chrome but fails in curl explains the mechanism.

The fix is to serve the full chain: leaf, then each intermediate, in one file, no root. The chain checker generates that file from the correct path; how to build a correct fullchain.pem shows the configuration line for nginx, Apache, HAProxy and the cloud load balancers. Deploy it to every edge, not just the one you tested, and re-run the check.

If you do not run the server, tell whoever does. Send them the chain checker result. Do not work around it on your side; the next client to hit the same server will fail identically, and adding a CA's intermediate to your own trust store is a habit that ends badly.

If it is the client

The server sends a complete chain, other clients accept it, and yours does not. Now the question is what your client is using as a trust store and whether it has the root the chain ends in. Work down this list; in my experience it is nearly always one of the first three.

1. A container with no certificates at all

Alpine and Debian slim images do not ship a CA bundle. Go binaries, curl and Python inside them fail on every HTTPS connection with this error, whatever the server does. The tell is that it fails against every site, including google.com.

# Alpine
RUN apk add --no-cache ca-certificates
# Debian / Ubuntu
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
# Distroless: use the :base or :static image, which include the bundle, not :static-debian12 minus certs

2. An old bundle that lacks a new root

CAs add roots; old trust stores do not have them. Let's Encrypt's ISRG Root X1 was the famous case (missing from Android before 7.1.1 and from anything with a 2015-era bundle), and every CA rotates eventually. The tell is that it fails for sites issued by one CA and works for the rest. Which bundle depends on the client:

ClientTrust storeUpdate
curl, wget, Git, PHP, Ruby, Go on LinuxThe OS bundle, /etc/ssl/certs/ca-certificates.crtapt upgrade ca-certificates / apk upgrade ca-certificates
Python requests, pipThe certifi package, not the OSpip install -U certifi; on macOS with python.org builds, run Install Certificates.command
Node.jsCompiled into the Node binaryUpgrade Node, or NODE_OPTIONS=--use-openssl-ca to use the OS store
Java$JAVA_HOME/lib/security/cacertsUpgrade the JDK; the file is only refreshed with it
Android appsThe device's system storeCannot, on old devices. Serve a chain that reaches a root they have, or use a CA whose root is older

3. Something in the middle is re-signing the traffic

Corporate networks, some antivirus products and some VPNs intercept TLS and present their own certificate, signed by a private CA that is installed on the company laptop's operating system store but not in certifi, Node or Java. Browsers work (they use the OS store); scripts fail. The tell is that openssl s_client from that machine shows an issuer you do not recognise, often with the company name in it, and the same command from a phone on mobile data shows the real one. The fix is to point your tool at the corporate CA bundle: REQUESTS_CA_BUNDLE and SSL_CERT_FILE for Python, NODE_EXTRA_CA_CERTS for Node, keytool -importcert for Java, curl --cacert or ~/.curlrc. Not verify=False.

4. An environment variable pointing at nothing

SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE set to a path that no longer exists, usually inherited from a shell profile or a CI variable. OpenSSL then has an empty trust store and everything fails. env | grep -i cert finds it.

5. The clock

A device with the wrong date sees every certificate as not yet valid or expired and some stacks report it with the generic error rather than a date error. Raspberry Pis without a real-time clock, freshly imaged VMs and long-suspended laptops. date is the check.

The advice to ignore

Search results for this error are full of one-line fixes: curl -k, verify=False, PYTHONHTTPSVERIFY=0, NODE_TLS_REJECT_UNAUTHORIZED=0, git config http.sslVerify false, a Java TrustManager that returns true. Each of them makes the error go away by making the client accept any certificate from anyone, which is to say by removing the thing TLS was providing. They are appropriate for about thirty seconds while you confirm the diagnosis and never in anything that gets committed. If the fault is the server, fix the server. If the fault is the client's trust store, give it the right root. Both take about as long as the workaround and neither leaves a hole.

The same error in other languages

Java reports it as PKIX path building failed: unable to find valid certification path to requested target. Python wraps it as CERTIFICATE_VERIFY_FAILED. Node says unable to verify the first certificate. Go says certificate signed by unknown authority. Android says Trust anchor for certification path not found. Every one of them is the same two-cause problem and the same one-minute test applies: check the server from a clean client first, then look at your own trust store.

All guides · All tools