The Browser Patched Itself. Your Server Did Not.

The Browser Patched Itself. Your Server Did Not.

On 17 October 2026 a researcher reported to the Apple, Chrome, Microsoft and Mozilla root programs, and to eMudhra, that the private keys of eight eMudhra intermediate certificates were compromised. The report included a signed proof of key control for each one. In the researcher's words: "The private keys for the intermediate certificates were incorrectly generated by the CA. This means that anyone can recover them from the certificates." Mozilla received the report at 20:06 UTC, recovered the keys itself at 21:48 UTC, and called the key generation "flawed" and "trivial to exploit".

Mozilla blocked the certificates in Firefox through OneCRL by 22:59 UTC the same day, with no update or restart needed. Chrome's block landed at 19:45 UTC, Apple blocked the CAs on 19 October and Microsoft on 20 October. Cloudflare put it simply: browser makers "were able to remediate the issue with a flip of a switch". The browsers had one application each to fix. You have a dozen machines, images and runtimes, and the trust store on each one is yours to maintain.

How trust works for self-hosted TLS certificates

A browser is a managed product. Mozilla pushes OneCRL and Chrome pushes CRLSet, so either can distrust a certificate on every install without the user lifting a finger. That is why the fix took hours, not weeks.

A server works differently. When your reverse proxy, backup script or container connects to another service over TLS, it checks the chain against a static list of root certificates. That list only changes when you change it: a package upgrade, an image rebuild, a library bump.

The awkward part is that one box rarely has one list. These are the places it actually lives:

  • The OS package. ca-certificates on Debian, Ubuntu and Fedora, built into a bundle such as /etc/ssl/certs/ca-certificates.crt (Debian, Ubuntu) or /etc/pki/tls/certs/ca-bundle.crt (Fedora).
  • Python's certifi bundle. The requests library uses it by default instead of the OS store, so a fresh OS package does nothing for a Python app with an old certifi.
  • Alpine and distroless base images. Every container carries its own copy, frozen at the moment the image was built.
  • A Java cacerts keystore. It ships with the JDK or JRE and is often managed separately from the OS.
  • A Go binary's system pool. On Linux, Go reads the system bundle, so it is only as current as the OS or image it runs in.
  • mTLS or pinning config in a proxy. A client CA file in Nginx (ssl_client_certificate), Caddy or Traefik, or a pinned certificate in an app config. You wrote these lists by hand, and no package will ever update them.

For scale: six of the eight keys belonged to TLS intermediates trusted by the Chrome, Mozilla, Microsoft and Apple root programs, and two to code-signing intermediates trusted by Microsoft. Each key also existed as a cross-certificate issued by Sectigo, so sixteen certificates in all. Mozilla also decided on the emergency removal of trust in the emSign ECC Root CA - G3, which is why a whole root's subscribers had to migrate.

eMudhra's own account is that the affected intermediates came from a Key Generation Ceremony in April 2025, that its root CA private keys are not affected, and that only one of the intermediates was ever deployed in production. It says none of them, including that one, issued any end-entity certificates, and that its analysis and Certificate Transparency records on crt.sh confirm this. Code-signing certificates are not logged in Certificate Transparency, so that check cannot be made in public for the two code-signing CAs. I found no published report of active exploitation in those sources.

How to check every trust store you own

Start with the OS. On Debian or Ubuntu:

sudo apt update
sudo apt install --only-upgrade ca-certificates
sudo update-ca-certificates

On Fedora, run sudo dnf upgrade ca-certificates and then sudo update-ca-trust. To list what the bundle contains, let OpenSSL print the subjects, because the file itself has no readable labels:

openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/ca-certificates.crt | openssl pkcs7 -print_certs -noout | grep -i emsign

On Fedora, point -certfile at /etc/pki/tls/certs/ca-bundle.crt, or run trust list | grep -i emsign. Compare the output before and after the upgrade, and read the package changelog to see exactly what your distribution changed.

For Python, check every virtualenv, not just the system interpreter:

python3 -m certifi
grep -i emsign "$(python3 -m certifi)"
pip install --upgrade certifi

The first line prints the path of the bundle in use. The certifi file includes comment lines naming each issuer, so a plain grep works.

For Java, list the keystore with keytool -list -v -cacerts -storepass changeit | grep -i emsign. The password changeit is the default; use yours if you changed it.

For containers, copy the bundle out without needing a shell inside, which also works on distroless images:

id=$(docker create myimage:tag)
docker cp "$id":/etc/ssl/certs/ca-certificates.crt ./bundle.pem
docker rm "$id"
openssl crl2pkcs7 -nocrl -certfile bundle.pem | openssl pkcs7 -print_certs -noout | grep -i emsign

The fix for an image is a rebuild on a fresh base: docker build --pull --no-cache ., then redeploy. Pulling a newer tag of someone else's image only helps if they rebuilt it.

Finally, check what a server actually sends you:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null | grep -E ' [si]:'

Each pair of lines shows a subject and its issuer, from your leaf certificate up the chain. If emSign or eMudhra appears in a chain you depend on, that service needs a certificate from another CA.

Why "it works in my browser" proves nothing

Your browser got a pushed block. Your services did not. On top of that, many applications never use the store you just tested. Cloudflare said it directly:

"A successful browser test does not prove that a background service is protected. Neither does a curl test if the application uses a different verifier."

Cloudflare updated its Cloudflare One/WARP and 1.1.1.1 clients and cloudflared 2026.10.2. It told customers to review three things: customer-configured TLS client authentication trust stores, origin servers using Full (strict) or Strict, and container images and bundled trust stores. It added a warning that applies to every library you use:

"Updating the SDK alone may not change the underlying policy."

So test from the process that makes the connection. Run the check inside the container, with the same runtime and the same bundle the app loads.

When a patched client starts refusing a certificate, the tempting fix is verify=False in Python, curl -k, InsecureSkipVerify: true in Go, or proxy_ssl_verify off in Nginx. Do not do it. That switches off the one check doing its job. Replace the certificate instead.

Renewal automation is the real resilience

How painful a CA failure is depends on how your certificates were issued. eMudhra estimates that about 30,000 certificates need migration, and by its own estimate about 97% of those came via ACME. For an ACME certificate, moving to another CA is mostly a config change: point the client at a new directory URL and run it again.

To be precise, that does not mean those certificates migrated by themselves. Someone still has to change the endpoint and confirm the new chain. Automation makes the migration survivable. A certificate made by hand two years ago and forgotten does not get that option.

What that looks like in practice:

  • One renewal path for every certificate. Certbot, acme.sh, or your proxy's built-in ACME. Not a mix of three tools and a manual file nobody remembers.
  • DNS-01 where ports are blocked. Internal services and boxes behind CGNAT can still get public certificates through a DNS API token.
  • A dry run you trust. sudo certbot renew --dry-run proves the path works before you need it.
  • An expiry check that shouts.
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate -checkend 1209600

The -checkend 1209600 flag exits non-zero if the certificate expires within 14 days. Put it in cron or your monitoring and send the failure somewhere you will see it. This is the same thinking as preparing for a cloud outage: assume the provider can disappear, and make switching cheap before it does.

What I would do this week on my own box

  1. Upgrade ca-certificates on every host and run update-ca-certificates or update-ca-trust.
  2. Run pip install --upgrade certifi in every virtualenv and confirm the path with python3 -m certifi.
  3. Rebuild every image I build myself with docker build --pull --no-cache, and check third-party images with the docker cp method above.
  4. Grep my proxy configs for ssl_client_certificate, client CA files and pinned certificates, and read each list by eye.
  5. Run openssl s_client -showcerts against every external service my stack calls, and note any chain that contains eMudhra.
  6. Move any hand-made certificate onto ACME, and add the -checkend alert to monitoring.

If you would rather hand this off

None of this is hard, but it is tedious, and it is easy to miss the one container that matters. If you would rather have someone audit your trust stores and set up renewal for you, see the services page.

Fiverr: hiteshsaini459 · Upwork: hiteshsaini25