eMudhra CA compromised: what to do in your Bun app
Every fetch("https://...") your Bun app makes trusts a few hundred companies you never picked. One of them just ran into trouble. A report came out saying that an intermediate CA from eMudhra was compromised. eMudhra is an Indian certificate authority included in the major trust stores. The site collecting the details has a name that sums up the mood: emudhra.we-have-a-problem.com. The investigation is still ongoing, so the details of the incident may change. But the part that matters for anyone writing a Bun backend can already be explained. What a compromised CA does to your code, and why the runtime doesn't protect you as well as the browser does.
What an intermediate CA is and why it's so sensitive
Quick refresher. When your code opens a TLS connection, the server sends a certificate. That certificate was signed by an intermediate CA. The intermediate, in turn, was signed by a root CA. The root lives in a list inside your system, your browser or your runtime.
The client walks up that chain. If it reaches a known root, the connection is accepted.
Here's the scary part: any trusted intermediate can issue a certificate for any domain. It doesn't matter if api.stripe.com uses a different CA. If someone controls the private key of an eMudhra intermediate, they can generate a valid certificate for api.stripe.com, for your managed database or for your app's login endpoint. The client sees a valid chain and moves on.
TLS security is only as strong as the weakest CA in your list.
To turn this into an attack, the attacker still has to sit in the middle of the path: public Wi-Fi, hijacked DNS, a corporate network, a compromised provider. It's not trivial. It's not science fiction either.
Browsers and Bun react differently
This is the point few people remember.
When an intermediate is compromised, browsers react fast. Firefox uses CRLite, Chrome uses CRLSets. These are revocation lists delivered through silent updates, sometimes within hours. The user doesn't even notice.
Server runtimes work differently. Node and Bun ship the root list baked into the binary, derived from Mozilla's store (NSS). And by default, neither of them checks revocation. No OCSP, no CRL, no CRLite. Verification goes like this:
- Does the chain reach a root that's in the binary? Pass.
- Is the certificate within its validity period? Pass.
- Does the hostname match? Pass.
Nobody asks whether that intermediate was revoked yesterday.
That means that as long as the eMudhra root stays inside your Bun binary, a certificate issued by the compromised intermediate can be accepted by your backend. And the fix only reaches you when two things happen. Mozilla removes or restricts the root, and you update Bun to a version that ships the new list.
My take, no sugarcoating: most teams treat the runtime version as an infra detail that "gets updated when we get to it". In this kind of incident, that version decides who your server trusts. The Docker image with oven/bun:1.1 forgotten in production for a year carries a trust list from a year ago.
How to tell if your runtime trusts eMudhra
Bun implements the node:tls and node:crypto APIs, so you can inspect the built-in root list in a few lines:
// check-roots.ts
import { rootCertificates } from "node:tls";
import { X509Certificate } from "node:crypto";
const suspects = rootCertificates
.map((pem) => new X509Certificate(pem))
.filter((cert) => /emudhra/i.test(cert.subject));
for (const cert of suspects) {
console.log(cert.subject.replace(/\n/g, ", "));
console.log(" valid until:", cert.validTo);
}
console.log(`Bun ${Bun.version}: ${suspects.length} eMudhra root(s)`);
bun check-roots.ts
Run it on your machine and, more importantly, inside the image that goes to production. If any eMudhra root shows up, your runtime trusts its entire chain. That doesn't mean you're under attack. It means you're exposed until the fix arrives.
One caveat. If you enable the operating system's certificate store (some recent versions of Bun and Node have an option for this), the result also depends on your distro's ca-certificates package. Then you have two lists to keep updated, not one.
What to do in your project now
No need to panic or rewrite anything. You just need some hygiene.
1. Update Bun and keep an eye on the next releases. When Mozilla acts, the change lands in the runtime's certificate bundle. Follow the changelog and update the base image. If you pin the version in your Dockerfile (and you should), set a reminder to review it.
# before: forgotten since last year
FROM oven/bun:1.1
# after: explicit version, reviewed often
FROM oven/bun:1.3
2. For critical integrations, restrict trust. If your backend talks to a few sensitive services (payment gateway, internal API, database), you don't need to trust the whole world for those calls. Bun's fetch accepts TLS options:
const internalCa = await Bun.file("./certs/internal-ca.pem").text();
const res = await fetch("https://api.internal.mycompany.com/orders", {
tls: {
ca: internalCa, // only accepts certificates from this CA
},
});
For an internal service, this solves the problem completely. A compromised eMudhra intermediate doesn't sign anything this CA accepts.
For third-party services, you can go further with checkServerIdentity and check the public key fingerprint (pinning). It works, but it comes at a price. When the provider rotates the key, your integration breaks at 3 a.m. Only use it if the provider publishes its keys and you have alerts in place.
3. Don't confuse this with rejectUnauthorized: false. I've seen plenty of code that turns off verification at the first TLS error, "just to test". At a moment like this, that code is the worst place you can be. Search your repo:
grep -rn "rejectUnauthorized: false\|NODE_TLS_REJECT_UNAUTHORIZED" src/
If anything shows up outside of tests, fix it today. It's free.
4. Be careful with NODE_EXTRA_CA_CERTS. Bun respects this variable. It's useful for corporate proxies, but every extra CA you add is one more company (or one more IT server) that can issue a certificate for any domain. Check what's configured in your environments.
What if I don't use Bun on the backend?
The same reasoning applies to Node, Deno and any other runtime that ships its own root list. In React Native, the app uses the phone's system store. That depends on Android or iOS updates, and an old Android device with no updates carries the old list forever. In that case, certificate pinning for your own API starts to make more sense.
The browser is, ironically, the best protected client in this story. The one with the most to lose is the backend nobody updates.
Why this keeps happening
The CA system works because most authorities do their job right, and because of Certificate Transparency. Every public certificate has to be recorded in open logs. That's what lets someone spot a bad issuance and build a site like the eMudhra one. Without CT, a fake certificate could stay in use for years without anyone noticing.
You can use this to your advantage too. Services like crt.sh let you search every certificate issued for your domain. If an issuer you don't recognize shows up, that's a red flag. Setting up a monitor like this takes five minutes and has caught plenty of people red-handed.
My take
This incident isn't a reason to switch runtimes or to start pinning everything. It's a reminder of something we forget. TLS trust is a dependency, just like any package in your package.json. It has a version, it ages and it needs updates. Run the check script, update the base image and restrict the CA for internal integrations. One afternoon of work, and you stop relying on luck.
If you want to see how I handle this in my Bun projects, take a look at some of the projects I maintain.
LinkedIn summary
Your Bun backend might be accepting a fake certificate right now, and the browser would be protecting you better. An eMudhra intermediate CA was compromised. Any trusted intermediate can issue a certificate for any domain, including your payment API's. The browser gets revocations within hours. Node and Bun don't check revocation at all. They trust the root list baked into the binary. That oven/bun:1.1 image forgotten in production trusts the world as it was a year ago. What I did: ran a 10-line script to see if the runtime trusts eMudhra, updated the base image and restricted the CA for internal integrations. It takes an afternoon. TLS trust is a dependency, just like a package in package.json. It has a version and it ages. I wrote the step by step with code on the blog. If you want to check your own environment, the link is in the comments. #Bun #NodeJS #Security #Backend #TLS