Python — CERTIFICATE_VERIFY_FAILED
`SSLCertificateVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]` means Python could not build a trusted path to the certificate a server presented. On macOS the usual cause is that Python ships its own CA bundle via the `certifi` package and does not read the system keychain — run the bundled `Install Certificates.command`. Elsewhere it is normally a corporate TLS-inspecting proxy or a private CA whose root is missing; point Python at a bundle containing it with the `SSL_CERT_FILE` or `REQUESTS_CA_BUNDLE` environment variable. Never fix it with `verify=False`.
Step 1 — is it Python, or is it the server? #
Test the server with a tool that has nothing to do with Python. If this reports a problem, the server is misconfigured and no Python change will help:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| grep "Verify return code" Expected output
Verify return code: 0 (ok)
Code 20 means the server is not sending its intermediates — see that error. Code 0 means the server is fine and the problem is local to Python.
Step 2 — see which bundle Python is actually using #
import ssl, certifi
print(ssl.get_default_verify_paths())
print(certifi.where()) Then confirm the failing certificate really is absent from it:
openssl s_client -connect example.com:443 -servername example.com \
-CAfile "$(python -c 'import certifi; print(certifi.where())')" </dev/null 2>/dev/null \
| grep "Verify return code" macOS — the most common cause by far #
A python.org installation on macOS does not read the system keychain. It expects certifi’s bundle to be installed by a script that does not run automatically:
/Applications/Python\ 3.12/Install\ Certificates.command Adjust the version number to match. With Homebrew Python, reinstalling certifi usually achieves
the same thing:
pip install --upgrade certifi Corporate proxies and private CAs #
If the chain ends at a root your organisation controls, that root has to be in the bundle Python uses. Appending it to certifi’s bundle works but is undone by the next upgrade, so prefer a separate file:
cat "$(python -c 'import certifi; print(certifi.where())')" corporate-root.crt > /etc/ssl/certs/python-bundle.crt
export SSL_CERT_FILE=/etc/ssl/certs/python-bundle.crt
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/python-bundle.crt | Variable | Read by |
|---|---|
SSL_CERT_FILE |
ssl, urllib, most stdlib TLS |
REQUESTS_CA_BUNDLE |
requests |
CURL_CA_BUNDLE |
requests also honours this |
pip config set global.cert |
pip specifically |
Or per call, which is explicit and does not depend on the environment:
import requests
requests.get("https://example.com", verify="/etc/ssl/certs/python-bundle.crt") In a container #
COPY corporate-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt update-ca-certificates alone is not enough — Python will keep using certifi’s bundle unless the
environment variables point it elsewhere.
What not to do #
Frequently asked questions
- Why does curl work but Python fail on the same URL?
- They use different trust stores. curl normally uses the system one; Python uses whatever
certifiships, or whateverSSL_CERT_FILEpoints at. A root installed for the system is invisible to Python unless it is in that bundle too. - Is verify=False acceptable?
- No. It disables verification for that request entirely, so the connection is encrypted but unauthenticated and can be transparently intercepted. Use it once to confirm a diagnosis, never in code that ships.
- How do I fix this on macOS?
- Run the
Install Certificates.commandscript inside/Applications/Python 3.x/. It installs certifi's bundle where Python expects it. A fresh python.org install ships without a usable trust store until you do. - Why does pip fail but python work?
- pip may be using a different bundle, particularly inside a virtualenv or a corporate build image. Set it explicitly with
pip config set global.cert /path/to/bundle.crt.
References
- Python ssl module — how Python locates a trust store, and what create_default_context does
- RFC 5280: X.509 and CRL profile — the specification every field on a certificate comes from