Tool Calling Seguro en Entornos Air-Gapped
El primer aviso fue un paquete DNS. Uno solo, saliendo de un laboratorio que se suponía aislado, hacia un dominio que parecía un identificador en base32. El operador lo estaba auditando con un agente local sobre Qwen 2.5 7B servido por Ollama, y el agente, en teoría, no tenía acceso a la red más allá del rango interno del lab. El paquete salió porque uno de los tools que el agente podía llamar era resolve_target, y el modelo, ante un prompt envenenado que venía en un banner SMB indexado por nuestro RAG, decidió que el siguiente paso lógico era resolver un FQDN que el atacante había metido a propósito en el banner.
No fue una APT. Fue una prueba de concepto que nos hicimos a nosotros mismos, y nos sirvió para entender algo que llevábamos meses dándole vueltas: el tool calling es la nueva superficie de ataque, y en entornos air-gapped esa superficie se siente más segura de lo que realmente es. Spoiler: no lo es.
Qué pasa cuando le das manos al modelo
El tool calling cambió todo. Pasamos de modelos que escupían texto a agentes que ejecutan nmap, leen ficheros, consultan vectores y, si nos descuidamos, hacen peticiones salientes. Yao y compañía lo describieron muy bien en su trabajo sobre jailbreaking via function calling, donde reportan tasas de éxito por encima del 90% atacando GPT-4o, Claude 3.5 Sonnet y Gemini 1.5 Pro a través del propio mecanismo de funciones (Wu et al., 2024)[1]. El modelo se alinea contra el usuario, pero no necesariamente contra las herramientas que le ofreces.
El problema es estructural. Cuando un LLM decide qué tool llamar, está consumiendo contexto. Y ese contexto, en un pentest, es texto no confiable por definición: respuestas HTTP, banners, ficheros recuperados, salidas de scanners. Wang y colaboradores lo formalizaron en From Allies to Adversaries, mostrando cómo un atacante puede inyectar manipulator tools o envenenar respuestas para forzar al agente a llamar funciones que no debería (Wang et al., 2024)[2]. La línea entre "datos" y "instrucciones" se difumina, y el modelo, que no tiene un parser semántico que distinga ambas cosas, se traga el envenenamiento.
El mito del air-gap
Mucha gente asume que si el modelo corre en local, sobre Ollama, sin salida a internet, el problema desaparece. No desaparece, se transforma. Vimos tres patrones recurrentes en nuestros propios laboratorios:
- Exfiltración por canal lateral: DNS interno, ARP, logs que se sincronizan más tarde, ficheros temporales que alguien recoge.
- Pivoting interno: el agente, legítimamente, tiene acceso a la red del cliente; el atacante lo usa como proxy.
- Auto-envenenamiento del RAG: el agente guarda observaciones que un futuro agente leerá como contexto autoritativo.
Esto último es especialmente desagradable. Los trabajos de Shi y otros sobre Log-To-Leak describen exactamente este vector: el agente invoca una herramienta de logging aparentemente benigna que termina filtrando queries, respuestas y estado interno (Shi et al., 2025)[3]. Y como el log vive dentro del perímetro, los SIEM tradicionales no ven nada raro.
Cómo lo abordamos en la práctica
Te lo vendo claro: no hay bala de plata. Lo que tenemos es una serie de capas, cada una asumiendo que la anterior puede fallar. En nuestro stack interno separamos tres responsabilidades, porque mezclarlas en un solo binario es pedir problemas.
Por un lado, Gandalf hace de gateway. Es lo único que habla con el modelo y con el operador. Tiene un componente que llamamos Sentinel que aplica políticas antes y después de cada tool call: valida que los argumentos están en el scope CIDR autorizado, que el comando no encaja en ninguna firma de exfiltración, y que el agente no está intentando salir del jardín. Si algo huele raro, el kill-switch corta la sesión, descarta el contexto y avisa.
Por otro, Gwaihir es el ejecutor. Cuando una llamada pasa Sentinel, se materializa como un proceso hijo con un filtro seccomp-bpf que solo permite el subconjunto de syscalls que ese tool concreto necesita. Nada de connect() arbitrario, nada de execve() a binarios fuera del allowlist. Esto se inspira directamente en lo que Wei y colaboradores plantean en Securing AI Agent Execution, donde argumentan que el ejecutor debe ser aprovisionado dinámicamente solo con los permisos del paso actual del plan (Wei et al., 2025)[4]. Principio de mínimo privilegio aplicado por syscall, no por rol.
Y luego Beorn, que mantiene el RAG con los vectores de HTB que usamos como conocimiento operativo (unos 9115 ahora mismo). Beorn nunca recibe input del modelo de forma directa; las consultas pasan por Gandalf, que las normaliza. El RAG es de solo lectura desde la perspectiva del agente, lo que mata el vector de auto-envenenamiento que mencionaba antes.
Un ejemplo de cómo se ve una política de Sentinel para un tool de escaneo:
{
"tool": "nmap_scan",
"scope_cidr": ["10.10.11.0/24"],
"deny_flags": ["-oN", "--script=http-fetch", "-iL"],
"max_runtime_s": 120,
"seccomp_profile": "gwaihir/profiles/nmap.json",
"kill_switch": {
"on_outbound_dns": true,
"on_unexpected_egress": true,
"on_token_budget_exceeded": 4096
}
}El on_outbound_dns fue lo que nos pilló el incidente del banner SMB. Cualquier resolución que no caiga en la zona interna del lab dispara el kill-switch antes de que el paquete salga de la interfaz.
Modelos locales: por qué Qwen, Llama y Phi
Operar contra modelos hosteados es, sencillamente, incompatible con air-gap. Pero hay otra razón más sutil: los modelos comerciales tienen tool calling entrenado con un esquema fijo, y cuando te sales de ese esquema tienden a alucinar argumentos. Con Qwen 2.5, Llama 3.1 y Phi-4 servidos por Ollama, podemos forzar structured output con gramáticas GBNF, lo que reduce muchísimo la superficie de "argumentos creativos". No es perfecto, pero es auditable.
El trade-off es real. Pierdes capacidad bruta: un Qwen 7B no razona como Claude Opus en un encadenamiento de cinco pasos con tools encadenados. Lo compensas dividiendo el plan en pasos más pequeños y dejando que Sentinel valide cada uno por separado. Ganas, a cambio, trazabilidad completa, latencia predecible (los tokens cuestan ms, no dólares) y la posibilidad de auditar el modelo bit a bit si hace falta.
Lo que se rompe cuando aíslas
No todo es ganancia. Hay tres cosas que se nos rompieron al aislar todo:
La primera, visibilidad de amenazas emergentes. Sin telemetría que salga del lab no puedes correlacionar con feeds de CTI en tiempo real. Lo resolvimos con un canal asimétrico: el lab no habla hacia fuera, pero un proceso fuera del lab puede tirar de un bucket interno cada X minutos y enriquecer.
La segunda, actualización del RAG. Los 9115 vectores de HTB que tenemos en Beorn no se actualizan solos. Hay que reindexar fuera y volver a empujar el snapshot firmado. Friccion operativa, sí, pero predecible.
La tercera, UX del operador. Acostumbrado a chatear con un modelo grande, el operador a veces siente que Qwen es "más tonto". Lo es, en cierto modo. Pero un modelo tonto que ejecuta dentro de un seccomp-bpf con scope CIDR y kill-switch es mucho menos peligroso que un modelo brillante con acceso libre a syscalls.
Take-away
Si te llevas algo de aquí, que sea esto: el air-gap no es una propiedad, es una arquitectura. Y dentro de esa arquitectura, el tool calling es el sitio donde la confianza se rompe primero. Empieza por lo más barato y lo más efectivo, en este orden:
- Define un scope CIDR explícito por sesión, no por agente. Sentinel-equivalente que lo valide antes de cada call.
- Mete cada ejecución de tool detrás de un filtro seccomp-bpf con allowlist de syscalls. Si no tienes Gwaihir, mira bubblewrap, gVisor o nsjail.
- Implementa un kill-switch que reaccione a egress no esperado, no solo a comandos sospechosos. El modelo te va a sorprender en los argumentos, no en los nombres de los tools.
- Trata el contexto del RAG como datos no confiables. Sí, incluso el que tú metiste ayer.
El paquete DNS aquel no llegó a ningún lado. Pero el log de Sentinel sigue en nuestro post-mortem, recordándonos que un agente local, sin internet y con buenas intenciones, puede intentar hablar con un dominio inventado porque alguien lo escribió en un banner hace seis meses. Esa es la realidad operativa. El resto es teatro.
Confía en los modelos lo justo para que hagan su trabajo. Desconfía de su contexto siempre.
Referencias
- Wu, Z. et al. (2024). The Dark Side of Function Calling: Pathways to Jailbreaking Large Language Models. arXiv:2407.17915.
- Wang, H. et al. (2024). From Allies to Adversaries: Manipulating LLM Tool-Calling through Adversarial Injection. arXiv:2412.10198.
- Shi, Y. et al. (2025). Log-To-Leak: Prompt Injection Attacks on Tool-Using LLM Agents via Model Context Protocol. OpenReview.
- Wei, J. et al. (2025). Securing AI Agent Execution. arXiv:2510.21236.
- Patel, R. et al. (2025). Architecting Resilient LLM Agents: A Guide to Secure Plan-and-Execute Patterns. arXiv:2509.08646.