runtime trust stores
runtime trust stores
a TLS client trusts whatever roots its runtime loads, and runtimes load them from different places: the OS store, a bundle shipped in a package, roots compiled into the binary, or a hardcoded distrust list in the standard library. where a process's trust comes from decides where a distrust has to be patched, and most processes have more than one source.
the concrete case: in 2026-10 eMudhra TLS intermediates were compromised, so their holder could mint a certificate for any hostname that chained to a root a client trusted. one service could need a toolchain bump, a package release and an OS store change before it refused those chains.
where each runtime gets its roots
- python:
ssl.create_default_context()uses OpenSSL's default paths (the system store). requests uses certifi's bundle only; httpx uses certifi unlessSSL_CERT_FILEis set andtrust_envis on. a distrust reaches them as a certifi release. - grpcio ships its own
roots.pemand does not readSSL_CERT_FILE;GRPC_DEFAULT_SSL_ROOTS_FILE_PATHpoints it at another bundle. - rust:
webpki-rootscompiles Mozilla's roots into the binary, so the fix is a dependency bump and rebuild (withrustls-webpki); crates using the platform verifier or native certs follow the OS store instead. - go reads the system store on Linux (
SSL_CERT_FILE,SSL_CERT_DIR) and the platform verifier on macOS and Windows, andcrypto/x509also carries a hardcoded list of distrusted SPKI hashes that rejects any chain containing one, whatever the root source. a toolchain release can therefore distrust roots for every Go binary rebuilt with it, even on an unpatched OS store. pointing onlySSL_CERT_FILEat a filtered bundle leaves Go's default certificate-directory scan active; setSSL_CERT_DIRtoo. - node uses its bundled Mozilla store by default.
NODE_EXTRA_CA_CERTSonly adds roots.--use-openssl-ca(or--use-system-ca) swaps the bundled store for OpenSSL's or the system's, for every connection in the process, so it changes trust far beyond the root you meant to remove. - bun:
SSL_CERT_FILEandSSL_CERT_DIRadd to its bundled roots, so a filtered file cannot remove a bundled root. bun 1.4.0 still accepted a compromised test chain with a filtered bundle, with or without--use-openssl-ca. - zig trusts the files it parses, not the OS evaluator; see http-client.
- debian and alpine images read
/etc/ssl/certs, rebuilt from/etc/ca-certificates.conf, where a!prefix distrusts an entry:
RUN sed -i -e 's|^mozilla/emSign_ECC_Root_CA_-_G3.crt|!&|' \
-e 's|^mozilla/Comodo_AAA_Services_root.crt|!&|' \
/etc/ca-certificates.conf && update-ca-certificatesthe OS store fixes only the runtimes that read it, and a vendor container keeps its own trust after its host is patched. a CA bundle mounted into the container and selected with SSL_CERT_FILE and SSL_CERT_DIR covers OpenSSL, Go and Python clients there, and nothing with embedded roots.
distrust roots, not intermediates
compromised intermediates cannot be enumerated from the outside, so removing the roots they chain to is the fix. a root can anchor much more than the compromised chain (Comodo AAA Services is a large Sectigo root), so smoke-test ordinary outbound calls after distrusting one. match certificates by SPKI or certificate hash against the advisory's list, not by file name: a store's file names and an issuer's display names do not line up reliably.
verify from inside the process
check from inside the running machine, through the client the application actually uses, with negative probes that must fail on a certificate error and a positive control that must succeed; a network failure does not count as a pass. for the eMudhra chains:
curl -sS https://dv-g3b-emudhra.we-have-a-problem.com
curl -sS https://dv-g3b-aaa-emudhra.we-have-a-problem.comcurl tests curl's trust, not the application's. a CLI that wraps its TLS errors in generic messages cannot give certificate-specific proof.
sources
- eMudhra advisory,
roots.jsonandimpacted-intermediates.json, 2026-10 - relay
docs/incidents/2026-10-21-emudhra/(public-ca.mdfor container CA overlays and Go's directory scan;ci-trust-results.mdfor bun and the wrapped-CLI limit;runner-trust-results.md,zig-results.md) - Go 1.27.3
src/crypto/x509/verify.go(distrustedSPKIHashes,containsDistrustedSPKIHashin chain filtering), read 2026-10-22 - grpcio
_cython/_credentials/roots.pemand theGRPC_DEFAULT_SSL_ROOTS_FILE_PATHsetting incygrpc; requestscerts.py; httpx_config.py, read 2026-10-22 node --help(22.23.4):--use-bundled-ca,--use-openssl-ca,--use-system-ca- bun environment variables; local bun 1.4.0 fetch probes with filtered
SSL_CERT_FILE/SSL_CERT_DIR, 2026-10-21 - inventory and per-app remediation:
emudhra/affected-apps.md,emudhra/prefect-services.md(project files, 2026-10-20)
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.