Skip to content
Testing the eMudhra CA fix: four shortcuts that don't work

Testing the eMudhra CA fix: four shortcuts that don't work

When a certificate authority is distrusted, the advice is to upgrade certifi, upgrade pip and rebuild. I followed that advice in the official Docker images, then asked each client to connect to a site signed with a cracked key. Four fixes that looked right did nothing. The only proof is your client refusing a bad certificate.

Testing the eMudhra CA fix. Who still trusts the hacked key? Python 3.12 and Node 22.23.4 are blocked from the test site; pip 25.0.1 and curl on macOS open it.

On 20 October 2026 the Python team posted a short notice: upgrade certifi, upgrade pip, rebuild your containers. A certificate authority called eMudhra had leaked the ability to sign certificates for any website, and the fix was to stop trusting it.

Most people will read that, bump a version, and move on. I wanted to know if that is enough. So I took the official Docker images, applied the usual fixes, and asked each client to connect to a site signed with one of the cracked keys. A fixed client must refuse it.

Four of the fixes I tried looked right and did nothing. This post is those four, why each one fails, and the test that caught it.

What happened, in two paragraphs#

When your code opens an HTTPS URL, the server sends a certificate. Your code accepts it only if the certificate chains up to a root on its trust list. Anyone with the private key of a trusted certificate authority (CA) can sign a certificate for any domain.

eMudhra generated the keys for eight of its intermediate CAs wrongly. The incident write-up says “anyone can recover them from the certificates.” Those intermediates had existed since April 2025, chained to roots that every major trust list carried.

Why browsers were fine and your server may not be#

Browsers were protected within days. They reject certificates that are missing from the public Certificate Transparency logs, and their vendors push block lists to every browser. Chrome and Firefox blocked these intermediates on 17 October.

Server-side code does neither. A Python script, a Node service or a curl in a cron job trusts whatever roots are in its own file, and that file only changes with a software update. And one server has several of those files:

Trust listUsed byHow it gets fixed
OS store (ca-certificates)Python’s ssl module, curl, most C programsOS package update
certifirequests, httpx, aiohttp with certifiupgrade certifi
pip’s own copy of certifipip installupgrade pip itself
Node’s built-in rootsevery Node programa new Node release
Bun’s built-in rootsevery Bun programa new Bun release

Fixing one does nothing for the others. And a version number won’t tell you which list a client really uses.

The test#

The incident write-up runs test sites that serve certificates signed by the compromised intermediates. A patched client must refuse them:

curl https://dv-g3b-emudhra.we-have-a-problem.com/

A certificate error is the good result. A page means your client would trust a forged certificate for any site. I ran this request from every client below.

After the eMudhra CA fix, who still trusts the hacked key? Each program uses its own list. Python 3.12 uses the OS list, which was updated, and is blocked. pip 25.0.1 uses its own old copy and opened the test site. Node 22.23.4 uses Node's own list, which was updated, and is blocked. curl on macOS uses an old macOS file and opened it. Opened it means it would trust a fake certificate for any website.

Fix 1: upgrade certifi#

This is the first line of the advice, and it is right for your app. It is not right for pip.

pip ships its own copy of certifi inside itself, at pip._vendor.certifi. In the official python:3.12-slim image, I installed certifi 2026.10.19 and ran the test:

python -c "import urllib.request as u; u.urlopen('https://dv-g3b-emudhra.we-have-a-problem.com/')"
  CERTIFICATE_VERIFY_FAILED                     # Python refuses

pip download https://dv-g3b-emudhra.we-have-a-problem.com/x-1.0-py3-none-any.whl
  Downloading ... (60 bytes)                    # pip connects

Same container, same moment. Python refuses, and pip downloads a file from the bad site.

This matters whenever pip downloads packages: during a Docker build, or any pip install. Someone who can intercept that traffic could hand pip a fake package. Your app’s own HTTPS calls don’t go through pip, so they use whatever list your app uses. To check what your pip trusts:

python -c "import pip._vendor.certifi as c; print('emSign ECC Root CA - G3' in open(c.where()).read())"

True means it still trusts the bad root. What works: pip 26.2.2 or later.

Fix 2: rebuild from a fresh image#

Rebuilding sounds like it should pick up every fix. It does for the OS store: every official Python image already refused the test site through Python’s ssl module. It does not for pip. The images rebuilt on 21 October ship the pip their Python release bundled:

image               pip      pip trusts bad root
python:3.11-slim    24.0     yes
python:3.12-slim    25.0.1   yes
python:3.13-slim    26.2.2   no
python:3.14-slim    26.2.2   no

3.11 and 3.12 are in security-only mode, so they get a new pip only with a new Python patch release. Until then, if you build on them, upgrade pip first:

RUN pip install --no-cache-dir -U "pip>=26.2.2"

Fix 3: point pip at the fixed OS store#

A common workaround is to make pip use the system bundle, which the OS update already fixed:

PIP_CERT=/etc/ssl/certs/ca-certificates.crt

I tested it on both old pips:

pip 24.0   + PIP_CERT   refuses the bad site
pip 25.0.1 + PIP_CERT   still connects

It works on 24.0 and does nothing on 25.0.1. Since 24.2, pip loads the system trust store through truststore by default, and, as a pip maintainer explained in the Python thread, it always loads its vendored certifi “in addition”. PIP_CERT and --cert only add CAs. The old roots stay.

The same line is a fix on one version and a no-op on the next, and both look identical until you test them. What works: upgrade pip.

Fix 4: make Node use the fixed OS store#

Node does not read the OS store by default. Its roots are compiled into the binary, and only Node 22.23.4, 24.22.1 and 26.12.1 or later refuse the test site. Node 20 is end of life and gets no fix. If you can’t move to a new Node, the obvious workaround is to make Node trust the fixed OS bundle instead:

NODE_OPTIONS=--use-openssl-ca

with the fixed bundle at /etc/ssl/certs/ca-certificates.crt. To prove it, I put an empty bundle in that place. With no roots at all, every HTTPS request must fail.

--use-openssl-ca, empty file at ca-certificates.crt     example.com 200

It still worked. OpenSSL reads the bundle file, and it also reads the /etc/ssl/certs/ directory, which holds each root as its own file. My bundle was never the only list. What works:

NODE_OPTIONS=--use-openssl-ca
SSL_CERT_FILE=/path/to/bundle.pem
SSL_CERT_DIR=/nonexistent
empty bundle    UNABLE_TO_GET_ISSUER_CERT_LOCALLY
real bundle     example.com 200
real bundle     bad test site: UNABLE_TO_GET_ISSUER_CERT_LOCALLY

Without the empty-bundle test, I would have shipped the first version and believed it.

And your laptop#

macOS itself blocks the intermediates. The built-in /usr/bin/curl and /usr/bin/python3 don’t use that. They read /etc/ssl/cert.pem, don’t check revocation, and that file had not been updated when I checked. Both connected to the bad site. Bun 1.4.3 connected too, and its fix is still listed as pending. The incident page has workarounds for both.

Test it on your machines#

Every fix above has the same shape. You change something, a version number says it worked, and only a request that should fail tells you the truth. So test the thing itself, not the version.

This script asks every TLS client it finds to open the bad test site:

curl -sO https://alokranjan.me/blog/trust-lists/check-trust.sh
sh check-trust.sh
python3 ssl            refused
python3 -m pip         CONNECTED
node                   refused (CERT_REVOKED)

A CONNECTED line is a trust list you still have to fix. Run it inside each container image too, not just on the host. Each image has its own lists.

Updated is not fixed. It is fixed when your client refuses a bad certificate.


References: the Python advisory thread, the incident write-up and test sites, Node.js October 2026 security releases, certifi releases. All tests ran on 22 October 2026 in the official Python, Node and Bun Docker images, and on macOS 27.2.

1–4 focus a window, j k scroll, g top