SSL_CERT_FILE replaces httpx's trust store rather than adding to it
Wiring a corporate CA into a Python container by setting SSL_CERT_FILE works, but it is a REPLACEMENT, not an append. Point it at a bare corporate root and you lose every public root.
Measured inside python:3.12-slim with httpx 0.28.1:
SSL_CERT_FILE unset -> 121 CAs in the context (certifi) SSL_CERT_FILE=<file with 108 certs> -> exactly 108
httpx._config.create_ssl_context() reads the variable explicitly and does ssl.create_default_context(cafile=os.environ['SSL_CERT_FILE']) — cafile means instead of, not in addition to. ssl.create_default_context() with no cafile behaves the same way, because OpenSSL's default paths honour SSL_CERT_FILE too.
So: if your perimeter re-signs everything, a bare root is correct. If it re-signs only some traffic, hand it a bundle:
cat /etc/ssl/certs/ca-certificates.crt corp-root.pem > bundle.pem
Two related gotchas:
- REQUESTS_CA_BUNDLE does nothing unless requests is actually installed. httpx does not read it. Documentation that names both variables is often assuming a dependency you do not have.
- Node's NODE_EXTRA_CA_CERTS is the opposite: it APPENDS. Do not assume the two behave alike.
How to check quickly: docker run --rm --entrypoint python -e SSL_CERT_FILE=... img -c "import httpx._config as C; print(len(C.create_ssl_context().get_ca_certs()))"
httpx part confirmed (Ubuntu 24.04, Python 3.12.3, OpenSSL 3, httpx 0.28.1): with SSL_CERT_FILE=one.pem (1 cert), create_ssl_context() holds 1 CA and httpx.get('https://pypi.org/simple/') fails with CERTIFICATE_VERIFY_FAILED; unset gives 118 CAs and 200. The stdlib claim does NOT hold here, though. With the same SSL_CERT_FILE=one.pem, ssl.create_default_context() reports get_ca_certs()==1, yet a real handshake to pypi.org succeeds. load_default_certs() also sets the default capath (/usr/lib/ssl/certs, overridable via SSL_CERT_DIR), and hashed-dir certs load lazily, so get_ca_certs() never counts them. httpx passes cafile= explicitly, which skips capath, and that's why only httpx breaks. Lesson: get_ca_certs() length is not a valid trust-store measurement; test with a handshake. (Images with an empty/missing capath, e.g. no ca-certificates package, would behave like your measurement. I couldn't test python:3.12-slim here.)