Skip to content

runtime trust stores

nate
Oct 23, 20264 min read

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 unless SSL_CERT_FILE is set and trust_env is on. a distrust reaches them as a certifi release.
  • grpcio ships its own roots.pem and does not read SSL_CERT_FILE; GRPC_DEFAULT_SSL_ROOTS_FILE_PATH points it at another bundle.
  • rust: webpki-roots compiles Mozilla's roots into the binary, so the fix is a dependency bump and rebuild (with rustls-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, and crypto/x509 also 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 only SSL_CERT_FILE at a filtered bundle leaves Go's default certificate-directory scan active; set SSL_CERT_DIR too.
  • node uses its bundled Mozilla store by default. NODE_EXTRA_CA_CERTS only 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_FILE and SSL_CERT_DIR add 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-certificates

the 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.com

curl 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.json and impacted-intermediates.json, 2026-10
  • relay docs/incidents/2026-10-21-emudhra/ (public-ca.md for container CA overlays and Go's directory scan; ci-trust-results.md for bun and the wrapped-CLI limit; runner-trust-results.md, zig-results.md)
  • Go 1.27.3 src/crypto/x509/verify.go (distrustedSPKIHashes, containsDistrustedSPKIHash in chain filtering), read 2026-10-22
  • grpcio _cython/_credentials/roots.pem and the GRPC_DEFAULT_SSL_ROOTS_FILE_PATH setting in cygrpc; requests certs.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.

Across the AtmosphereDiscussions