Detectar pivotes de CI/CD antes de que lleguen a producción
Un runner con demasiados secretos es un acceso inicial. Tres detecciones que disparan en cuanto un pipeline empieza a comportarse como un atacante.

Un runner de build es una máquina que ejecuta código arbitrario, guarda credenciales de producción y goza de la confianza de todo lo que hay aguas abajo. Casi todas las organizaciones vigilan de cerca sus controladores de dominio y no vigilan sus runners en absoluto. Esa asimetría es el problema entero, y no exige que un atacante haga nada exótico: un pipeline al que han hecho ejecutar código ajeno se comporta casi igual que un pipeline ejecutando el tuyo.
El pivote rara vez es espectacular. Alguien abre una pull request contra un repositorio cuyo workflow se dispara con pull_request y corre con los secretos del repositorio. O una dependencia arrastra un script de post-install. O el token de un desarrollador, sacado de un portátil, sirve para empujar una rama que edita el fichero de workflow que corre en cada push. A partir de ahí el runner hace lo que hacen los runners: autenticarse contra un proveedor cloud, bajar artefactos, firmar cosas, desplegar. Cada una de esas acciones es legítima por separado. El problema de detección es que el comportamiento normal del runner ya resulta indistinguible de los objetivos de una intrusión.
Primero la línea base, o nada de esto funciona
Las tres detecciones de abajo comparan el comportamiento presente con el esperado, lo que significa que no sirven de nada hasta que sabes qué se supone que hace cada pipeline. Ese inventario es el trabajo de verdad, y es poco lucido: para cada workflow, qué secretos guarda, con qué hosts externos habla legítimamente y qué artefactos produce.
Hacerlo una vez produce algo más valioso que las detecciones. Durante el ejercicio, la mayoría de los entornos descubren que un número nada despreciable de workflows guarda credenciales que ya no usa, habla con hosts que nadie sabe explicar y se dispara con triggers que tenían sentido hace dos años. Arreglar eso quita más riesgo que cualquier alerta.
Detección uno: salida de tráfico que el pipeline no tiene motivo para hacer
Un build de un servicio Java habla con tu registry de artefactos, con tus mirrors de paquetes y con los endpoints de API de tu proveedor cloud. Ese conjunto es pequeño, estable y conocible. Cualquier cosa fuera de él es o una dependencia legítima nueva, que alguien puede confirmar en un minuto, o el caso interesante.
La señal es más fuerte acotada por workflow que por flota de runners, porque la salida agregada de una flota es la unión de las necesidades de todos los proyectos y resulta enorme. Por workflow suele ser una docena de hosts.
title: CI runner contacted an unbaselined destination
logsource:
category: network_connection
detection:
runner:
Image|contains:
- '/runner/'
- '/actions-runner/'
known_good:
DestinationHostname|endswith:
- '.internal-registry.acme.local'
- '.blob.core.windows.net'
- 'registry.npmjs.org'
condition: runner and not known_good
falsepositives:
- A genuinely new dependency host. Confirm with the workflow owner, then baseline it.
level: medium
Dos notas de implementación importan más que el texto de la regla. Resuelve y registra el hostname de destino, no solo la dirección, o un destino alojado en un CDN parecerá cien cosas distintas a lo largo de una semana. Y aplícalo al namespace de red del runner en concreto: lanzarlo contra todo el clúster de build produce un volumen que nadie leerá pasado el primer día.
La detección de salida caza el extremo tosco del espectro: bajar un payload de segunda etapa, mandar secretos robados a alguna parte, abrir un canal hacia fuera. No caza a un atacante que se limite a infraestructura en la que ya confías, y por eso no es la única regla aquí.
Detección dos: deriva del alcance del token
Esta es la señal de más valor de las tres y la que más veces falta por completo.
Las credenciales de un pipeline se emiten con un alcance. Un workflow que construye y publica una imagen de contenedor necesita escritura en el registry y nada más. Cuando esa misma identidad empieza a enumerar IAM, a leer secretos que nunca ha leído o a llamar a servicios fuera de su radio de impacto, la credencial se está usando para algo distinto de su propósito. La cuenta está autorizada, así que el proveedor cloud lo permite y devuelve éxito. El evento es invisible salvo que compares las llamadas contra un perfil por identidad.
Construye ese perfil desde el log de auditoría. Treinta días de histórico de una identidad de workflow dan un conjunto de acciones de API corto y muy repetitivo; un build hace las mismas cuatro cosas cada vez que corre. Alerta la primera vez que se observe una acción fuera de él.
let baseline =
CloudAudit
| where TimeGenerated between (ago(30d) .. ago(1d))
| where PrincipalType == "workload_identity"
| distinct Principal, Action;
CloudAudit
| where TimeGenerated > ago(1d)
| where PrincipalType == "workload_identity"
| join kind=leftanti baseline on Principal, Action
| project TimeGenerated, Principal, Action, Resource, SourceIP
Lo que hace que esto funcione es el reconocimiento. Cualquiera que aterrice en un runner y no conozca ya el entorno tiene que mirar alrededor primero, y mirar alrededor significa listar cosas. Las llamadas List*, Describe* y Get* contra servicios que un build no ha tocado nunca son lo primero que ocurre y lo más barato de cazar. Un atacante que sepa exactamente qué única llamada de API quiere se escapará, pero ese atacante tuvo que aprender el entorno en algún sitio, y eso es problema de otra detección.
La misma regla caza una cantidad considerable de deriva ordinaria: roles demasiado amplios, ficheros de workflow copiados y pegados, credenciales compartidas entre pipelines que no deberían compartirlas. Trata esos hallazgos como la regla pagándose sola, no como ruido.
Detección tres: el artefacto no coincide con el código
El estado final de un pipeline comprometido suele ser una salida de build modificada, firmada por tu infraestructura y en la que confía todo lo que hay aguas abajo. Si el cambio es lo bastante pequeño, ningún humano lo revisa, porque los humanos revisan código fuente y no binarios.
La reproducibilidad es la respuesta limpia y es difícil. Comparar builds sucesivos del mismo componente es la alcanzable: construye dos commits adyacentes, haz diff de las salidas y espera que el delta se corresponda con el delta del código. Un cambio de una línea en la documentación que altera el árbol de dependencias, añade una llamada de red o cambia el entrypoint merece una pregunta.
- Haz diff de los manifiestos de dependencias entre builds, resueltos y no declarados. Un cambio transitivo sigue siendo un cambio.
- Vigila las llamadas salientes nuevas o los procesos nuevos que introduzca un commit que no añadió ninguna de las dos cosas.
- Alerta cuando el digest de salida de un build cambie para un commit que ya se había construido. Entradas deterministas deberían producir el mismo artefacto.
- Registra qué workflow, qué runner y qué identidad produjeron cada artefacto que promocionas, para que uno malo se pueda rastrear hasta la ejecución que lo hizo y no hasta una franja horaria aproximada.
Un pipeline que puede desplegar a producción es producción. Vigílalo como producción, o asume que estás vigilando la ruta menos defendida hacia ella.
Por dónde empezar
Si solo tienes capacidad para una, quédate con la deriva del alcance del token. No requiere agente en el runner, los datos ya están en tu log de auditoría cloud y es la única de las tres que caza a un atacante operando enteramente dentro de infraestructura en la que confías. La línea base de salida va segunda, y sale barata una vez existe el inventario. La comparación de artefactos es la que más trabajo cuesta y la más duradera.
Después, pruébalas. Una detección que nadie ha disparado a propósito es una hipótesis, no un control, que es el tema de otra nota.
// ¿te ha servido esta nota?