← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El token del agente era el del propietario

El 23 de octubre, LiteLLM publicó la causa raíz de su versión 1.107.0 envenenada. La primera credencial robada no salió de un registro. Salió de la propia plataforma de agentes de LiteLLM, cuya API devolvía el token de GitHub que sus agentes en la nube usaban para abrir pull requests: el token de un propietario de la organización, sin restringir. La clave de esa API llevaba cuatro meses y medio en un archivo de configuración público. En Bromure Agentic Coding, un agente que abre pull requests tiene un sustituto, el token real se queda cifrado en el Mac y ninguna API lo devuelve.

Los agentes de LiteLLM necesitaban un token de GitHub para abrir pull requests. Recibieron uno que pertenecía a un propietario de la organización. Una sola llamada a la API con una clave filtrada lo devolvió.

El 23 de octubre, LiteLLM publicó la cadena de ataque detrás de su versión 1.107.0, la que salió el 21 de octubre con un ladrón de credenciales dentro. El artículo se presenta como «la fuente de verdad canónica» y corrige la primera teoría de LiteLLM. Los tokens de GitHub robados no salieron del tráfico de modelos registrado. El primero salió de un servicio experimental que LiteLLM operaba para sus propios agentes de programación.

Cubrimos la versión en sí en La versión tenía dos horas, y allí seguimos la primera teoría. El informe final empieza en otro sitio. La primera credencial que se llevó el atacante pertenecía a los agentes.

Una plataforma de agentes con el token de un propietario

LiteLLM Agent Platform era, en palabras de LiteLLM, «un proyecto experimental de agentes en la nube autoalojados para enviar PR a repositorios de GitHub». Para abrir pull requests, la plataforma tenía un token de GitHub. Ese token «no estaba asociado a una cuenta de servicio y no tenía el alcance restringido». Era el token OAuth de la CLI de GitHub de uno de los dos propietarios de la organización.

La cronología del informe, en UTC:

  • 27 de mayo, 18:53. Se sube un archivo opencode.json al repositorio público LiteLLM-Labs/lite-harness. Es la configuración de un arnés de agentes y contiene la clave maestra de un despliegue de Agent Platform. El equipo lo detecta, sobrescribe el commit con un force-push y marca las claves expuestas para su revocación. «La revocación no se produjo para la clave maestra».
  • 10 de octubre, 21:27. Desde 69.48.229.143, el atacante llama a la API de la plataforma con esa clave. GET /api/v1/managed_agents/agents?limit=100 devuelve la configuración de cada agente gestionado y, con ella, el token del propietario.
  • 10 de octubre, 22:11. Con el token del propietario, el atacante sube una rama desechable, ci-tmp-a351cc, a BerriAI/litellm. Su workflow envía seis secretos del repositorio a webhook.site. Uno de ellos, GH_TOKEN, es el token de la CLI de GitHub de un segundo mantenedor. Veinticuatro segundos después, una segunda rama en BerriAI/litellm-auto envía el token de un tercer mantenedor al mismo endpoint. Ambas ejecuciones se borran en menos de tres minutos.
  • 12 de octubre. Más workflows en repositorios privados envían todos los secretos de repositorio a los que llegan.
  • 21 de octubre, 10:08. La protección de rama rechaza un push al repositorio de publicación hecho con el token del segundo mantenedor. Cinco segundos después, el token del propietario desactiva la protección y fuerza el push del cambio, que apunta el workflow de publicación al imitador Berri-AI/litellm.
  • 21 de octubre, 11:11. El token del segundo mantenedor aprueba la puerta de producción. Una regla que exigía la aprobación de una cuenta vinculada a un dispositivo en la oficina de LiteLLM se había relajado esa semana, así que dos ingenieros cualesquiera podían publicar.
  • 21 de octubre, de 11:39 a 11:46. Siete wheels llegan a PyPI, y después las imágenes de Docker Hub y GHCR.
  • 21 de octubre, 11:53. El token del tercer mantenedor borra los registros de la ejecución de publicación. A las 12:24, el token del propietario deja main limpio con un force-push.

El resumen de LiteLLM: «Este incidente ha ilustrado con claridad los riesgos de no rotar una clave potencialmente expuesta». Ahora está revocando todos los tokens vinculados a cada sistema interno y cada plataforma de proveedor.

Toda flota de agentes tiene un token así

LiteLLM cuenta con un equipo de seguridad capaz, y su configuración era habitual. Un agente que abre pull requests necesita un token que pueda hacer push. El token más a mano es tu propia sesión de gh, y ese es el que recibieron los agentes de LiteLLM.

Después, el token tiene que vivir en algún sitio donde los agentes puedan leerlo: una variable de entorno, un archivo de configuración, una fila de base de datos detrás de una API que configura los agentes. Quien pueda leer ese sitio tiene el token. Aquí bastó una clave de API que se filtró una vez y nunca se rotó.

Los tokens de la CLI de GitHub lo empeoran. LiteLLM señala que «son posteriores a la 2FA y no están sujetos a los flujos de reautenticación 2FA de “superusuario” de GitHub asociados a acciones sensibles sobre la organización o el repositorio». Con uno de ellos, el atacante podía desactivar la protección de rama, forzar un push, aprobar una publicación y borrar las pruebas.

Busca las mismas piezas en tu propia configuración: un GH_TOKEN en el entorno del agente, una configuración MCP con un token bearer dentro, una configuración de arnés que alguien acabará subiendo. El opencode.json de LiteLLM contenía más que la clave de la plataforma. También llevaba tokens bearer para sus servidores MCP de GitHub, Linear y Slack y la clave de su pasarela de modelos, todo en texto plano.

La misma flota en una máquina Bromure

Bromure Agentic Coding ejecuta Claude Code, Codex, Grok, Kimi y omp en VM Linux en tu Mac. Sus automatizaciones hacen lo que hacía la plataforma de agentes de LiteLLM: un agente se lanza según un horario o ante un nuevo issue de GitHub, trabaja en un worktree nuevo y sube una rama o abre una pull request. Reproduce la cadena de LiteLLM contra una máquina que ejecuta esas automatizaciones.

El agente tiene un sustituto

Añade el token de GitHub a la máquina en Credentials. Al arrancar, Bromure escribe uno falso en la VM: un valor ghp_ de la longitud correcta, en GH_TOKEN, en la configuración de gh y en ~/.git-credentials. Los gh pr create y git push del agente funcionan como siempre. Cuando una petición sale de la VM hacia github.com, el proxy del Mac cambia el falso por el token real, y solo para ese host.

El token real se guarda cifrado en el Mac, con una clave que se conserva en el llavero de macOS. Nunca se escribe en la VM, ni en un archivo ni en una variable. El manual dice del canal entre el host y la VM: «No existe ninguna petición con la que la VM pueda pedir de vuelta un token real».

Así que el paso que abrió la organización de LiteLLM no tiene nada que devolver. Vuelca el entorno del agente, lee su configuración, lista todas las automatizaciones por la API: el valor de GitHub en todas ellas es el sustituto. Pégalo en tu propio gh y GitHub lo rechaza, porque solo el proxy de esa máquina sabe a qué sustituye.

La configuración subida también tiene sustitutos

La filtración empezó con una configuración de arnés en un repositorio público. En una máquina Bromure, los archivos de configuración que lee un agente se escriben con sustitutos. El token bearer de un servidor MCP, definido en el panel MCP de la máquina, llega al agente como un valor brm-mcp_…, que se cambia en tránsito para las peticiones al host de ese servidor. Una clave de modelo definida en Preferences › Models nunca entra en la VM; el agente ve uno falso con el formato del proveedor. Cualquier otra clave de API va en Other API keys con un filtro de host, y la VM recibe un valor brm_… en la variable que indiques.

Cuando un agente sube su configuración por error, o un desarrollador la copia en un repositorio de demostración, el archivo lleva sustitutos. Nada de lo que contiene funciona fuera de ese Mac, así que ni un force-push ni una lista de revocaciones que nadie termina deciden si te comprometen.

No hay API de plataforma a la que llamar

El atacante de LiteLLM llegó a la API de la plataforma desde internet. Las automatizaciones de Bromure se ejecutan en tu Mac. En palabras del manual: «no hay ningún servicio en la nube ni ninguna superficie de red entrante». Los disparadores de GitHub y Linear se consultan desde el Mac cada tres minutos con el token guardado de la máquina, que se queda en el host. No hay endpoint de webhook, ni clave maestra, ni ruta /managed_agents que pueda abrir una clave filtrada.

Un sustituto robado hace saltar una alarma

Los workflows del 10 de octubre enviaban secretos a webhook.site. Imagina la misma jugada contra una máquina Bromure: una dependencia envenenada o una instrucción inyectada en un prompt lee GH_TOKEN y lo envía a webhook.site.

El proxy examina cada petición saliente, cabeceras y cuerpo, en busca de sustitutos que vayan a un host distinto de aquel para el que se emitieron. Un sustituto ghp_ con destino webhook.site se bloquea con un HTTP 451 antes de que salga un solo byte. La VM se pausa y una alerta en el Mac muestra This environment may have been compromised, con la vista previa del token, el nombre de la credencial, el host al que pertenece y el host al que se envió. Puedes apagar la máquina o guardar su disco y su carpeta personal para investigarlos.

Con Unmatched traffic en Deny en la política de sandbox de la máquina, webhook.site ni siquiera es alcanzable, y el intento aparece como una fila roja en la Security Timeline.

Un push te espera

Las máquinas Bromure nuevas arrancan con la política de escritura de GitHub en Prompt before write. Las lecturas pasan. Un git push o cualquier otra escritura en github.com se detiene en un diálogo en tu Mac:

Allow write on "GitHub" from workspace "release-bot"?
POST /repos/<owner>/<repo>/git/refs

Todas las jugadas del atacante eran escrituras: las ramas ci-tmp-, el cambio en el workflow de publicación, el force-push a main, el borrado de los registros de ejecución. Desde una máquina Bromure, cada una se habría detenido en ese diálogo. Allow once no concede nada más allá de esa única escritura, así que el siguiente push vuelve a preguntar. Un force-push sigue siendo un push en tránsito, y Prompt before write lo controla.

Una automatización desatendida no puede esperar un clic, así que dale el token más limitado que cumpla su función, con una política propia para esa máquina. El token que añadas a una máquina release-bot puede ser uno de grano fino para un solo repositorio. Tu propia sesión de gh, con derechos de propietario sobre todo, no tiene por qué estar en esa máquina.

El registro está en tu Mac

LiteLLM cotejó hashes de tokens en el registro de auditoría de GitHub para saber qué token robado hizo qué. En una máquina Bromure, cada sustitución de una credencial en tránsito aparece como una fila Credential brokering en la Security Timeline, con el token enmascarado y el host. Cada escritura que rechazas es una fila roja Guardrails con el método y la ruta. En un Mac inscrito en bromure.io, los mismos eventos llegan a tu organización, así que un token usado desde una máquina inesperada aparece sin que nadie tenga que rebuscar en registros de auditoría.

De un archivo de configuración a una versión firmadaLiteLLM, del 27 de mayo al 21 de octubre1 · Clave en una configopencode.json en un repopúblico, nunca revocada2 · La API da el tokenla config del agente tiene eltoken gh de un propietario3 · Secretos enviadosramas ci-tmp envían dostokens más a webhook.site4 · Pushes y publicaciónprotección fuera, force-push,puerta aprobada, logs borradosEn una máquina BromureSustitutos en configslas claves MCP, de modelo yAPI de la VM son falsas. Uncommit no filtra nada.Ningún token que darsin API entrante; el agentetiene un sustituto ghp_ queGitHub rechaza fuera.Bloqueado, VM pausadaun sustituto rumbo awebhook.site recibe un 451y una alerta.Prompt before writecada push, force-pushy escritura de API esperaun diálogo en el Mac.
La cadena de ataque de LiteLLM, y dónde una máquina Bromure que ejecuta los mismos agentes detiene cada paso. Cada comprobación se ejecuta en el Mac, fuera de la VM.

Los agentes reciben sustitutos

GH_TOKEN, la configuración de gh y ~/.git-credentials tienen un ghp_ falso. El token real se cambia en el Mac, solo para los hosts de GitHub.

Ninguna API reparte tokens

Las automatizaciones se ejecutan en tu Mac sin superficie entrante. Nada que la VM pueda llamar devuelve una credencial real.

Las filtraciones pausan la máquina

Un sustituto enviado a cualquier sitio que no sea su propio host se bloquea con un 451, y la VM se pausa mientras decides.

Las escrituras preguntan antes

Las máquinas nuevas preguntan antes de cada push y cada escritura en GitHub, force-push incluido, con la petición exacta en pantalla.

No le des el token al agente

LiteLLM está revocando todos sus tokens y publicará sus cambios de proceso la semana que viene. La rotación, los alcances reducidos y las cuentas de servicio ayudan. Pero cada una deja un token funcional en algún sitio donde un agente puede leerlo, y eso es lo que el atacante fue a buscar el 10 de octubre.

En una máquina Bromure, el agente trabaja con un token que solo funciona a través de su propio proxy, hacia el único host para el que se emitió. Todo lo que el agente puede leer también puede leerlo un atacante que llegue al agente, y en una máquina Bromure eso nunca incluye el token real.

Para ver cómo resisten esos mismos sustitutos a un stealer que apunta a los archivos de los agentes de programación, lee El stealer se llevó los sustitutos. Para las copias que un agente deja en sus propias transcripciones, lee La transcripción se quedó con el token.