← All Articles

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 -showcerts

If 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/null

Fix: 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 -dates

If 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=False in requests / rejectUnauthorized: false in 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

  1. openssl s_client -connect host:443 -showcerts — decrypts which layer fails (chain? dates? hostname?)
  2. Check the system clock.
  3. Check whether the network is corporate (try off-VPN).
  4. If you own the server: validate the full chain at ssllabs.com/ssltest.
  5. Only then touch client code — and never as a permanent fix.