Observabilidad en Operaciones Ofensivas
El correo llegó a las 09:14 de un martes. El CISO del cliente, escueto: "¿qué hizo exactamente tu agente a las 03:42 de la noche? El SOC vio una conexión saliente que no estaba en el runbook y la bloqueó. Necesitamos saber si fue cosa vuestra."
Le respondimos en once minutos. No porque tuviéramos buena memoria, sino porque buscamos el trace_id del comando que el SOC nos pasó y reconstruimos la cadena entera: el agente había encontrado un endpoint /api/internal/health que respondía con un banner de versión, había decidido validar si era explotable, y antes de enviar nada Sentinel le cortó la operación porque el destino estaba fuera del CIDR autorizado. La "conexión saliente" era un SYN que ni siquiera completó el handshake. Le mandamos al cliente el span completo, con el ATT&CK ID, la decisión del policy engine y los 47 bytes que salieron por el cable.
Ese día decidimos que la observabilidad iba a ser un producto, no un nice-to-have.
La observabilidad ofensiva no se parece a la defensiva
Llevo años montando pipelines de telemetría para SOCs. Loki, Tempo, Jaeger, lo que toque. Y al principio pensé que el problema de instrumentar un agente ofensivo era el mismo problema, solo que con otro schema. Estaba equivocado.
Un SIEM defensivo asume que el ruido del sistema es señal: cada auth.fail importa, cada DNS extraño importa. La cardinalidad explota pero el modelo mental es claro: guardar todo, decidir luego. En ofensiva el modelo se invierte. El agente es el ruido por definición: escanea, prueba, falla, retoma. Si guardas todo con el granular de un EDR, una operación de cuatro horas te genera 18 GB de telemetría que nadie va a leer.
Pero hay algo que sí tienes que poder reconstruir con precisión quirúrgica: la cadena causal de cualquier acción que tocó al cliente. Cualquiera. Si el agente envía un payload a producción, tienes que saber por qué decidió ese payload, qué información previa lo justificaba, qué validó el policy engine y qué respondió el objetivo. No es paranoia: es lo que separa un red team profesional de un script kiddie con presupuesto.
Las guías de mapeo de MITRE ATT&CK lo dicen sin rodeos: la evidencia tiene que estar anclada en telemetría real, no en suposiciones del operador (CISA, 2023)[1]. Si tu informe dice "se ejecutó T1190 contra el host X", quiero ver el span con el http.request.body, la decisión del agente, y el código de respuesta. Sin eso, es opinión.
Diseño de eventos: spans, traces, atributos
Usamos OpenTelemetry como columna vertebral. No porque esté de moda, sino porque las semantic conventions resuelven un problema real que tuvimos: cada operador escribía los logs como le daba la gana. Un día un target_host, otro día un dst, otro victim_ip. Imposible correlacionar. OpenTelemetry impone un schema compartido y, lo más importante, propaga el trace context a través de procesos y workers (OpenTelemetry, 2024)[2].
El Gandalf CLI emite cada comando como un span. Cada span lleva su trace_id, su span_id, un parent_span_id que apunta al razonamiento que lo originó, y atributos siguiendo nuestra convención berialabs.* encima de las semánticas estándar. La regla interna: si un atributo existe en la spec oficial, lo usamos tal cual. Si es específico de ofensiva (técnica ATT&CK, decisión del agente, hash del payload), lo prefijamos para no contaminar el namespace.
Un ejemplo concreto. Esto es un span real (anonimizado) de un agente probando una inyección SQL en un parámetro de búsqueda:
{
"name": "gandalf.exploit.sqli_attempt",
"trace_id": "4a1f9b2c8e3d7f6a5b4c3d2e1f0a9b8c",
"span_id": "7c8b9a0d1e2f3a4b",
"parent_span_id": "6b7a8c9d0e1f2a3b",
"start_time_unix_nano": 1709823742891000000,
"end_time_unix_nano": 1709823743104000000,
"kind": "SPAN_KIND_CLIENT",
"status": { "code": "STATUS_CODE_OK" },
"attributes": {
"http.request.method": "GET",
"http.response.status_code": 500,
"url.full": "https://target.example.com/api/search?q=*REDACTED*",
"server.address": "10.42.7.18",
"server.port": 443,
"berialabs.attack.tactic": "TA0001",
"berialabs.attack.technique": "T1190",
"berialabs.agent.decision_id": "dec_8f3a",
"berialabs.agent.reasoning_ref": "trace://4a1f9b2c.../6b7a8c9d",
"berialabs.payload.sha256": "9e3f...c7a1",
"berialabs.payload.family": "boolean_blind_sqli",
"berialabs.sentinel.scope_check": "passed",
"berialabs.sentinel.cidr_match": "10.42.0.0/16",
"berialabs.evidence.response_signature": "mysql_error_xpath"
},
"events": [
{
"name": "sentinel.validation",
"attributes": {
"policy.id": "scope_v3",
"policy.result": "allow"
}
},
{
"name": "response.received",
"attributes": {
"response.size_bytes": 1247,
"response.contains_error_signature": true
}
}
]
}Hay tres cosas que me importan de este formato. Primero, el parent_span_id apunta al razonamiento del agente, no al comando anterior; eso me permite reconstruir por qué hizo lo que hizo, no solo qué hizo. Segundo, el sha256 del payload se referencia, no se inlinea: el cuerpo entero vive en un almacén separado, y el span solo guarda el hash. Tercero, los eventos del span capturan transiciones clave (la validación de Sentinel, la respuesta) que un atributo plano no podría representar bien.
ATT&CK como atributo, no como tag suelto
Mapeamos cada acción a su técnica ATT&CK en el momento de emitir el span, no a posteriori. El agente lleva una tabla interna que asocia familias de payload a IDs de técnica, y el atributo viaja con el span hasta Tempo. Cuando el cliente nos pide un informe, no es un ejercicio arqueológico: es una query en Grafana filtrando por berialabs.attack.technique.
eBPF para capturar lo que el agente no te cuenta
Aquí está el truco que me llevó tiempo aceptar. Por muy bien que instrumentes tu agente, hay cosas que el agente no sabe que está haciendo. Una biblioteca de terceros que abre un socket que tú no esperabas. Una llamada DNS que pasa por getaddrinfo sin pasar por tu cliente HTTP. Un proceso hijo que escribe un fichero temporal. Si solo confías en la instrumentación de userspace, tu trazabilidad tiene agujeros.
Por eso pusimos hooks de eBPF en el kernel del host donde corre el agente. eBPF permite ejecutar programas sandboxed dentro del kernel y capturar eventos sin modificarlo (Gregg, 2019; ebpf.io)[3]. Nosotros enganchamos cuatro cosas: tcp_connect, execve, openat y resolución DNS vía udp_sendmsg. Cada evento se enriquece con el cgroup_id del proceso, que correlacionamos con el trace_id activo del agente vía una pequeña tabla en memoria compartida.
El resultado: si el agente abre una conexión a un IP que el span dice que abrió, perfecto, todo cuadra. Si abre una a un IP que no aparece en ningún span, salta una alerta. Lo hemos usado un par de veces para descubrir que una librería de scraping hacía prefetch de favicons sin avisar. No fue malicioso, pero podía haberlo sido, y el cliente tenía derecho a saberlo.
Una nota incómoda: eBPF es potente, pero no es inviolable. Hay trabajo público mostrando cómo rootkits diseñados ad-hoc pueden cegar herramientas basadas en eBPF si el atacante ya controla el kernel (Matheuz, 2024)[4]. En nuestro caso el modelo de amenaza es distinto (queremos auditar nuestro propio agente, no defendernos de un adversario con root), pero conviene tenerlo presente.
El pipeline
De agente a Grafana, los saltos son estos. El Gandalf CLI emite OTLP por gRPC al OpenTelemetry Collector que corre como sidecar. El Collector hace tres cosas: filtra atributos sensibles con un processor que escribe a /dev/null cualquier cosa que matchee patrones de PII (correos, números que parezcan tarjetas, headers de auth), batchea y reexporta. Las trazas van a Tempo. Los logs estructurados van a Loki vía el otlphttp exporter, que Loki acepta nativamente desde la 3.0 (Grafana Labs, 2024)[5]. Las métricas (latencia por técnica, ratio de payloads bloqueados por Sentinel, throughput de peticiones) van a Prometheus.
Encima de todo, Grafana. Y la killer feature no es ningún dashboard bonito: es el trace-to-logs. Click en cualquier span, salta a los logs correlacionados por trace_id. Click en cualquier log, salta al span. Esa correlación es la que nos permitió responderle al CISO en once minutos.
Los trade-offs que duelen
No todo es bonito. Tres tensiones reales que seguimos negociando.
Ruido vs señal. Si instrumentas cada decisión interna del agente, generas millones de spans por operación. Si solo instrumentas comandos externos, pierdes la cadena causal. Encontramos un equilibrio razonable instrumentando los nodos de decisión del agente (no cada token) y los side-effects externos sin excepción. Aun así, en operaciones largas hemos visto picos de 200k spans/hora.
Retention. Tempo en S3 cuesta poco, pero los logs en Loki con alta cardinalidad cuestan más. Nuestra política actual: trazas completas 90 días, logs 30 días, agregados (métricas y resúmenes) tres años. La presión legal por SOC 2 y por contratos de red team nos empuja a guardar más, no menos.
PII en logs. Este es el que más me preocupa. Si el agente extrae un volcado de una base de datos para demostrar el impacto, ese volcado no debe quedar en los logs. El filtro del Collector ayuda, pero no es suficiente. Mantenemos una segunda capa: los hallazgos sensibles se cifran con la clave pública del cliente antes de tocar el pipeline, y solo se guardan referencias.
Cómo cambió nuestra forma de operar
Antes de tener este pipeline, los informes de red team los hacíamos a mano. Capturas de pantalla, copy-paste de outputs, narrativa reconstruida de memoria. Tardábamos días. Ahora la mayoría del informe se genera consultando Tempo y Loki, y el operador humano se dedica a interpretar, no a transcribir.
Más importante: discutimos con el cliente desde un terreno común. No "creemos que el agente hizo X" sino "aquí está el span de las 03:42:18 con la decisión, el payload y la respuesta". Un cliente nos dijo el mes pasado que era la primera vez que un red team le entregaba telemetría que su SOC podía ingestar tal cual para entrenar sus reglas de detección. Eso, para mí, es la métrica que importa: que la observabilidad ofensiva sea útil también para el defensor.
Y cuando llega un correo a las 09:14 de un martes, respondemos en once minutos.
Referencias
- CISA (2023). Best Practices for MITRE ATT&CK Mapping.
- OpenTelemetry Authors (2024). Semantic Conventions Specification 1.41.
- eBPF Foundation (2024). What is eBPF? An Introduction and Deep Dive into the eBPF Technology.
- Matheuz (2024). Breaking eBPF Security: How Kernel Rootkits Blind Observability Tools.
- Grafana Labs (2024). Ingesting logs to Loki using OpenTelemetry Collector.