Programming languages

Python [SSL: CERTIFICATE_VERIFY_FAILED]: causes and fixes

By · Last verified

Error messagessl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed means Python reached the server but could not verify its certificate against the CA certificates it trusts. Usually the server is not sending its intermediate certificate, or the Python installation is looking at an empty, outdated or wrong CA bundle.

Quick fix

A one-certificate chain means fixing the server; otherwise Python's CA bundle is the problem.

Decide whether the server or your Python environment is at fault. First test the server from a shell:

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

If only one certificate comes back and the result is Verify return code: 21 or 20, the server chain is incomplete: fix the server (see missing intermediate certificate). If the chain is complete, fix the client side:

# See where Python's OpenSSL looks for CA certificates
python3 -c "import ssl; print(ssl.get_default_verify_paths())"

# requests: point it at a bundle that contains your root
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

# urllib / ssl: point OpenSSL at the bundle
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt

On macOS with the python.org installer, run /Applications/Python 3.x/Install Certificates.command once per Python version.

Causes

1. The server sends an incomplete chain

An incomplete server chain is the most frequent cause on public sites. OpenSSL needs every intermediate from the server; it does not fetch missing ones.

  • Confirm: openssl s_client -showcerts shows only depth=0, or the SSL checker reports a chain problem.
  • Fix: configure the server with the full chain (for Nginx, fullchain.pem). You cannot fix this permanently from the client.

2. Python cannot find any CA certificates

The python.org macOS installer ships its own OpenSSL without a CA bundle until you run Install Certificates.command. Self-compiled Python, minimal containers and some embedded builds behave the same way.

  • Confirm: ssl.get_default_verify_paths() returns cafile=None and a capath that is empty or missing.
  • Fix: run Install Certificates.command, install the ca-certificates package, or set SSL_CERT_FILE to a valid bundle.

3. requests does not see your corporate or internal root

requests verifies against certifi, which contains only public roots from Mozilla. A root added to the Windows or macOS store is invisible to it.

  • Confirm: urllib succeeds but requests fails, or the server issuer is your company CA.
  • Fix: pass the bundle explicitly:
import requests
requests.get("https://intranet.example.com", verify="/etc/ssl/certs/company-bundle.pem")

Alternatively install truststore (Python 3.10+) and call truststore.inject_into_ssl() at startup so Python uses the OS trust store.

4. A TLS-inspecting proxy or antivirus

Proxies and "HTTPS scanning" features re-sign traffic with their own root. The issuer in openssl s_client output then names the proxy vendor.

  • Fix: export the proxy root (PEM) from your IT team, append it to a copy of the bundle and point REQUESTS_CA_BUNDLE / SSL_CERT_FILE at it.

5. Strict verification in Python 3.13+

If the message ends with Missing Authority Key Identifier or names another certificate extension, the certificate itself does not meet RFC 5280 and strict mode rejects it. Reissue the internal CA and leaf certificates with RFC 5280 extensions.

6. Expired certificate or wrong clock

certificate has expired means the chain is trusted but the dates are wrong. Renew the certificate, or fix the client's clock.

Fixes by environment

Every fix gives Python a current CA bundle that includes any private root; the steps depend on how Python was installed.

macOS (python.org installer): run Install Certificates.command. It installs certifi and links it as the OpenSSL bundle for that Python.

macOS (Homebrew Python): Homebrew's OpenSSL uses Homebrew's CA bundle. Run brew upgrade to refresh it; add private roots via SSL_CERT_FILE.

Windows: ssl.create_default_context() loads the Windows ROOT and CA stores, so urllib usually trusts roots that IT deployed by Group Policy. requests does not; use verify=, REQUESTS_CA_BUNDLE or truststore.

Debian / Ubuntu / Docker:

apt-get update && apt-get install -y ca-certificates
cp company-root.crt /usr/local/share/ca-certificates/ && update-ca-certificates
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

RHEL family: put the root in /etc/pki/ca-trust/source/anchors/, run update-ca-trust, and use /etc/pki/tls/certs/ca-bundle.crt.

pip itself has its own options; see pip SSL certificate verify failed.

Don't do this

  • requests.get(url, verify=False). It disables certificate and hostname checks; any machine on the path can impersonate the server and read tokens or passwords.
  • ssl._create_default_https_context = ssl._create_unverified_context. This monkeypatch turns off verification for the whole process, including libraries you did not write.
  • urllib3.disable_warnings() to silence the InsecureRequestWarning. Hiding the warning leaves the hole open and makes it harder to find later.
  • Clearing VERIFY_X509_STRICT globally to make a malformed internal certificate pass. Fix the certificate instead.

FAQ

Why does the URL open in my browser but fail in Python?

Browsers can download a missing intermediate certificate on their own and use the operating system trust store. Python does neither by default: OpenSSL needs the full chain from the server, and requests only trusts the certifi bundle. Check the chain with the chain builder; if it is complete, the difference is the trust store.

Does `pip install --upgrade certifi` fix it?

Only when the server chains to a public root that your old certifi version did not have yet. It does not help with a missing intermediate on the server, a corporate proxy root or an internal CA, because certifi only contains the public Mozilla roots.

What is the difference between urllib and requests here?

urllib, http.client and most standard-library code use ssl.create_default_context(), which loads OpenSSL's default paths (and the Windows ROOT and CA stores on Windows). requests uses certifi unless you pass verify= or set REQUESTS_CA_BUNDLE. The same script can therefore pass with urllib and fail with requests, or the other way round.

My internal certificates broke after upgrading to Python 3.13. Why?

Python 3.13 added VERIFY_X509_STRICT and VERIFY_X509_PARTIAL_CHAIN to the default context. Strict mode rejects certificates that do not follow RFC 5280, for example a certificate missing the Authority Key Identifier extension, which some hand-made internal CAs omit. Reissue the internal CA and certificates with correct extensions.

Sources

  1. Python docs: ssl — TLS/SSL wrapper for socket objects
  2. Requests: Advanced Usage (CA Certificates)
  3. pip: HTTPS Certificates
  4. curl: CA certificates extracted from Mozilla