← Debugging Guides

阅读中文版本

Python CERTIFICATE_VERIFY_FAILED — Trust Store and CA Bundle Fixes

Fix Python SSL certificate verification failures by auditing the CA bundle, corporate interception, environment overrides, and clock skew without disabling verification.

intermediatedebuggingpythontlscertificates

Symptoms

  • ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed
  • unable to get local issuer certificate when calling an HTTPS API
  • pip install fails with certificate verify failed while curl works
  • The same code works on one machine and fails on another or inside a container

Possible causes

  • A corporate TLS inspection proxy presents a certificate signed by a root CA that Python does not trust
  • The CA bundle is missing, outdated, or overridden by SSL_CERT_FILE or REQUESTS_CA_BUNDLE
  • Python on macOS was installed from python.org without running Install Certificates.command
  • The server certificate is expired, self-signed, or missing an intermediate certificate
  • The system clock is far off, so the certificate appears not yet valid or expired

Diagnosis steps

Inspect the certificate chain the server actually presents

openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -dates

Interpretation

The issuer reveals TLS interception when it is a corporate CA. The dates reveal expiry, and a chain with only the leaf suggests a missing intermediate.

Ask Python which trust store it uses

python -c "import ssl; print(ssl.get_default_verify_paths())"

Interpretation

Confirm the cafile and capath exist and are readable. An empty or container-local path explains failures that do not occur with system curl.

Check environment overrides

env | grep -iE 'SSL_CERT_FILE|SSL_CERT_DIR|REQUESTS_CA_BUNDLE|CURL_CA_BUNDLE'

Interpretation

REQUESTS_CA_BUNDLE and SSL_CERT_FILE override the default bundle. A stale path breaks every request even though certifi is installed.

Verify the clock and test an explicit bundle

date -u; python -c "import certifi, requests; print(requests.get('https://api.example.com', verify=certifi.where()).status_code)"

Interpretation

A large clock skew invalidates otherwise valid certificates. Success with the explicit certifi path isolates the problem to trust store selection, not the certificate.

Root cause

The TLS handshake failed because the certificate chain could not be validated against the trust anchors available to Python. Either the issuing CA is not in the bundle, the bundle path is wrong or overridden, the chain is incomplete or expired, or the local clock makes the validity window wrong.

Fix

Install the corporate root CA into the system trust store and point SSL_CERT_FILE at a PEM bundle that contains it, or append it to the bundle used by certifi. Update certifi, run the macOS Install Certificates.command for python.org builds, and remove stale SSL_CERT_FILE or REQUESTS_CA_BUNDLE values. Fix expired or incomplete chains on the server. Keep verification enabled.

Risk note

Setting verify=False or disabling verification globally removes the protection against man-in-the-middle attacks. Trusting a corporate root CA extends to every certificate it can issue. REQUESTS_CA_BUNDLE overrides certifi silently, so an incorrect value can break all HTTPS traffic in the environment.

Verify the fix

Run the original request with verification enabled and confirm a 200 response, then repeat inside the deployment container. openssl verify against the same bundle should return OK, and pip install from the configured index should complete without certificate errors.

Read the verification error precisely

CERTIFICATE_VERIFY_FAILED means the TLS handshake completed far enough to receive a certificate chain, but validation failed. The trailing reason is the useful part:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)

unable to get local issuer certificate points at a missing root or intermediate in the trust store. certificate has expired points at the server or the clock. self-signed certificate in certificate chain is typical of an interception proxy that Python does not trust.

Inspect the chain and the trust store

openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates
python -c "import ssl; print(ssl.get_default_verify_paths())"
env | grep -iE 'SSL_CERT_FILE|REQUESTS_CA_BUNDLE|CURL_CA_BUNDLE'

If the issuer is your organization’s CA, the proxy is intercepting TLS and Python needs that root. If the issuer is public but the chain is short, the server is not sending the intermediate certificate and most clients will fail even though browsers may recover it.

Fix the trust path, not the check

# Debian/Ubuntu: install the corporate root into the system store
sudo cp corporate-root.crt /usr/local/share/ca-certificates/corporate-root.crt
sudo update-ca-certificates
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt

# macOS python.org builds
open "/Applications/Python 3.12/Install Certificates.command"

For requests, REQUESTS_CA_BUNDLE takes precedence over certifi, so point it at a complete bundle rather than a single certificate. Keep a copy of the corporate root in the container image build so the runtime does not depend on an interactive fix.

Verify

Repeat the failing call with verification enabled, then run it inside the deployment image, not only on the developer laptop. openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt server.pem should end with OK, and the package installer must work against the configured index. If it still fails, compare the exact interpreter and environment variables with the machine where it works.