CA de eMudhra comprometida: qué hacer en tu app Bun
Cada fetch("https://...") que hace tu app Bun confía en unos cientos de empresas que nunca elegiste. Una de ellas acaba de tener un problema. Se publicó un informe de que una CA intermedia de eMudhra, autoridad certificadora india presente en los principales almacenes de confianza, fue comprometida. El sitio que reúne el caso tiene un nombre que resume bien el ambiente: emudhra.we-have-a-problem.com. La investigación sigue en curso, así que los detalles del incidente pueden cambiar. La parte que le importa a quien escribe backend en Bun ya se puede explicar: qué le hace una CA comprometida a tu código, y por qué el runtime no te protege tanto como el navegador.
Qué es una CA intermedia y por qué es tan sensible
Repaso rápido. Cuando tu código abre una conexión TLS, el servidor envía un certificado. Ese certificado lo firmó una CA intermedia. La intermedia, a su vez, la firmó una CA raíz. La raíz está guardada en una lista dentro de tu sistema, del navegador o del runtime.
El cliente recorre esa cadena hacia arriba. Si llega a una raíz conocida, la conexión se acepta.
El detalle que asusta: cualquier intermedia de confianza puede emitir un certificado para cualquier dominio. No importa si api.stripe.com usa otra CA. Si alguien controla la clave privada de una intermedia de eMudhra, puede generar un certificado válido para api.stripe.com, para tu base de datos gestionada o para el endpoint de login de tu app. El cliente ve una cadena correcta y sigue como si nada.
La seguridad de TLS es tan fuerte como la CA más débil de tu lista.
Para que esto se convierta en un ataque, el atacante todavía necesita estar en medio del camino: Wi-Fi público, DNS secuestrado, red corporativa, proveedor comprometido. No es trivial. Tampoco es ciencia ficción.
El navegador y Bun reaccionan de forma distinta
Aquí está el punto que poca gente recuerda.
Cuando una intermedia es comprometida, los navegadores reaccionan rápido. Firefox usa CRLite, Chrome usa CRLSets. Son listas de revocación que llegan por actualización silenciosa, a veces en horas. El usuario ni se entera.
Los runtimes de servidor funcionan distinto. Node y Bun traen la lista de raíces integrada en el binario, derivada del almacén de Mozilla (NSS). Y, por defecto, ninguno de los dos consulta revocación: no hay OCSP, no hay CRL, no hay CRLite. La verificación funciona así:
- ¿La cadena llega a una raíz que está en el binario? Pasa.
- ¿El certificado está dentro de su validez? Pasa.
- ¿El hostname coincide? Pasa.
Nadie pregunta si esa intermedia fue revocada ayer.
Eso significa que, mientras la raíz de eMudhra siga dentro del binario de tu Bun, un certificado emitido por la intermedia comprometida puede ser aceptado por tu backend. Y la corrección solo te llega cuando pasan dos cosas: que Mozilla quite o restrinja la raíz, y que actualices Bun a una versión que ya traiga la lista nueva.
Mi opinión, sin rodeos: la mayoría de los equipos trata la versión del runtime como un detalle de infraestructura que "se actualiza cuando se pueda". En este tipo de incidente, esa versión define en quién confía tu servidor. La imagen Docker con oven/bun:1.1 olvidada en producción hace un año carga una lista de confianza de hace un año.
Cómo saber si tu runtime confía en eMudhra
Bun implementa las APIs node:tls y node:crypto, así que puedes revisar la lista de raíces integrada en pocas líneas:
// check-roots.ts
import { rootCertificates } from "node:tls";
import { X509Certificate } from "node:crypto";
const sospechosas = rootCertificates
.map((pem) => new X509Certificate(pem))
.filter((cert) => /emudhra/i.test(cert.subject));
for (const cert of sospechosas) {
console.log(cert.subject.replace(/\n/g, ", "));
console.log(" válido hasta:", cert.validTo);
}
console.log(`Bun ${Bun.version}: ${sospechosas.length} raíz(ces) eMudhra`);
bun check-roots.ts
Córrelo en tu máquina y, más importante, dentro de la imagen que va a producción. Si aparece alguna raíz de eMudhra, tu runtime confía en toda su cadena. Eso no significa que te estén atacando. Significa que estás expuesto hasta que llegue la corrección.
Una salvedad: si activas el uso del almacén de certificados del sistema operativo (algunas versiones recientes de Bun y de Node tienen una opción para eso), el resultado también depende del paquete ca-certificates de tu distro. Entonces son dos listas que mantener actualizadas, no una.
Qué hacer en tu proyecto ahora
No hace falta entrar en pánico ni reescribir nada. Hace falta un poco de higiene.
1. Actualiza Bun y sigue de cerca los próximos releases. Cuando Mozilla actúe, el cambio entra en el bundle de certificados del runtime. Sigue el changelog y actualiza la imagen base. Si fijas la versión en el Dockerfile (y deberías), ponte un recordatorio para revisarla.
# antes: olvidado desde el año pasado
FROM oven/bun:1.1
# después: versión explícita, revisada con frecuencia
FROM oven/bun:1.3
2. Para integraciones críticas, restringe la confianza. Si tu backend habla con pocos servicios sensibles (pasarela de pagos, API interna, base de datos), no necesitas confiar en el mundo entero para esas llamadas. El fetch de Bun acepta opciones de TLS:
const caInterna = await Bun.file("./certs/ca-interna.pem").text();
const res = await fetch("https://api.interna.miempresa.com/pedidos", {
tls: {
ca: caInterna, // solo acepta certificados de esta CA
},
});
Para un servicio interno, esto resuelve el problema por completo. Una intermedia comprometida de eMudhra no firma nada que esta CA acepte.
Para servicios de terceros, puedes ir más allá con checkServerIdentity y comprobar la huella de la clave pública (pinning). Funciona, pero tiene un precio: cuando el proveedor cambie la clave, tu integración se rompe a las 3 de la mañana. Úsalo solo si el proveedor publica sus claves y tienes alertas para eso.
3. No lo confundas con rejectUnauthorized: false. He visto mucho código que, ante cualquier error de TLS, desactiva la verificación "solo para probar". En un momento como este, ese código es el peor lugar donde puedes estar. Haz una búsqueda en el repositorio:
grep -rn "rejectUnauthorized: false\|NODE_TLS_REJECT_UNAUTHORIZED" src/
Si aparece algo fuera de los tests, corrígelo hoy. Es gratis.
4. Cuidado con NODE_EXTRA_CA_CERTS. Bun respeta esta variable. Es útil para un proxy corporativo, pero cada CA extra que agregas es una empresa más (o un servidor de TI más) que puede emitir certificados para cualquier dominio. Revisa qué está configurado en tus entornos.
¿Y si no uso Bun en el backend?
El razonamiento vale igual para Node, Deno y cualquier otro runtime que traiga su propia lista de raíces. En React Native, la app usa el almacén del sistema del celular. Eso depende de las actualizaciones de Android o iOS, y un Android viejo sin actualizaciones carga la lista antigua para siempre. En ese caso, el pinning de certificado para tu propia API tiene más sentido.
El navegador es, irónicamente, el cliente más protegido de esta historia. Quien más tiene que perder es el backend que nadie actualiza.
Por qué esto sigue pasando
El sistema de CAs funciona porque la mayoría de las autoridades hace bien su trabajo, y porque existe Certificate Transparency: todo certificado público tiene que registrarse en logs abiertos. Eso es lo que permite que alguien descubra una emisión indebida y arme un sitio como el de eMudhra. Sin CT, un certificado falso podía estar años en uso sin que nadie lo notara.
También puedes usar esto a tu favor. Servicios como crt.sh te dejan buscar todos los certificados emitidos para tu dominio. Si aparece un emisor que no reconoces, es una señal de alerta. Configurar un monitor así lleva cinco minutos y ya ha pillado a mucha gente.
Mi opinión
Este incidente no es motivo para cambiar de runtime ni para hacer pinning en todo. Es un recordatorio de algo que olvidamos: la confianza en TLS es una dependencia, igual que cualquier paquete del package.json. Tiene versión, envejece y necesita actualizarse. Corre el script de verificación, actualiza la imagen base y restringe la CA en las integraciones internas. Una tarde de trabajo, y dejas de depender de la suerte.
Si quieres ver cómo organizo esto en mis proyectos con Bun, échale un vistazo a algunos proyectos que mantengo.
Resumen para LinkedIn
Tu backend Bun puede estar aceptando un certificado falso ahora mismo, y el navegador te estaría protegiendo mejor. Una CA intermedia de eMudhra fue comprometida. Cualquier intermedia de confianza puede emitir certificados para cualquier dominio, incluido el de tu API de pagos. El navegador recibe la revocación en horas. Node y Bun no consultan ninguna revocación: confían en la lista de raíces que viene integrada en el binario. Esa imagen oven/bun:1.1 olvidada en producción confía en el mundo de hace un año. Lo que hice: corrí un script de 10 líneas para ver si el runtime confía en eMudhra, actualicé la imagen base y restringí la CA en las integraciones internas. Lleva una tarde. La confianza en TLS es una dependencia, igual que un paquete del package.json. Tiene versión y envejece. Escribí el paso a paso con el código en el blog. Si quieres revisar tu propio entorno, el enlace está en los comentarios. #Bun #NodeJS #Seguridad #Backend #TLS