Skip to content

Fix: SSL certificate problem: unable to get local issuer certificate

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix 'SSL certificate problem: unable to get local issuer certificate', 'CERT_HAS_EXPIRED', 'ERR_CERT_AUTHORITY_INVALID', and 'self signed certificate in certificate chain' errors in Git, curl, Node.js, Python, Docker, and more. Covers CA certificates, corporate proxies, Let's Encrypt, certificate chains, and self-signed certs.

The Error

You run a git clone, curl, npm install, or any HTTPS request and get one of these:

Git:

fatal: unable to access 'https://github.com/user/repo.git/':
SSL certificate problem: unable to get local issuer certificate

curl:

curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

Node.js:

Error: unable to get local issuer certificate
    at TLSSocket.onConnectSecure (node:_tls_wrap:1674:34)
    code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'

Or one of these related errors:

SSL certificate problem: certificate has expired
Error: CERT_HAS_EXPIRED
ERR_CERT_AUTHORITY_INVALID
SSL certificate problem: self signed certificate in certificate chain

Python (requests/pip):

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed:
unable to get local issuer certificate (_ssl.c:1007)

pip:

WARNING: Retrying (Retry(total=4)) after connection broken by
'SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate'))':
/simple/package-name/

All of these mean the same thing: the client cannot verify the server’s SSL/TLS certificate.

Why This Happens

When you connect to a server over HTTPS, the server sends its SSL certificate. Your client (Git, curl, Node.js, Python, a browser) must verify that certificate by tracing a chain of trust from the server’s certificate up through intermediate certificates to a root Certificate Authority (CA) that your system already trusts.

The verification fails when:

  • Your system’s CA certificate bundle is outdated or missing. The root CA that signed the server’s certificate isn’t in your local trust store.
  • The server’s certificate has expired. The certificate’s validity period has passed.
  • The certificate chain is incomplete. The server isn’t sending the intermediate certificates, so your client can’t trace the chain back to a trusted root.
  • A corporate proxy or firewall is intercepting HTTPS traffic. Your organization performs TLS inspection (MITM), replacing the server’s certificate with one signed by a corporate CA that your tools don’t trust.
  • The certificate is self-signed. It wasn’t issued by any recognized CA.
  • You’re inside a Docker container that ships with a minimal CA bundle or no CA bundle at all.

Why This Error Surged in 2021, and Why It Keeps Coming Back

The single largest spike of this error globally happened on September 30, 2021, when the cross-signed DST Root CA X3 expired. Let’s Encrypt had been cross-signing certificates against that root so older clients (Android 7.0 and earlier, OpenSSL 1.0.2 and earlier, GnuTLS prior to 3.6.14) could validate the chain. When DST Root X3 expired, every client that did not have ISRG Root X1 in its trust store suddenly broke — including a lot of long-running Ubuntu 16.04 boxes, Alpine 3.5/3.6 containers, and pinned Docker images.

Knowing the timeline helps you diagnose:

  • Pre-September 2021 base images (e.g., alpine:3.6, ubuntu:16.04, centos:7 without a ca-certificates update since 2020) still ship a CA bundle without ISRG Root X1 and will fail every Let’s Encrypt-signed handshake. Update or rebuild the image.
  • ca-certificates package cadence varies by distro. Debian ships a new ca-certificates package roughly every Mozilla CA Bundle release (about every 2–3 months). RHEL and CentOS update on a slower cycle. Alpine updates within days but only if you rebuild your image. A Docker image built once and never updated is the most common source of “it worked yesterday” SSL failures.
  • The next root rotations to watch: GlobalSign Root R3 (expires 2029), DigiCert Global Root G2 (expires 2038), and the next Let’s Encrypt root rotation (ISRG Root X2, ECDSA, was added as a backup in 2020). Any client that does not refresh its trust store at least annually is on borrowed time.
  • OpenSSL 1.1.1 reached end of life on September 11, 2023. Distros like Debian 10 and Ubuntu 18.04 that shipped 1.1.1 no longer receive upstream fixes for new edge cases (post-quantum hybrid handshakes, MLS-style extensions). Newer servers may negotiate features 1.1.1 cannot follow. Upgrade to OpenSSL 3.x where possible.
  • Python 3.10+ removed ssl.PROTOCOL_TLS (deprecated since 3.6, alias ssl.PROTOCOL_TLS_CLIENT is the replacement). Old scripts that still import it fail under newer Pythons with what looks like a TLS error.

When this error appears suddenly on a system that has not changed, almost always: a root expired, or your CA bundle is older than you think.

Fix 1: Update Your CA Certificate Bundle

The most common cause on Linux systems. Your CA certificates are outdated and don’t include the root CA that signed the server’s certificate.

Debian/Ubuntu:

sudo apt update && sudo apt install -y ca-certificates
sudo update-ca-certificates

RHEL/CentOS/Fedora:

sudo yum install -y ca-certificates
sudo update-ca-trust

Alpine (common in Docker):

apk add --no-cache ca-certificates
update-ca-certificates

macOS:

CA certificates come from the system Keychain. Update macOS to get the latest root certificates:

softwareupdate --install --all

If you’re using Homebrew’s OpenSSL or curl, make sure they can find the certificates:

brew install ca-certificates

Windows:

Windows manages root certificates automatically through Windows Update. Run Windows Update to get the latest root CAs.

If you’re using Git for Windows and it bundles its own CA file, update Git for Windows to the latest version.

Fix 2: Git – Configure the CA Bundle

Git uses its own SSL backend and sometimes can’t find the system CA bundle.

Find where Git looks for certificates:

git config --global http.sslCAInfo

If this is empty or points to a nonexistent file, set it to the correct path:

Linux:

git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt

macOS (Homebrew):

git config --global http.sslCAInfo "$(brew --prefix)/share/ca-certificates/cacert.pem"

Or download an up-to-date CA bundle from curl’s website:

curl -o ~/cacert.pem https://curl.se/ca/cacert.pem
git config --global http.sslCAInfo ~/cacert.pem

The http.sslVerify false Shortcut (Use With Caution)

You’ll find this everywhere online:

git config --global http.sslVerify false

This disables SSL verification entirely. Do not use this in production or on public networks. It makes you vulnerable to man-in-the-middle attacks. Use it only as a temporary diagnostic step to confirm the issue is certificate-related, then find the real fix.

If you must use it, scope it to a single repository instead of setting it globally:

git config http.sslVerify false  # Only affects the current repo

Or for a one-off clone:

GIT_SSL_NO_VERIFY=true git clone https://example.com/repo.git

Fix 3: curl – Specify a CA Bundle or Diagnose

Check what CA bundle curl is using:

curl -vI https://example.com 2>&1 | grep CAfile

Specify a CA bundle manually:

curl --cacert /etc/ssl/certs/ca-certificates.crt https://example.com

The -k flag disables verification:

curl -k https://example.com

Same warning as Git: -k / --insecure skips all certificate checks. Fine for debugging, dangerous for anything else. Never pipe curl -k output to sh or use it to download files you’ll execute.

Inspect the server’s certificate chain:

openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null

This shows every certificate the server sends. Look for:

  • Certificate chain completeness — you should see the server cert, intermediate cert(s), and optionally the root.
  • Expiration dates — check Not After for each certificate.
  • The issuer — see if it’s a recognized CA or a corporate/self-signed issuer.

Fix 4: Node.js – Set the CA or Fix the Environment

Node.js uses its own compiled-in CA bundle (from Mozilla’s trust store), not the system’s. When you see UNABLE_TO_GET_ISSUER_CERT_LOCALLY in Node.js, the certificate’s CA isn’t in Node’s bundle.

Option A: Point Node.js to an extra CA file:

export NODE_EXTRA_CA_CERTS=/path/to/your/ca-bundle.crt
node app.js

This adds certificates from the specified file to Node’s built-in CAs. This is the correct fix for corporate environments where a custom CA signs MITM certificates.

Option B: The nuclear option (do not use in production):

export NODE_TLS_REJECT_UNAUTHORIZED=0
node app.js

This disables all certificate verification for every TLS connection in your Node.js process. This is dangerous. It means any certificate, including a malicious one, will be accepted. Never deploy code with this setting. It exists for debugging only.

If you see NODE_TLS_REJECT_UNAUTHORIZED in someone’s production code, that’s a security vulnerability.

Option C: Add the CA in code (for specific connections):

const https = require('https');
const fs = require('fs');

const ca = fs.readFileSync('/path/to/corporate-ca.crt');

https.get('https://internal.example.com', { ca }, (res) => {
  // ...
});

For npm specifically (see also Fix: npm EACCES Permission Denied for related npm issues):

npm config set cafile /path/to/your/ca-bundle.crt

Fix 5: Python / pip – SSL Certificate Verification

pip (if you’re also hitting build errors, see Fix: pip Could Not Build Wheels):

pip install --cert /path/to/ca-bundle.crt package-name

Or permanently:

pip config set global.cert /path/to/ca-bundle.crt

Python requests library:

import requests

# Specify CA bundle
response = requests.get('https://example.com', verify='/path/to/ca-bundle.crt')

# Or disable verification (debugging only)
response = requests.get('https://example.com', verify=False)

macOS Python from python.org:

Python installed from python.org on macOS doesn’t use the system certificates. Run the included script to install the certifi package:

/Applications/Python\ 3.x/Install\ Certificates.command

Or install certifi manually:

pip install certifi
python -c "import certifi; print(certifi.where())"

This prints the path to the CA bundle that Python’s requests and urllib3 use. If you need to add a custom CA, append it to that file.

Fix 6: Docker – Add CA Certificates to Your Image

Minimal Docker base images (especially Alpine-based) often don’t include CA certificates.

Alpine:

FROM node:20-alpine
RUN apk add --no-cache ca-certificates

Debian/Ubuntu-based:

FROM python:3.12-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

Adding a custom/corporate CA to a Docker image (for other Docker issues, see Fix: Docker COPY Failed: File Not Found):

FROM ubuntu:24.04
COPY corporate-ca.crt /usr/local/share/ca-certificates/corporate-ca.crt
RUN update-ca-certificates

The certificate file must have a .crt extension and be in PEM format for update-ca-certificates to pick it up.

Real-world scenario: In corporate environments, the #1 cause of this error is TLS-intercepting proxies (Zscaler, Fortinet, etc.). Your browser works fine because IT pushed the corporate CA to your system trust store, but Git, Node.js, and pip use their own CA bundles that don’t include it. Ask your IT department for the root CA certificate in PEM format.

Fix 7: Corporate Proxy / Firewall MITM Certificates

Many corporate networks run a TLS-intercepting proxy (Zscaler, Fortinet, BlueCoat, etc.). The proxy terminates your HTTPS connection, inspects the traffic, then re-encrypts it with a certificate signed by the corporation’s own CA.

Your browser trusts this CA because IT pushed it to your system trust store. But Git, Node.js, Python, and curl may use their own CA bundles that don’t include it.

Step 1: Get the corporate CA certificate.

Ask your IT department for the root CA certificate in PEM format. Or extract it yourself:

# Connect to any HTTPS site through the proxy and grab the CA
openssl s_client -connect google.com:443 -showcerts </dev/null 2>/dev/null | \
  awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ print }' > chain.pem

The last certificate in the output is usually the root CA.

Step 2: Add it to your system trust store.

On Ubuntu/Debian:

sudo cp corporate-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

On RHEL/CentOS/Fedora:

sudo cp corporate-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

Step 3: Configure individual tools.

Even after adding to the system store, some tools need explicit configuration:

# Git
git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt

# npm
npm config set cafile /etc/ssl/certs/ca-certificates.crt

# pip
pip config set global.cert /etc/ssl/certs/ca-certificates.crt

# Node.js
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt

Fix 8: Expired Certificates (Server-Side)

If the server’s certificate has expired, the fix is on the server side.

Check certificate expiration:

echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

Output:

notBefore=Jan  1 00:00:00 2025 GMT
notAfter=Apr  1 00:00:00 2025 GMT

If notAfter is in the past, the certificate has expired.

Renew with Let’s Encrypt (certbot):

sudo certbot renew
sudo systemctl reload nginx   # or apache2 / httpd

If auto-renewal isn’t set up, add it:

sudo certbot renew --dry-run   # Test first

Then add a cron job or systemd timer:

# /etc/cron.d/certbot
0 0,12 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"

Let’s Encrypt root CA changes: In 2021, Let’s Encrypt switched from the DST Root CA X3 (cross-signed) to the ISRG Root X1 root. Older systems that don’t have ISRG Root X1 in their trust store will fail to verify Let’s Encrypt certificates. The fix is to update your CA certificates (Fix 1).

Fix 9: Certificate Chain Order (Server Misconfiguration)

The server must send certificates in order: server cert first, then each intermediate up to (but not necessarily including) the root. If the order is wrong or intermediates are missing, clients can’t build the trust chain.

Test your certificate chain:

openssl s_client -connect example.com:443 </dev/null 2>/dev/null

Look for Verify return code:

  • 0 (ok) — chain is valid.
  • 21 (unable to verify the first certificate) — missing intermediate certificate.
  • 10 (certificate has expired) — expired certificate in the chain.

Check with an online tool: SSL Labs Server Test gives a detailed chain analysis and flags missing intermediates.

Fix the chain in Nginx:

Your certificate file should contain the server certificate followed by the intermediate(s):

cat server.crt intermediate.crt > fullchain.crt

In nginx.conf:

ssl_certificate     /etc/ssl/fullchain.crt;
ssl_certificate_key /etc/ssl/server.key;

Fix the chain in Apache:

SSLCertificateFile    /etc/ssl/server.crt
SSLCertificateChainFile /etc/ssl/intermediate.crt
SSLCertificateKeyFile /etc/ssl/server.key

Let’s Encrypt’s certbot handles this automatically — the fullchain.pem file includes both the server cert and the intermediate.

Fix 10: Self-Signed Certificates in Development

Self-signed certificates are not signed by any CA, so every client will reject them by default. This is normal and expected.

Generate a self-signed certificate for local development:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
  -subj "/CN=localhost"

Trust it on your machine:

On macOS:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem

On Ubuntu/Debian:

sudo cp cert.pem /usr/local/share/ca-certificates/localhost-dev.crt
sudo update-ca-certificates

On Windows, double-click the .crt file, install it to the “Trusted Root Certification Authorities” store.

Better alternative for local dev: Use mkcert to create locally-trusted development certificates. It installs a local CA in your system trust store and generates certificates signed by that CA:

mkcert -install
mkcert localhost 127.0.0.1 ::1

This creates localhost+2.pem and localhost+2-key.pem that are automatically trusted by your browsers and system tools. No more SSL errors in development.

Still Not Working?

  1. Stale certificate cache. Some applications cache SSL sessions and certificates. Restart the application, clear browser cache, or restart your shell session after updating CA certificates.

  2. Wrong system time. SSL certificate validation checks the current date against the certificate’s validity period. If your system clock is significantly off, valid certificates appear expired. Check with date and sync with NTP:

    sudo timedatectl set-ntp true
  3. SNI (Server Name Indication) issues. If the server hosts multiple domains on one IP, older clients that don’t send SNI may receive the wrong certificate. Test with:

    openssl s_client -connect example.com:443 -servername example.com </dev/null

    The -servername flag sends the SNI header.

  4. VPN or proxy overriding DNS. Your VPN might resolve the hostname to an internal IP that serves a different certificate. Check what IP you’re actually connecting to:

    dig +short example.com
    curl -v https://example.com 2>&1 | grep "Connected to"
  5. The SSL_CERT_FILE or SSL_CERT_DIR environment variables are set incorrectly. Some tools respect these OpenSSL environment variables. If they point to a nonexistent or incomplete CA bundle, verification fails even though the system store is fine:

    echo $SSL_CERT_FILE
    echo $SSL_CERT_DIR

    Unset them to use the defaults, or point them to the correct location.

  6. WSL2 doesn’t share Windows certificate stores. If you’re running Git, curl, or Node.js inside WSL2, the Linux environment has its own CA store separate from Windows. You need to add corporate or custom CAs inside WSL2 as well. See Fix 7 for how to add certificates to the Linux trust store.

  7. Java/JVM applications use their own trust store. Java doesn’t use the system CA bundle. It uses a cacerts keystore file. Add your CA with:

    keytool -importcert -alias corporate-ca -file corporate-ca.crt \
      -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt
  8. Certificate pinning. Some applications pin specific certificates or public keys. Even if the certificate is valid, if it doesn’t match the pinned value, the connection fails. This is common in mobile apps and some security-conscious services. You can’t fix this by updating CA bundles — the application itself needs to be updated.

  9. OCSP stapling failures. If the server has OCSP stapling enabled but the stapled response is invalid or expired, some clients reject the connection. Check with:

    openssl s_client -connect example.com:443 -status </dev/null 2>/dev/null | grep -A 5 "OCSP"

    On the server side (Nginx), ensure OCSP stapling is configured correctly:

    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
  10. certifi version inside a long-lived virtualenv. Python’s requests, httpx, and most async HTTP libraries use the certifi package’s bundled CA list, not the system trust store. A virtualenv created in 2022 has a 2022 certifi, which predates the ISRG X1 transition for some chains. Run pip install --upgrade certifi inside the venv, then verify with python -c "import certifi; print(certifi.where())". See Fix: pip SSL Certificate Verify Failed for pip-specific certifi handling.

  11. Git’s bundled OpenSSL vs the system OpenSSL. Git for Windows ships its own OpenSSL and its own CA file (mingw64/ssl/certs/ca-bundle.crt). On Linux, distro Git uses the system OpenSSL. If curl https://... works but git clone https://... fails, suspect Git’s bundled CA file is older than your system trust store. Reinstall the latest Git for Windows, or point Git at the system bundle: git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt.

  12. The server uses a private CA you previously trusted but the trust expired. Corporate internal CAs sometimes issue 1-year root certificates. If you added the CA a year ago, it has now expired and is silently rejected. Run openssl x509 -in /usr/local/share/ca-certificates/corporate-ca.crt -noout -enddate to check, and ask IT for the renewed root.

  13. Python ssl module with system openssl mismatch on macOS Homebrew. A Python built against Homebrew’s OpenSSL 3 but a CLI tool using LibreSSL bundled with macOS will report different errors for the same server. Confirm with python -c "import ssl; print(ssl.OPENSSL_VERSION)" and openssl version. If they disagree, set SSL_CERT_FILE explicitly to the Homebrew bundle path.


Related: If your Git connection fails with an SSH key issue instead of SSL, see Fix: Permission denied (publickey). If your Nginx reverse proxy returns a 502 after configuring SSL, see Fix: Nginx 502 Bad Gateway. For connection issues to local development servers, see Fix: ERR_CONNECTION_REFUSED on localhost.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles