Skip to main content

Certificate trust and corporate CA interception

Summary: HTTPS clients only trust certificates signed by a CA in their own trust store. A corporate proxy that inspects traffic re-signs everything with a private CA, so clients that don't know that CA reject every connection. The fix is to trust that CA — not to stop verifying.

How certificate trust works

  • A CA (certificate authority) is an organisation that signs certificates — a passport office for websites.
  • On every HTTPS call the client checks the server's certificate was signed by a CA in its trust store. No match, no connection.
  • Each runtime has its own trust store. Python uses certifi (~150 CAs), Node has its own bundle, browsers and the OS have theirs. Installing a certificate in one does not install it in the others.

Why corporate networks break it

To inspect encrypted traffic, a proxy has to decrypt it — and to decrypt it, it must terminate the TLS connection itself and re-encrypt with its own certificate. Every response therefore arrives signed by the company's private CA instead of the real site's.

IT pushes that CA into the OS trust store, so browsers work and nothing looks broken. Anything with its own store — Python, Node, Java, Docker builds, git — still rejects it, which is why the failure shows up only in code.

What to do

OptionHowWhen
Skip verificationSSL_VERIFY=falseDev only — accepts any certificate
Point at the CA fileSSL_CERT_FILE=/path/ca.pem (or REQUESTS_CA_BUNDLE)The correct per-app fix
Add the CA to the trust storeinstall into the OS and certifi onceBest long-term — every app benefits

Skipping verification doesn't just accept the proxy's certificate, it accepts anyone's — which is the attack TLS exists to prevent. Never let it default to off, and never ship it. Make it an explicit environment variable so production fails closed.

Where this came up

Every outbound HTTPS call from the Data Patch Agent failed certificate verification on the agency network. SSL_VERIFY was hardcoded False as a workaround; it's now env-configurable — .env sets false locally, production omits the var and defaults to True.

This is outbound traffic — the app calling out. The inbound counterpart is exposing localhost with a tunnel.