Hace unos días estaba diseñando un pipeline de GitHub Actions con herramientas de security scanning. Eligiendo qué integrar, cómo estructurarlo, qué permisos darle, especialmente para el contexto del proyecto para el que estaba haciéndolo. El tipo de trabajo que se siente productivo — estás construyendo algo que va a mejorar la postura de seguridad del equipo.

En ese momento me enteré de lo que le había pasado a Trivy.

El 19 de marzo de 2026, TeamPCP comprometió el escáner de vulnerabilidades de código abierto más usado en el ecosistema cloud-native. No hackearon una aplicación de negocio. No explotaron una vulnerabilidad en código de producción. Comprometieron la herramienta que miles de organizaciones usan para encontrar vulnerabilidades en sus propias aplicaciones. El escáner de seguridad se convirtió en el arma.

Eso me hizo detenerme. No para tirar el pipeline a la basura — sino para replantear desde qué principios lo estaba construyendo.

Este post no es un análisis de threat intelligence. Soy un practitioner que está aprendiendo cada vez más a construir pipelines de seguridad y que reflexionó sobre este caso. Lo que encontré en el camino cambió cómo pienso la confianza en dependencias externas.

¿Qué pasó exactamente?

El ataque no fue un evento único — fue una campaña en fases que se extendió varios días y que empezó antes de lo que la mayoría cree.

Tres semanas antes del 19 de marzo, un bot automatizado llamado hackerbot-claw explotó un workflow mal configurado en el repositorio de Trivy y robó un Personal Access Token. Aqua Security detectó el incidente y rotó credenciales — pero la rotación fue incompleta. TeamPCP retuvo acceso a las credenciales que sobrevivieron. Eso es lo que hizo posible todo lo que vino después. [Palo Alto Networks]

El 19 de marzo a las 17:43 UTC, usando esas credenciales retenidas, TeamPCP comprometió tres componentes simultáneamente:

El binario de Trivy. Se publicó una versión maliciosa — v0.69.4 — en todos los canales de distribución oficiales: GitHub Releases, Docker Hub, GHCR, ECR Public, repositorios deb/rpm, y get.trivy.dev. La lógica de scanning vive en este binario. Los otros dos componentes son wrappers que lo invocan. [Wiz Research] trivy-action. El GitHub Action que la mayoría de los equipos usa para integrar Trivy en sus pipelines. TeamPCP hizo force-push de 76 de 77 tags de versión para que apuntaran a commits maliciosos. Los pipelines que referenciaban estas acciones por tag empezaron a ejecutar el código del atacante en su próxima ejecución — sin ningún cambio visible en el nombre del tag ni en la página de releases. [Microsoft Security Blog] setup-trivy. El Action que instala el binario. Los 7 tags existentes fueron comprometidos de la misma forma.

El payload era un infostealer que robaba SSH keys, tokens de cloud, credenciales de Kubernetes y variables de entorno del pipeline — todo mientras Trivy corría normalmente y producía el output esperado. Para un engineer revisando los logs, el paso aparecía como exitoso. [SANS Institute]

Las ventanas de exposición variaron entre 3 y 12 horas según el componente. Tres días después, el 22 de marzo, TeamPCP publicó imágenes maliciosas adicionales en Docker Hub — v0.69.5 y v0.69.6 — usando credenciales de Docker Hub comprometidas por separado, extendiendo la exposición otras 10 horas. [Legit Security]

Según SANS Institute, se reporta que fueron más de 10.000 workflows de CI/CD afectados.

No explotaron código — explotaron confianza

Cuando la mayoría de los equipos piensa en un ataque, imagina a alguien encontrando un buffer overflow, una inyección SQL, un RCE. Una vulnerabilidad técnica en el código que hay que parchear.

TeamPCP no hizo nada de eso.

No encontraron un bug en Trivy. No rompieron ningún algoritmo criptográfico. Explotaron algo mucho más difícil de parchear: la lógica de confianza implícita que las organizaciones depositan en sus dependencias. La premisa de que si una herramienta viene del proveedor oficial, del canal oficial, con el tag oficial — es segura.

Esa premisa es la que falló.

Hay una ironía brutal en cómo se distribuyó el impacto. Los equipos más diligentes — los que habían integrado Trivy en cada PR, en cada merge, en cada deploy — fueron los más expuestos. Cuanto más disciplinado era el equipo en ejecutar su pipeline de seguridad, más veces corrió el infostealer. La buena práctica se convirtió en el vector. [SANS Institute]

Esto no es un argumento para dejar de escanear. Es un argumento para entender que confiar en una herramienta externa sin verificar su integridad es exactamente lo mismo que confiar en un usuario sin verificar su identidad. En IAM nadie acepta eso. En dependencias, lo aceptamos todo el tiempo.

La expansión del ataque lo confirma. TeamPCP no se quedó en Trivy. El 23 de marzo comprometió Checkmarx KICS — el escáner de infraestructura como código. El 24 de marzo llegó a LiteLLM en PyPI, usando credenciales robadas desde el pipeline de Trivy de BerriAI. Según SANS Institute, un token robado se propagó a través de cinco ecosistemas distintos: CI/CD, npm, Docker, PyPI e infraestructura de IA. [Arctic Wolf]

TeamPCP no está apuntando a aplicaciones de negocio. Está apuntando al ecosistema de herramientas de seguridad — exactamente las herramientas que los equipos más conscientes de seguridad tienen integradas en sus pipelines.

Tags mutables: no el vector, sino el multiplicador

Cuando se reportó el ataque, mucho del foco mediático cayó sobre los tags mutables. Y tiene sentido — es el detalle técnico más llamativo. Pero vale la pena entender exactamente qué rol jugaron, porque no fueron el vector de entrada sino lo que convirtió un acceso puntual en un problema masivo.

Para entender la diferencia, hay que separar tres conceptos.

El vector de ataque fue la credencial retenida — el Personal Access Token que sobrevivió a una rotación incompleta de un incidente previo. Esa credencial con permisos de escritura sobre los repositorios de Aqua Security fue lo que le dio acceso inicial a TeamPCP. Sin ella, nada de lo que siguió habría sido posible. [Palo Alto Networks] La superficie de ataque fueron los pipelines de CI/CD de miles de organizaciones que referenciaban Trivy y sus GitHub Actions por tag. Cada pipeline que ejecutaba uses: aquasecurity/trivy-action@v0.35.0 era una superficie expuesta — no porque tuviera una vulnerabilidad, sino porque su modelo de confianza dependía de la inmutabilidad de un tag que no era inmutable. Según SANS Institute, más de 10.000 workflows de CI/CD formaban parte de esa superficie. Los tags mutables fueron el multiplicador. Una vez que TeamPCP tenía el acceso, la mutabilidad de los tags les permitió redirigir silenciosamente 76 de 77 tags en trivy-action y los 7 tags en setup-trivy a commits maliciosos, sin ningún cambio visible en nombres, fechas ni páginas de releases. [Microsoft Security Blog]

Ahora, la diferencia fundamental entre tag y digest.

Un tag es un puntero con nombre — v0.35.0, latest, v0.69.4. En Git y en registros de contenedores, ese nombre puede redirigirse a cualquier commit o imagen sin que el nombre cambie. No hay notificación. No hay alerta. No hay cambio visible en la página de releases.

Un digest es el SHA256 del contenido real — sha256:a3f8d2c1.... Es inmutable por definición. Si el contenido cambia, el digest cambia. No se puede falsificar, no se puede redirigir. Referenciar una dependencia por digest es anclarla a un contenido específico verificado.

Cualquier pipeline que referenciara esas acciones por tag pasó a ejecutar código malicioso en su próxima ejecución. Sin ningún cambio visible. Sin ninguna alerta. Si esos mismos pipelines hubieran referenciado las acciones por digest, el force-push no habría tenido efecto. El digest seguiría apuntando al commit original. El blast radius habría sido drásticamente menor. [Legit Security]

Supply chain security = mínimo privilegio aplicado a dependencias

Hay un principio que cualquier engineer que trabaja con AWS conoce de memoria: mínimo privilegio. No le das a un rol más permisos de los que necesita. No creas access keys cuando puedes usar roles temporales. No dejas un * en un Resource cuando puedes especificar el ARN exacto.

Es el principio más repetido en seguridad cloud. Y sin embargo, cuando se trata de dependencias externas, lo ignoramos sistemáticamente.

Cuando un pipeline ejecuta uses: aquasecurity/trivy-action@v0.35.0, está depositando confianza total en ese tag — sin verificar su integridad, sin anclar a un contenido específico, sin cuestionar si el puntero sigue apuntando a lo que apuntaba ayer. Es el equivalente de darle AdministratorAccess a un rol porque "es más fácil". Funciona. Hasta que no funciona.

Supply chain security no es un dominio separado de la seguridad que ya practicas. Es el mismo principio de mínimo privilegio aplicado a un nivel diferente: tus dependencias. La pregunta no es "¿confío en esta herramienta?" sino "¿qué verificación tengo de que lo que estoy ejecutando es exactamente lo que creo que es?"

La expansión del ataque ilustra por qué esto importa a escala. TeamPCP no necesitó comprometer a cada organización individualmente. Comprometió una herramienta central del ecosistema y dejó que la confianza implícita hiciera el resto. LiteLLM fue comprometido porque su pipeline usaba Trivy — el infostealer robó el token de publicación en PyPI, y desde ahí TeamPCP publicó versiones maliciosas con 3.6 millones de descargas diarias. [SANS Institute]

Un token. Cinco ecosistemas comprometidos. Esa es la geometría de un ataque de supply chain bien ejecutado.

Lo que hace especialmente incómodo este caso es que las víctimas no eran equipos descuidados. Eran equipos que habían invertido en seguridad, que escaneaban sus pipelines, que usaban herramientas del ecosistema cloud-native. La diligencia no los protegió — porque la diligencia estaba aplicada dentro del perímetro de confianza, no en el perímetro mismo.

La solución no es dejar de usar herramientas externas

La conclusión incorrecta de este incidente sería dejar de usar Trivy, o desconfiar de todas las herramientas de código abierto, o construir todo internamente. Eso no es viable ni es la lección correcta.

La lección es que la confianza en dependencias externas necesita ser explícita y verificable — no implícita y asumida.

Digest pinning

El cambio más concreto e inmediato es referenciar GitHub Actions por digest en lugar de por tag.

En lugar de esto:

- uses: aquasecurity/trivy-action@v0.35.0

Esto:

- uses: aquasecurity/trivy-action@sha256:57a97c7...

El digest ancla el pipeline a un contenido específico verificado. Si alguien hace force-push del tag, el pipeline sigue ejecutando el commit original. La mutabilidad del tag deja de ser un problema porque el pipeline no depende del tag. [Legit Security]

La objeción obvia es que esto hace las actualizaciones más difíciles — y es válida. Un digest fijo no se actualiza solo. Ahí es donde entra la segunda parte.

Dependabot y Renovate

Dependabot y Renovate pueden gestionar actualizaciones de GitHub Actions automáticamente, incluyendo digest pinning. Cuando sale una nueva versión verificada, abren un PR con el digest actualizado. El equipo revisa, aprueba, y el pipeline se actualiza de forma controlada y auditable.

La combinación es la que cierra el loop: digest pinning elimina la exposición a tags mutables, y Dependabot/Renovate elimina la fricción de mantener los digests actualizados manualmente.

Versiones seguras confirmadas

Para quienes usaban Trivy al momento del incidente, las versiones seguras confirmadas son: binario Trivy v0.69.3 o anterior, trivy-action v0.35.0 en el commit 57a97c7, setup-trivy v0.2.6 en el commit 3fb12ec. Cualquier referencia a v0.70.0 en logs debe tratarse como sospechosa — el atacante intentó publicar esa versión pero fue detenido antes de que el tag fuera empujado. [Legit Security]

Secrets: menos es más

Un punto que el incidente expone con claridad es el problema de los pipelines que heredan más secrets de los que necesitan. Si un job de scanning no necesita credenciales de producción, no debería tenerlas — independientemente de que sean temporales o estáticas. El principio de mínimo privilegio aplicado a qué secrets pasan a cada job reduce el blast radius cuando una herramienta es comprometida.

Rotar inmediatamente cualquier credencial que haya estado expuesta a un pipeline que corrió versiones comprometidas de Trivy entre el 19 y el 22 de marzo. Eso incluye tokens de GitHub, credenciales de cloud, tokens de registry, SSH keys y passwords de bases de datos. [Legit Security]

El cierre — lo que pasó en LATAM

Toda la reflexión anterior podría quedarse en lo teórico si no fuera porque alguien de nuestra comunidad vivió esto en tiempo real.

Alejandro Castañeda, AWS Community Builder y Cloud Engineer de Colombia, compartió en LinkedIn lo que le pasó a su equipo. Su pipeline fue comprometido. El infostealer corrió en un runner self-hosted dentro de su cluster EKS y mandó aproximadamente 80KB al servidor del atacante. Para que dimensionen: 80KB es suficiente para llevarse todos los secrets de un pipeline completo.

Y sin embargo, revisaron CloudTrail de arriba a abajo y el resultado fue cero movimiento lateral. Ninguna llamada API desde la IP del atacante. No intentaron crear usuarios, no tocaron políticas de IAM, no accedieron a ningún recurso.

¿Por qué?

Porque el equipo de Alejandro había tomado una decisión de arquitectura tiempo antes, cuando estaban definiendo los cimientos de la infraestructura: OIDC con IAM Roles en lugar de access keys estáticas. Sus pipelines asumían roles temporales que expiraban en minutos. Para cuando el atacante tuvo las credenciales en sus manos, ya eran papel mojado. Y la trust policy del rol solo permitía uso desde GitHub — así que aunque no hubieran expirado, no podían usarse desde otro lado.

Si hubieran tenido access keys estáticas guardadas como secrets en GitHub, la historia sería completamente distinta. El atacante habría tenido acceso a ECR para inyectar imágenes maliciosas en contenedores de producción, y a Secrets Manager para leer credenciales de bases de datos y tokens de servicios financieros.

La diferencia entre "nos robaron secrets y no pasó nada" y un desastre real fue una decisión de arquitectura que nadie tomó pensando en este ataque específico. La tomaron porque era la forma correcta de construir.

Eso es lo que me llevo de este incidente. No la lista de versiones afectadas — eso cambia. No el nombre del grupo atacante — eso también cambia. Lo que no cambia es el principio: diseñar para que cuando alguien comprometa una dependencia, no encuentre nada útil del otro lado.

La seguridad no va de construir un muro perfecto. Va de que cuando alguien lo salte, no encuentre nada útil del otro lado.

Sobre el autor

Gerardo Castro es AWS Security Hero y Cloud Security Engineer con foco en LATAM. Fundador y Lead Organizer del AWS Security Users Group LatAm. Cree que la mejor forma de aprender seguridad en la nube es construyendo cosas reales — no memorizando frameworks. Escribe sobre lo que construye, lo que encuentra, y lo que aprende en el camino.

🔗 GitHub: gerardokaztro

🔗 LinkedIn: gerardokaztro

Gerardo Castro

Gerardo Castro

AWS Security Hero · Founder, AWS Security Users Group LatAm

Cloud Security Engineer con foco en LATAM. Cree que la mejor forma de aprender seguridad en la nube es construyendo cosas reales — no memorizando frameworks.

Comentarios