Volver a todas las publicaciones
Publicado el · por Renaud Deraison

Cambiaron la dirección, no el paquete

Durante catorce horas, el 31 de agosto de 2026, algunas peticiones a registry.coder.com las respondieron servidores que un atacante había añadido al propio pool de origen de Cloudflare de Coder. El hostname era correcto, el certificado validaba, los módulos tenían los nombres y las versiones de siempre, y el código que volvió salió a buscar claves SSH, tokens OIDC y contraseñas de bases de datos. Coder no tiene los registros del atacante, así que su aviso te pide que revises tu propio tráfico de salida. Esa es una pregunta que la mayoría de las máquinas que ejecutan un agente de programación no puede responder.

El hostname era el de Coder. El certificado validaba. El módulo llevaba el nombre y la versión que tu plantilla ya fijaba. Durante catorce horas, el 31 de agosto, algunos de los servidores que respondían eran de otra persona.

Una ingeniera de plataforma actualiza una plantilla de espacio de trabajo un lunes por la mañana. terraform init descarga los módulos que la plantilla nombra (Docker, Zed, aider) desde registry.coder.com, el mismo sitio del que venían el mes pasado. Terraform imprime su muro de verde habitual y el build pasa. Ninguna herramienta señala nada, ni esa mañana ni ese mes.

El 1 de septiembre, Coder publicó GHSA-vx42-ghc9-gw65, calificado como crítico con CVSS 9.0. En sus palabras: “Un actor malicioso no identificado obtuvo acceso a la infraestructura de Cloudflare de Coder y añadió direcciones IP no autorizadas al pool que usa el registro de módulos de Coder.” Entre las 07:35 y las 21:45 UTC del lunes 31 de agosto, algunas peticiones de módulos de Coder las sirvieron máquinas que controlaba el atacante. Coder construye entornos de desarrollo en la nube autoalojados; BleepingComputer menciona a Dropbox, Palantir, Square, Mercedes-Benz, KKR, EnBW, organismos del gobierno de EE. UU. y contratistas de defensa entre sus usuarios.

Nadie tocó el paquete

Todas las costumbres de cadena de suministro que tiene un equipo cuidadoso son comprobaciones del artefacto. Si el nombre está bien escrito. Si quien publica es quien dice ser. Si esta versión se publicó ayer. Si el hash del lockfile sigue cuadrando. Lee el diff antes de subir la versión.

El atacante cambió qué máquinas respondían cuando tu cliente preguntó por la dirección, y dejó el artefacto en paz. No publicó ningún paquete, no se apoderó de ninguna cuenta de mantenedor, no registró ningún nombre parecido y no editó nada en el registro de Coder. El nombre resolvió como resuelve siempre. El handshake TLS se completó contra un certificado válido para el hostname real. El módulo se llamaba docker, y zed, y aider, porque esos son los módulos que pide una plantilla de Coder, y llevaba el número de versión que la plantilla ya fijaba.

Lo que volvió era distinto. Las copias envenenadas traían un script de shell llamado dlp.sh, con su propio build para los módulos docker, aider, rstudio-server, windows-rdp y zed, y su propio hash para cada uno en el aviso. El aviso describe la carga útil como código “diseñado para identificar una serie de credenciales y exfiltrarlas a un servidor remoto con un nombre de dominio parecido a uno que Coder usa habitualmente.” Ese dominio es coder-infra[.]com, registrado el 28 de agosto, tres días antes de que nadie lo usara, resolviendo a 199.91.220.205, con la llamada de vuelta en http://www[.]coder-infra[.]com/cli/check.

La lista de lo que buscaba se lee como un inventario de la confianza de una plataforma de desarrollo: variables de entorno y secretos del provisionador, tokens OIDC de usuarios, claves SSH, credenciales de autenticación externa y variables de configuración de coderd, incluidas contraseñas de bases de datos y credenciales del proveedor de autenticación.

No hacía falta hacer nada raro para estar dentro de la ventana. Según el aviso: “Los usuarios que usan el registro principal de módulos de Coder (registry.coder.com) como origen de sus plantillas y que actualizaron componentes durante el periodo en el que este código estuvo disponible pueden haber instalado los paquetes maliciosos.” Crear una plantilla, subir la versión de una plantilla, lanzar un dry run o construir un espacio de trabajo con la caché de módulos apagada es trabajo normal de lunes.

31 de agosto, 07:35–21:45 UTC — la ventana de catorce horasel build pideterraform initplantilla nueva, subidade versión o dry runel nombre resuelveregistry.coder.comCloudflare, la puertade entrada realuna IP en el poolañadida por el atacante,sirviendo un módulodlp.shla llamadacoder-infra[.]comregistrado el 28/08,usado el 31/08Qué comprobó el cliente, y qué encontróel hostnameel propio de Coder, noun dominio parecidoel certificadoválido, para el nombrereal, en el edge realel nombre del módulodocker, zed, aider —los que pedistela versiónla que ya estaba en laplantilla. Nada nuevoLas cuatro comprobaciones pasan. Qué servidor respondió es lo único que ninguna comprobación del cliente mira.
El atacante añadió sus propias direcciones IP al pool de origen que hay detrás de registry.coder.com, así que el hostname, el certificado, el nombre del módulo y la versión fijada eran todos genuinos mientras los bytes venían de otro sitio. Un build envenenado se diferencia de uno limpio en una sola cosa observable: una conexión de salida a un dominio registrado tres días antes.

“Revisa los registros de firewall, proxy, DNS y flujo de la VPC”

Ese es el paso cinco de la remediación de Coder, y la línea más instructiva del aviso. Los pasos del uno al cuatro son cosas con las que Coder puede ayudar: SQL para encontrar módulos cacheados de la ventana, SQL para purgarlos, versiones parcheadas (2.37.0, 2.36.4, 2.35.7 y 2.34.9, todas publicadas el 1 de septiembre) y una lista de credenciales que rotar. El paso cinco va más allá de todo eso, porque los servidores que dirían a quién le tocó son del atacante. Coder no puede verlos, y lo dice: no puede identificar de forma concluyente cada despliegue comprometido.

Así que la pregunta vuelve a ti, y es una pregunta sobre pruebas. ¿Algo en la máquina que ejecutó ese módulo se conectó a coder-infra[.]com? Para una VPC de producción, la mayoría de los equipos puede responder en menos de una hora, porque alguien del equipo de plataforma ya es dueño de los registros de flujo.

Ahora hazte la misma pregunta sobre la máquina donde trabaja tu agente de programación. Un agente ejecuta terraform init, npm install, make, un build de contenedor, una batería de pruebas, un script que encontró en el repositorio, un binario que descargó un módulo. Cada una de esas cosas abre conexiones, y nadie las está anotando en un sitio donde pudieras buscarlas una semana después. La transcripción del propio agente guarda las llamadas a herramientas que hizo el modelo. No tiene ninguna fila para lo que un script de shell dentro de un módulo de Terraform hizo con la red, porque el modelo nunca vio que ocurriera.

No puedes comprar una respuesta al paso cinco a posteriori. O algo registró la petición cuando salía de la máquina, o no hay nada que buscar.

Lo que habría encontrado el script

Toma la carga útil en serio y déjala correr. Barre el entorno en busca de credenciales, lee las claves SSH, se lleva los tokens OIDC y de autenticación externa, recoge las variables de configuración que llevan dentro la contraseña de la base de datos y lo publica todo en coder-infra[.]com.

Bromure Agentic Coding le da a cada espacio de trabajo su propia VM Ubuntu bajo el hipervisor de Apple, con un proxy en el host como única ruta del guest hacia la red. Ejecuta ahí el mismo barrido y vuelve con un juego completo de credenciales, todas falsas.

ANTHROPIC_API_KEY guarda un señuelo sk-ant-api03-brm-…, GH_TOKEN una cadena ghp_ de la longitud correcta, y ~/.git-credentials, ~/.docker/config.json y ~/.kube/config guardan marcadores equivalentes. Las formas se mantienen, así que gh, docker y kubectl las aceptan sin rechistar. El proxy mantiene tus valores reales cifrados en tu Mac y los cambia en el cable después de que la petición haya salido de la VM, solo cuando va dirigida al host para el que se acuñó esa credencial. Ningún interruptor apaga esto; así funcionan las credenciales en la aplicación.

Las claves SSH son el caso más limpio: no hay ninguna en la VM que leer. SSH_AUTH_SOCK apunta a un puente sobre un socket virtual hasta un agente en el host, y ese protocolo tiene una petición que significa “firma este reto” y ninguna petición que signifique “dame la clave”. ls ~/.ssh/id_* vuelve vacío mientras git push sigue funcionando.

AWS merece su propio párrafo, porque es contra AWS contra lo que corre Terraform. El ~/.aws/config de la VM apunta a un helper que entrega tu ID de clave de acceso real emparejado con una clave secreta falsa. Terraform, los SDK de AWS, boto3 y la CLI de aws leen ese helper sin ninguna configuración, luego firman sus peticiones con la falsa y producen una firma bien formada que AWS rechazaría. El host quita esa firma y vuelve a firmar con el secreto real de salida. Un script que rastrilla el entorno buscando material de AWS se va con un ID de clave y cuarenta caracteres que no autentican nada.

Después tiene que enviar el botín a alguna parte, y ahí es donde está el firewall de salida del espacio de trabajo. Pon Unmatched traffic en Deny y la VM llega a los hosts que hayas listado y a nada más. La aplicación de la regla ocurre dos veces, en dos sitios que el guest no puede alcanzar: el switch de red virtual casa cada flujo por IP de destino y por hostname espiado en el DNS, en todos los protocolos, y el proxy vuelve a casarlo por el nombre de servidor TLS. Nadie puso un dominio de tres días en esa lista. Las ediciones de reglas llegan a las sesiones en marcha sin reiniciar, así que puedes añadir deny any coder-infra[.]com con el build todavía corriendo.

Si el tráfico sale igualmente, porque el espacio de trabajo corre en la postura por defecto que permite todo o porque el destino parecía corriente, los señuelos hacen su segundo trabajo. El proxy escanea cada petición de salida, cabeceras y cuerpo, buscando una credencial falsa que va a un sitio para el que nunca se acuñó. Una cadena sk-ant-api03-brm-… dirigida a coder-infra[.]com no es ambigua. El proxy la bloquea con HTTP 451 antes de que un solo byte llegue al destino, pausa la VM y levanta una alerta nombrando la credencial y los dos hosts. Bromure marca el espacio de trabajo como comprometido, y no volverá a arrancar hasta que borres su disco y su home.

Tienes el paso cinco respondido mientras ocurre, en vez de reconstruirlo a partir de registros una semana después.

En la máquina que ejecutó el móduloterraform apply → dlp.shcorre con los privilegios del propio buildqué recogeenv vars y secretos del provisionadorclaves SSH, credenciales de auth externatokens OIDC de usuariosconfig de coderd — contraseña de la BDsalida: un POST HTTPS desde un host debuild que hace HTTPS todo el día“¿nos tocó?” → registros que quizá no tengasSalieron credenciales reales, y nada anotó la petición.En un espacio de trabajo de Bromureterraform apply → dlp.shsigue corriendo — como ubuntu, en el guestVM desechable · el proxy del host es la única salidacada env var y token de config es un señueloningún byte de clave privada — el host firma por vsockAWS: key ID real, secret falso, el host refirmafirewall de salida deniega por defecto — rechazadaun señuelo fuera de su alcance → HTTP 451,VM pausada, espacio marcado como comprometidocada petición tiene su fila, sea cual sea el procesoRecogió señuelos, el proxy rechazó la llamada y guardó la fila.
El mismo módulo, el mismo dlp.sh, en dos máquinas. A la izquierda encuentra variables de entorno reales, claves SSH reales y tokens reales, y su llamada de vuelta es una petición HTTPS corriente que nadie anotó. A la derecha encuentra señuelos, ningún byte de clave privada y un secreto de firma que no autentica nada, su llamada se topa con un firewall de salida que deniega por defecto, y un señuelo dirigido a un host fuera de su alcance detiene la VM.

Un comando responde al aviso

El proxy del host escribe un registro de metadatos para cada petición que sale de la VM, sea cual sea el proceso que la hizo: hora, host, puerto, método, ruta, estado, latencia y bytes medidos antes de cualquier cambio de credencial. Pon el Session trace del espacio de trabajo en Activity only y obtienes ese registro sin que se almacene ningún cuerpo de petición. Después, bromure-cli trace hostnames my-workspace imprime cada host distinto que ha contactado el espacio de trabajo, con recuentos. El paso cinco de Coder, en una línea, para la máquina donde corrió el código.

Lo que decidieron los motores

La Security Timeline (Window → Security Timeline…) es la vista aparte de lo que los motores de Bromure decidieron: cada veredicto del firewall de salida, cada credencial que intermedió el proxy, cada bloqueo de cadena de suministro, con código de color y filtros. Una conexión rechazada a un dominio registrado hace tres días aparece ahí como una fila roja de Firewall mientras el build sigue corriendo, no como un grep por un mes de registros de flujo después de que el aviso de otra persona te diga dónde mirar.

El guest puede llegar a una sola cosa de tu Mac: una carpeta compartida, una ventana viva de lectura y escritura a tu sistema de archivos real que el borrado por compromiso deja en paz por diseño. Comparte el directorio del proyecto en lugar de tu directorio home, y un script que va a la caza de id_rsa y .env recorre un home de Linux lleno de checkouts y cachés de paquetes.

Cuando no puedes comprobar el origen, vigila la salida

Todas las comprobaciones de procedencia se reducen a un movimiento: comparar lo que llegó con algo en lo que ya confías. Un nombre, quien publica, una firma, un hash en un lockfile. El movimiento funciona mientras el atacante tenga que meter algo en el canal para que puedas compararlo. Un atacante que controla un tramo del canal no mete nada en él, y el pool de origen de Coder era un tramo del canal. El compromiso de BdThemes en agosto tenía la misma forma, donde un bucket de almacenamiento secuestrado cambió un feed JSON que descargaban 350.000 sitios WordPress, sin ninguna publicación y sin ningún archivo modificado.

Catorce horas son una jornada de trabajo. Coder lo detectó, entregó parches a la mañana siguiente, publicó hashes e indicadores y les dijo a sus lectores qué comprobar. Aun así, el aviso termina pidiéndole a cada lector que vaya a mirar su propio tráfico de salida, porque el registro del otro lado ya no existe.

Rota las claves que lista el aviso, purga los módulos cacheados, aplica el parche. Después decide a qué va a poder llegar el siguiente, porque el siguiente llegará por un canal del que no tenías ninguna razón para dudar. Instala Bromure Agentic Coding, dale al build su propia máquina, y ten la respuesta lista para cuando aparezca el aviso.