unable to get local issuer certificate
The error "unable to get local issuer certificate" (OpenSSL verify error 20) means the verifier could not find the CA certificate that signed the certificate it is checking. There are only two causes: the server is not sending its intermediate CA certificates, or the root CA is missing from the local trust store. Run `openssl s_client -connect example.com:443 -showcerts` and count the certificates returned — if only one comes back, the server is misconfigured and you must append the intermediates to its certificate file; if the full chain comes back, the missing root is a local trust store problem.
What the error actually means #
Verification walks a chain. Your server certificate says “I was signed by Example Intermediate CA”, so the verifier looks for a certificate belonging to Example Intermediate CA, then asks who signed that, and continues until it reaches a certificate in the local trust store.
This error is emitted at the moment that walk fails: the verifier holds a certificate, knows the name of its issuer, and cannot find a certificate for that issuer anywhere. It is a missing link, not an invalid signature and not an expired certificate.
There are exactly two places the link can be missing from, and the fix is completely different for each.
Step 1 — find out which cause you have #
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null Count the -----BEGIN CERTIFICATE----- blocks in the output:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -c "BEGIN CERTIFICATE" | What you see | Cause | Go to |
|---|---|---|
| 1 certificate returned, and the CA is a public one | The server is not sending its intermediates | Cause A |
| 2 or more returned, still fails | The root is not in your local trust store | Cause B |
Verify return code: 0 (ok) |
Chain is fine; your application is using a different trust store | Cause B |
Cause A — the server is not sending its intermediates #
A TLS server is required to send its own certificate and every intermediate needed to reach a trusted root. Serving only the end-entity certificate is the single most common cause of this error.
The fix is to concatenate the certificates into one file, leaf first, then each intermediate in order, walking up toward the root:
cat certificate.crt intermediate.crt > fullchain.crt Then point the server at the combined file:
nginx has no separate chain directive — ssl_certificate must point at the concatenated file.
ssl_certificate /etc/ssl/certs/fullchain.crt;
ssl_certificate_key /etc/ssl/private/private.key; Apache 2.4.8 and later also read the chain from the certificate file.
SSLCertificateFile /etc/ssl/certs/fullchain.crt
SSLCertificateKeyFile /etc/ssl/private/private.key HAProxy expects the key appended to the same PEM file as the chain.
cat certificate.crt intermediate.crt private.key > /etc/haproxy/certs/site.pem Reload the service, then confirm the chain is complete:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| grep "Verify return code" Expected output
Verify return code: 0 (ok)
Where do I get the intermediate? #
Your CA publishes it, and the certificate itself tells you where. Read the CA Issuers URL out of the Authority Information Access extension:
openssl x509 -in certificate.crt -noout -text \
| grep -A1 "Authority Information Access" Expected output
Authority Information Access: OCSP - URI:http://ocsp.example-ca.com CA Issuers - URI:http://crt.example-ca.com/intermediate.der
That file is usually DER-encoded, so convert it to PEM before concatenating it.
Cause B — the root is not in your trust store #
If the server returns a complete chain and verification still fails, the root CA at the top of that chain is not trusted by whatever is doing the verifying. This is normal for a private or corporate CA, and for containers built FROM scratch that ship no CA bundle at all.
You can confirm it by verifying against the root explicitly. If this succeeds, the certificate is fine and only trust is missing:
openssl verify -CAfile root-ca.crt -untrusted intermediate.crt certificate.crt Expected output
certificate.crt: OK
Then install the root:
sudo cp root-ca.crt /usr/local/share/ca-certificates/root-ca.crt
sudo update-ca-certificates The file must have a .crt extension and contain PEM, or update-ca-certificates silently skips it.
sudo cp root-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract sudo cp root-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates A bare alpine image has no trust store at all until you apk add ca-certificates.
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain root-ca.crt Runtimes that ignore the system trust store #
Several runtimes ship their own CA bundle and will keep failing after you have fixed the system store. Point each at the right bundle:
| Runtime | How to add the root |
|---|---|
| Node.js | NODE_EXTRA_CA_CERTS=/path/to/root-ca.crt |
Python requests |
REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt |
Python ssl / urllib |
SSL_CERT_FILE=/path/to/ca-bundle.crt |
| Git | git config --global http.sslCAInfo /path/to/ca-bundle.crt |
| Java | keytool -importcert -trustcacerts -alias corp-root -file root-ca.crt -keystore $JAVA_HOME/lib/security/cacerts |
| curl | curl --cacert /path/to/root-ca.crt |
What not to do #
Frequently asked questions
- What does "verify error num=20" mean?
- It is the numeric form of this same error. OpenSSL returns error code 20,
X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, when it cannot locate the issuer of a certificate it is trying to verify. - Why does the site work in my browser but fail with curl?
- Browsers quietly repair incomplete chains. They cache intermediates from previous visits and follow the Authority Information Access extension to fetch a missing intermediate over HTTP. OpenSSL and curl do neither, so a chain that is genuinely broken only fails outside the browser. The server is still misconfigured.
- Is -k or verify=False a valid fix?
- No. Both disable certificate verification entirely, which removes the protection TLS exists to provide and makes the connection trivially interceptable. Use them only to confirm a diagnosis, never in anything that ships.
- How do I fix this in Python requests?
- The underlying cause is the same. Point requests at a CA bundle that contains the missing issuer with
requests.get(url, verify='/path/to/ca-bundle.crt'), or installcertifiand make sure the corporate root is appended to its bundle.
References
- openssl-verify(1) — verify error 20 and the flags that resolve it
- RFC 4158: certification path building — why a verifier can fail to find a path that exists