eMudhra exits SSL/TLS: what changes for your Bun app
eMudhra announced it is leaving the global SSL/TLS certificate market. If any of your domains uses a certificate it issued, that certificate will expire at some point and there will be no renewal. This post is for anyone running a Bun app in production who wants to know if they're affected, how to find out and how to migrate without surprises.
I already wrote here about eMudhra's compromised CA. This is the natural next chapter: the company decided to leave the business. The official announcement is on eMudhra's website.
What eMudhra announced
eMudhra is an Indian certificate authority, known for the emSign brand. It sells several kinds of digital certificates, and public SSL/TLS is only one part of the business. It's the part that matters to us: the certificate that makes the padlock show up in the browser and that your fetch validates when it calls an API.
The news, in short: the company will stop offering public SSL/TLS certificates in the global market. Customers will have to move to another CA.
In practice, leaving this market means three things:
- No new certificates.
- No renewals.
- Current certificates stay valid until they expire, unless something forces an earlier revocation.
The last point is the one that matters most. After a security incident, browser root programs (Chrome, Mozilla, Apple, Microsoft) have little patience. If any of them stops trusting the CA's root, the certificate is still "valid" on paper and the browser rejects it anyway.
Why this matters even if you never bought anything from them
You might be thinking: "I never bought a certificate from eMudhra, so this isn't about me." Maybe it isn't. But there are two scenarios where it is.
The first is the certificate you didn't buy yourself. Maybe the client did, or the infra team from three years ago, or a reseller. Lots of companies buy certificates inside a "security" bundle and nobody looks at the CA behind it.
The second is the third-party API your backend calls. If a payment gateway, a government agency or a partner uses an emSign certificate and that certificate stops being accepted, it's your fetch that breaks. And the error shows up as UNABLE_TO_VERIFY_LEAF_SIGNATURE at three in the morning, with no hint that the cause is a CA leaving the market.
Certificates are the only dependency in your project with a scheduled date to break.
How Bun decides who to trust
This detail catches a lot of people off guard. By default, Bun doesn't read the operating system's CA list. It ships with its own bundle of root certificates, based on Mozilla's list, just like Node.
That has two consequences:
- If Mozilla removes a root from the list, a future Bun version stops trusting it. You only notice when you upgrade the runtime.
- If your company installed an internal CA on the system, Bun ignores it by default.
For the second case, there are two options. To add an extra CA without touching the code:
NODE_EXTRA_CA_CERTS=./certs/internal-ca.pem bun run start
Or, in recent versions, tell Bun to use the operating system's list:
bun --use-system-ca run start
And for a specific case, you pass the CA straight to fetch:
const res = await fetch("https://api.partner.com/orders", {
tls: { ca: Bun.file("./certs/partner-ca.pem") },
});
Now the strong opinion: never fix a certificate error with rejectUnauthorized: false. That "fix" turns off TLS validation entirely and leaves the door open to man-in-the-middle attacks. I've seen it in production, committed with the comment // temporary. It was from 2021.
How to find out if any of your domains uses emSign
You can build a quick inventory. In the terminal, openssl does the job:
echo | openssl s_client -connect api.yoursite.com:443 -servername api.yoursite.com 2>/dev/null \
| openssl x509 -noout -issuer -enddate
If the issuer contains "emSign" or "eMudhra", write down the domain and the expiry date.
If you have many domains, a Bun script handles it. It uses the node:tls API, which Bun implements:
import tls from "node:tls";
const hosts = ["yoursite.com", "api.yoursite.com", "api.partner.com"];
for (const host of hosts) {
const socket = tls.connect({ host, port: 443, servername: host }, () => {
const cert = socket.getPeerCertificate();
const issuer = `${cert.issuer?.O ?? ""} ${cert.issuer?.CN ?? ""}`;
const flag = /emsign|emudhra/i.test(issuer) ? " <-- WARNING" : "";
console.log(`${host}: ${issuer} | expires ${cert.valid_to}${flag}`);
socket.end();
});
socket.on("error", (err) => console.log(`${host}: error ${err.message}`));
}
Put your own domains in the list and the domains of the external APIs your backend calls. The second part is the one everyone forgets.
It's also worth checking:
- The
.pemfiles committed to the repository (git grep -l "BEGIN CERTIFICATE"). - The secrets in your CI and hosting provider.
- Mobile apps with certificate pinning. This is the worst case: swapping the certificate without swapping the pin breaks the app for anyone who hasn't updated.
How to migrate without breaking production
If you found an eMudhra certificate, the migration itself is simple. What needs care is the order of the steps.
1. Pick the new CA. For most projects, Let's Encrypt via ACME solves it for free. If you need an OV/EV certificate because of a contract or a client requirement, go with a commercial CA.
2. Check the CAA record in DNS. If the domain has a CAA record that only authorizes eMudhra, the new CA will refuse to issue. This blocks a lot of migrations:
dig CAA yoursite.com +short
If all you see is 0 issue "emsign.com", add the new CA before requesting the certificate.
3. Issue and install the new certificate before removing the old one. In Bun.serve, the swap is straightforward:
Bun.serve({
port: 443,
tls: {
cert: Bun.file("/etc/ssl/new/fullchain.pem"),
key: Bun.file("/etc/ssl/new/privkey.pem"),
},
fetch: (req) => new Response("ok"),
});
Use the fullchain, not just the leaf certificate. Without the intermediates, the browser sometimes works because it completes the chain on its own, but your partner's HTTP client fails. Classic "works on my machine".
4. If you use pinning, ship the new pin first. Release an app version that accepts both pins, the old one and the new one, wait for your user base to update and only then swap the certificate.
5. Automate renewal. And this applies regardless of eMudhra.
The real problem is renewing by hand
eMudhra is just the trigger. The CA/Browser Forum approved a gradual reduction in the validity of public certificates: 200 days starting March 2026, 100 days in 2027 and 47 days in 2029. Anyone still renewing certificates by hand, with a calendar reminder, will suffer well before 2029.
So use eMudhra's exit as an excuse to do what should already be done:
- Certificates issued and renewed via ACME, with no human involved.
- An alert when any certificate is 14 days from expiring, including the ones from external APIs. The script above, running in a daily cron, already covers a good part of this.
- No CA hardcoded anywhere you don't control.
If your setup already works like this, today's news becomes one line in the changelog. If it doesn't, it becomes an incident.
My take
The news itself isn't scary. CAs leave the market, and the TLS ecosystem was designed for that: there are many CAs, validity is short and there's an automation protocol. What's scary is how many teams don't even know which CA signs their own certificates.
Run the script, check the CAA record and automate renewal. It takes an afternoon and saves you a sleepless night later. If you want to see how I organize infra and deploys in my projects, take a look at the projects here on the site.
LinkedIn summary
eMudhra is leaving the SSL/TLS market, and plenty of teams don't even know which CA signs their own certificates. If your domain's certificate is emSign, it will expire and there will be no renewal. It gets worse if it's your partner's API: then it's your fetch that breaks, at three in the morning. In Bun, the first step is a 15-line script that lists the issuer and expiry date of each domain. Then you check the CAA record in DNS and switch to the new CA's fullchain before removing the old certificate. And please, no rejectUnauthorized: false with a "// temporary" comment. With validity dropping to 47 days by 2029, anyone renewing certificates by hand is going to suffer either way. I wrote the full step by step on the blog, the link is in the comments. #Bun #TLS #DevOps #InfoSec #JavaScript