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

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.

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.

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;

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.

Related Articles