SSL Certificate Verify Failed — What It Means and the 6 Real Causes
The Error, Translated
"SSL: CERTIFICATE_VERIFY_FAILED" (Python), "UNABLE_TO_VERIFY_LEAF_SIGNATURE" (Node), "SSL certificate problem: unable to get local issuer certificate" (curl) — all one sentence: the client checked the server's certificate against its trust store, and the check failed. The connection is deliberately refused before any data is sent.
This is a security feature working as designed. The question is always which layer broke the trust chain. Six causes cover roughly everything.
Cause 1: Missing Local Certificates (Python, Fresh Installs)
The classic on macOS: python.org's installer historically didn't run its certificate script. Python then can't verify anything.
# macOS python.org install:
/Applications/Python 3.x/Install Certificates.command
# verify:
python3 -c "import ssl; print(ssl.get_default_verify_paths())"Cause 2: Corporate Proxy or Antivirus Interception
Corporate networks inspect TLS traffic by re-signing it with their own root certificate. Your machine needs that root cert in its trust store, or every HTTPS call fails inside the network while working fine at home.
# check the actual chain the server presents:
openssl s_client -connect api.example.com:443 -showcertsIf the issuer shown is your company's name (not DigiCert/Let's Encrypt/Google Trust), you need the corporate root cert installed — ask IT for the .crt file, or on macOS: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain corp-root.crt.
Cause 3: Expired or Incomplete Chain on the Server (If You Run the Server)
If the failure only happens from some networks, your server is likely sending an incomplete chain — it works in your browser because browsers cache intermediates, but curl and API clients fail.
# what does your server actually send?
openssl s_client -connect yourdomain.com:443 -showcerts < /dev/nullFix: concatenate the full chain (your cert + intermediate + root-issuer intermediate) into the file your web server serves. The classic nginx mistake is pointing ssl_certificate at only the leaf certificate.
Cause 4: Certificate Actually Expired
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -datesIf it's expired: renew. If it's a certificate you rely on as a client (pinned CA, VPN internal CA), update the trust store — not your code.
Cause 5: Clock Skew
A machine whose clock drifted (dead CMOS battery on old hardware, VM time sync broken) sees valid certificates as "not yet valid" or expired. Check with date -u and compare against a known time. People burn hours on this one because it never occurs to anyone to check the clock.
Cause 6: Hostname Mismatch
The cert is valid but for a different hostname — common when testing against an IP, an internal hostname, or a stale DNS entry pointing somewhere else. The error usually says it directly: "hostname 'x' doesn't match either of 'a', 'b'." Fix the hostname you're calling, not the trust settings.
The Tempting Wrong Answers
verify=Falsein requests /rejectUnauthorized: falsein Node /curl -k— disables all verification permanently in that code path. Fine as a 30-second diagnostic, dangerous if committed. Machines that disable verification still get "secured" connections to attackers.pip install --trusted-host/npm config set strict-ssl false— the same trap, one abstraction layer up.- Manually deleting the CA bundle — makes things worse; restore it with a package reinstall.
Use the diagnostic bypass only to confirm which cause you have, then fix the cause. If you're debugging an internal service and bypass is genuinely acceptable long-term, scope it to that exact hostname and document why — never globally.
Diagnostic Checklist
openssl s_client -connect host:443 -showcerts— decrypts which layer fails (chain? dates? hostname?)- Check the system clock.
- Check whether the network is corporate (try off-VPN).
- If you own the server: validate the full chain at ssllabs.com/ssltest.
- Only then touch client code — and never as a permanent fix.