Fix: SSL certificate problem: unable to get local issuer certificate
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 certificatecurl:
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.htmlNode.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 expiredError: CERT_HAS_EXPIREDERR_CERT_AUTHORITY_INVALIDSSL certificate problem: self signed certificate in certificate chainPython (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:7without aca-certificatesupdate since 2020) still ship a CA bundle withoutISRG Root X1and will fail every Let’s Encrypt-signed handshake. Update or rebuild the image. ca-certificatespackage cadence varies by distro. Debian ships a newca-certificatespackage 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, aliasssl.PROTOCOL_TLS_CLIENTis 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-certificatesRHEL/CentOS/Fedora:
sudo yum install -y ca-certificates
sudo update-ca-trustAlpine (common in Docker):
apk add --no-cache ca-certificates
update-ca-certificatesmacOS:
CA certificates come from the system Keychain. Update macOS to get the latest root certificates:
softwareupdate --install --allIf you’re using Homebrew’s OpenSSL or curl, make sure they can find the certificates:
brew install ca-certificatesWindows:
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.sslCAInfoIf 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.crtmacOS (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.pemThe http.sslVerify false Shortcut (Use With Caution)
You’ll find this everywhere online:
git config --global http.sslVerify falseThis 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 repoOr for a one-off clone:
GIT_SSL_NO_VERIFY=true git clone https://example.com/repo.gitFix 3: curl – Specify a CA Bundle or Diagnose
Check what CA bundle curl is using:
curl -vI https://example.com 2>&1 | grep CAfileSpecify a CA bundle manually:
curl --cacert /etc/ssl/certs/ca-certificates.crt https://example.comThe -k flag disables verification:
curl -k https://example.comSame 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/nullThis 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 Afterfor 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.jsThis 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.jsThis 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.crtFix 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-nameOr permanently:
pip config set global.cert /path/to/ca-bundle.crtPython 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.commandOr 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-certificatesDebian/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-certificatesThe 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.pemThe 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-certificatesOn RHEL/CentOS/Fedora:
sudo cp corporate-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trustStep 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.crtFix 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 -datesOutput:
notBefore=Jan 1 00:00:00 2025 GMT
notAfter=Apr 1 00:00:00 2025 GMTIf notAfter is in the past, the certificate has expired.
Renew with Let’s Encrypt (certbot):
sudo certbot renew
sudo systemctl reload nginx # or apache2 / httpdIf auto-renewal isn’t set up, add it:
sudo certbot renew --dry-run # Test firstThen 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/nullLook 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.crtIn 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.keyLet’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.pemOn Ubuntu/Debian:
sudo cp cert.pem /usr/local/share/ca-certificates/localhost-dev.crt
sudo update-ca-certificatesOn 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 ::1This 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?
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.
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
dateand sync with NTP:sudo timedatectl set-ntp trueSNI (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/nullThe
-servernameflag sends the SNI header.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"The
SSL_CERT_FILEorSSL_CERT_DIRenvironment 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_DIRUnset them to use the defaults, or point them to the correct location.
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.
Java/JVM applications use their own trust store. Java doesn’t use the system CA bundle. It uses a
cacertskeystore file. Add your CA with:keytool -importcert -alias corporate-ca -file corporate-ca.crt \ -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -nopromptCertificate 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.
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;certifi version inside a long-lived virtualenv. Python’s
requests,httpx, and most async HTTP libraries use thecertifipackage’s bundled CA list, not the system trust store. A virtualenv created in 2022 has a 2022certifi, which predates the ISRG X1 transition for some chains. Runpip install --upgrade certifiinside the venv, then verify withpython -c "import certifi; print(certifi.where())". See Fix: pip SSL Certificate Verify Failed for pip-specific certifi handling.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. Ifcurl https://...works butgit 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.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 -enddateto check, and ask IT for the renewed root.Python
sslmodule with systemopensslmismatch 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 withpython -c "import ssl; print(ssl.OPENSSL_VERSION)"andopenssl version. If they disagree, setSSL_CERT_FILEexplicitly 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: curl: (7) Failed to connect / (6) Could not resolve host / (28) Operation timed out
How to fix curl errors including 'Failed to connect to host', 'Could not resolve host', 'Operation timed out', and 'SSL certificate problem'. Covers curl exit codes 6, 7, 28, 35, 56, and 60, DNS resolution, proxy settings, timeout tuning, SSL issues, retry strategies, verbose debugging, and more.
Fix: Docker Container Keeps Restarting
How to fix a Docker container that keeps restarting — reading exit codes, debugging CrashLoopBackOff, fixing entrypoint errors, missing env vars, out-of-memory kills, and restart policy misconfiguration.
Fix: Certbot Certificate Renewal Failed (Let's Encrypt)
How to fix Certbot certificate renewal failures — domain validation errors, port 80 blocked, nginx config issues, permissions, and automating renewals with systemd or cron.
Fix: Docker Compose Environment Variables Not Loading from .env File
How to fix Docker Compose not loading environment variables from .env files — why variables are empty or undefined inside containers, the difference between env_file and variable substitution, and how to debug env var issues.