Runbook: enjaular al agente de IA — bubblewrap en el cliente, Tetragon en el cluster
Compañero operativo de El contratista con la llave maestra. Aquel post explica el porqué y el dónde —el modelo de amenaza, las cinco familias de aislamiento, qué dominio usa cada una—; este es el cómo, con comandos. Si no lo has leído, léelo antes: aquí doy por sabido qué es el radio de explosión, por qué
bwrapcorre sin root y qué vigila Tetragon. El procedimiento va en dos tracks independientes —cliente y cluster— porque, como argumenta el post hermano, el control se extrapola pero la primitiva se reescribe.
TL;DR
Dos procedimientos reproducibles. Cliente (workstation): instala ai-jail (envuelve bubblewrap), genera el .ai-jail por proyecto, audita con --dry-run, fija las allowlists con --bootstrap, usa --lockdown para lo que no te fíes, y deja al agente sin permiso de git push. Cluster (RKE2 con Cilium + Tetragon): pon el baseline de pod (securityContext sin privilegios, seccomp: RuntimeDefault, NetworkPolicy default-deny), mete el pod del agente no confiable en una microVM con runtimeClassName: kata, y despliega las TracingPolicy de Tetragon en dos fases —observar con action: Post para levantar el baseline, luego promover a action: Sigkill sobre tcp_connect (egress) y security_file_open (rutas de secretos)—. La regla de oro de la fase Tetragon: adopta primero, bloquea después; nunca metas un Sigkill en producción sin haber visto antes los eventos en modo observación.
El flujo de los dos tracks
Track A — Cliente (workstation del desarrollador)
A0 — Instalar ai-jail y bubblewrap
ai-jail envuelve el sandbox; en Linux necesita bubblewrap aparte, en macOS no necesita dependencia extra.
# ai-jail (macOS y Linux)
brew tap akitaonrails/tap && brew install ai-jail
# o, con cargo:
cargo install ai-jail
# o, con mise:
mise use -g ubi:akitaonrails/ai-jail
# bubblewrap en Linux (elige tu distro)
sudo pacman -S bubblewrap # Arch
sudo apt install bubblewrap # Debian / Ubuntu
sudo dnf install bubblewrap # Fedora
Comprueba que el binario está y que bwrap corre sin root (no debe pedir sudo):
ai-jail --version
bwrap --ro-bind / / --unshare-all echo "bwrap ok sin root"
Si bwrap falla pidiendo privilegios, tu kernel tiene los unprivileged user namespaces deshabilitados; habilítalos (sysctl kernel.unprivileged_userns_clone=1 en Debian/Ubuntu antiguos) antes de seguir.
A1 — El fichero .ai-jail por proyecto
En el primer arranque dentro del proyecto, ai-jail crea un .ai-jail (TOML) commiteable al repo: cualquier compañero que clone hereda la misma política.
cd ~/Projects/mi-app
ai-jail claude # crea .ai-jail y lanza Claude Code dentro del sandbox
El fichero generado:
# .ai-jail — configuración del sandbox (commitéalo al repo)
command = ["claude"]
rw_maps = [] # directorios extra con escritura
ro_maps = [] # directorios extra de solo lectura
Antes de confiar en el sandbox, audítalo. --dry-run --verbose imprime cada punto de montaje, cada flag de aislamiento y el comando bwrap completo, sin ejecutar nada:
ai-jail --dry-run --verbose claude
Lee la salida y confirma tres cosas: que $HOME se monta como tmpfs (no el real), que ~/.ssh, ~/.aws y ~/.gnupg no aparecen entre los montajes, y que el único directorio con escritura es el del proyecto. Si necesitas un directorio extra:
ai-jail --rw-map ~/Projects/shared-lib claude # extra con escritura
ai-jail --map /opt/datasets claude # extra de solo lectura
Otros agentes, mismo binario:
ai-jail codex
ai-jail opencode
ai-jail bash # shell pelado para depurar el sandbox
ai-jail -- python script.py # cualquier comando
A2 — Las allowlists de permisos con –bootstrap
--bootstrap genera las configuraciones de permisos de cada agente, con allow/deny/ask sensatos, y hace backup antes de sobrescribir:
ai-jail --bootstrap
Lo que produce, en resumen:
| Agente | Fichero | Política base |
|---|---|---|
| Claude Code | ~/.claude/settings.json | allow: git status/diff/log, ls, grep, cargo, npm, python, docker compose · ask: git push, rm, docker run · deny: rm -rf, sudo, chmod 777, git push --force |
| Codex | ~/.codex/config.toml | approval_policy = "on-request" |
| OpenCode | ~/.config/opencode/opencode.json | permisos de bash, edit, write |
La clave operativa: git push está en ask, no en allow, y git push --force en deny. El agente puede commitear, ramear y rebasar localmente cuanto quiera; nada de eso toca el remoto. (Si usas el /sandbox de Claude Code, fija además "allowUnsandboxedCommands": false para cerrar el escape hatch dangerouslyDisableSandbox, que de fábrica es opt-out.)
A3 — Lockdown para lo que no te fíes
Para auditar código de terceros o correr un agente sobre un proyecto que no conoces, --lockdown va más allá: proyecto montado en read-only, GPU/Docker/display deshabilitados, --rw-map/--map ignorados, $HOME tmpfs puro sin dotfiles del host, red cortada con --unshare-net y environment limpiado con --clearenv.
ai-jail --lockdown bash
Es el sandbox más restrictivo posible sin llegar a una VM. Úsalo como defecto mental para todo lo que no sea tu propio código en tu propia máquina.
A4 — La red de seguridad: git sin push
No es un flag, es una propiedad del entorno que cambia el cálculo de riesgo. Si el proyecto está en git con remoto, y el agente no tiene permiso de push, el peor caso —que corrompa cada fichero del proyecto— se revierte con:
git checkout . # vuelve al último commit
# y si tocó .git (improbable): borra el dir y re-clona
El remoto nunca se tocó. Sandbox para el filesystem + git para el código + push manual es ya un nivel razonable para uso diario: ai-jail protege tus datos y el sistema, git protege el código, y la decisión de publicar sigue siendo tuya.
Track B — Cluster (RKE2 con Cilium + Tetragon)
El agente no confiable —o la inferencia que ejecuta código generado— corre como pod. El mismo principio del cliente, otras primitivas. Asumimos un cluster genérico RKE2 con Cilium como CNI y Tetragon ya desplegado (el DaemonSet del agente eBPF en cada nodo).
B0 — El baseline del pod
Antes de cualquier eBPF, lo de serie. securityContext sin privilegios, raíz read-only, seccomp por defecto:
apiVersion: v1
kind: Pod
metadata:
name: ai-agent
namespace: agentes
labels:
app: ai-agent
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: agent
image: registry.interno/ai-agent:pinned
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: work, mountPath: /work } # único escribible
volumes:
- name: work
emptyDir: {}
Y el corte de egress por defecto —el gemelo cluster del --unshare-net—. NetworkPolicy default-deny de salida en el namespace, abriendo solo DNS y lo imprescindible:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: agentes
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
B1 — RuntimeClass Kata: el pod no confiable en su propia microVM
Para código realmente no confiable, sácalo del kernel compartido. Con Kata desplegado existe un RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata
handler: kata
Y el pod lo pide con una línea —runtimeClassName: kata—, ejecutándose en su propia microVM con kernel dedicado en lugar de compartir el del nodo:
spec:
runtimeClassName: kata # ← el pod corre en una microVM, no en el kernel del nodo
# ...resto igual que B0
Es el gemelo cluster del aislamiento por construcción: un exploit de kernel dentro del pod no alcanza al nodo.
B2 — Tetragon, fase observación (Post)
Ahora la capa que distingue una plataforma con visibilidad de runtime. Primero observar, nunca matar de entrada. Una TracingPolicyNamespaced —scoped al namespace y a la etiqueta del agente— que reporta (no mata) tres cosas: ejecuciones de proceso, conexiones de red y aperturas de rutas sensibles. action: Post solo emite el evento.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
name: agente-observa
namespace: agentes
spec:
podSelector:
matchLabels:
app: ai-agent
kprobes:
# --- conexiones salientes ---
- call: "tcp_connect"
syscall: false
args:
- index: 0
type: "sock"
selectors:
- matchActions:
- action: Post
# --- aperturas de ficheros sensibles ---
- call: "security_file_open"
syscall: false
args:
- index: 0
type: "file"
selectors:
- matchArgs:
- index: 0
operator: "Prefix"
values:
- "/var/run/secrets"
- "/work/.git/config"
matchActions:
- action: Post
(Las ejecuciones de proceso no necesitan kprobe: Tetragon emite process_exec/process_exit de forma nativa.) Despliega y observa los eventos en vivo desde el pod de Tetragon del nodo:
kubectl apply -f agente-observa.yaml
# eventos legibles, filtrando por el namespace:
kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents -o compact --namespace agentes
Deja esto rodando una jornada típica del agente. Apunta a qué destinos conecta de verdad (tu registry interno, tu mirror de HF, tu endpoint de vLLM) y qué rutas abre. Eso es tu baseline: la lista de lo legítimo. Sin este paso, un Sigkill mata trabajo bueno y te genera un incidente de disponibilidad —justo lo que el ENS te pide evitar—.
B3 — Tetragon, fase enforcement (Sigkill)
Con el baseline en la mano, promueve a bloqueo. Dos reglas. La primera: mata cualquier conexión cuyo destino no esté en la allowlist —NotDAddr invierte el match: dispara para todo lo que no sea esas redes—. La segunda: mata cualquier intento de abrir una ruta de secretos.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicyNamespaced
metadata:
name: agente-enforce
namespace: agentes
spec:
podSelector:
matchLabels:
app: ai-agent
kprobes:
# --- egress: mata todo lo que NO sea la allowlist ---
- call: "tcp_connect"
syscall: false
args:
- index: 0
type: "sock"
selectors:
- matchArgs:
- index: 0
operator: "NotDAddr"
values:
- "127.0.0.1"
- "10.0.0.0/8" # red interna del cluster
- "172.16.10.20" # registry interno (ejemplo)
matchActions:
- action: Sigkill
# --- lectura de secretos: mata el proceso ---
- call: "security_file_open"
syscall: false
args:
- index: 0
type: "file"
selectors:
- matchArgs:
- index: 0
operator: "Prefix"
values:
- "/var/run/secrets/kubernetes.io/serviceaccount/token"
- "/work/.ssh"
matchActions:
- action: Sigkill
kubectl apply -f agente-enforce.yaml
Ahora el agente puede hacer lo que quiera dentro del pod, pero en el instante en que intenta conectar a un destino no permitido o leer el token de la service account, Tetragon lo mata en el kernel —antes de que el paquete salga o el read devuelva bytes—. Es el gemelo cluster de la blocklist de curl y del ~/.ssh no montado, pero aplicado en runtime y sobre cualquier binario, no solo los que conoces.
Aviso operativo. El enforcement con
Sigkillrequiere kernel reciente con soporte de la acción en eBPF (5.10+ es seguro). Despliegaagente-enforceprimero en un namespace de pruebas, y manténagente-observaactivo en paralelo: si el bloqueo dispara, el eventoPostte dice exactamente qué lo provocó. Adopta primero, bloquea después.
La tabla de equivalencias cliente ↔ cluster
El mismo vector, las dos primitivas. Esto es “extrapolar la tecnología” hecho explícito:
| Vector de amenaza | Cliente (workstation) | Cluster (RKE2) |
|---|---|---|
$HOME / raíz escribible | $HOME como tmpfs efímero (bwrap) | readOnlyRootFilesystem: true + emptyDir |
| Egress arbitrario | blocklist curl/wget · --unshare-net | NetworkPolicy default-deny + Tetragon NotDAddr→Sigkill |
| Lectura de secretos | ~/.ssh/~/.aws/~/.gnupg no montados | secretos fuera del pod + Tetragon security_file_open→Sigkill |
| Escape del kernel | Landlock (2ª barrera VFS) | runtimeClassName: kata (microVM, kernel propio) |
| Sin escape hatch | proceso dentro de bwrap, sin salida | sin privileged, drop ALL, allowPrivilegeEscalation:false |
| Daño al código | git remoto sin push → git checkout . | GitOps + revisión de PR, el agente no aplica a main |
| Visibilidad | --dry-run --verbose (estático, pre-run) | Tetragon tetra getevents (dinámico, en runtime) |
Checklist de gotchas
- No metas un
Sigkillsin pasar porPost. El baseline de observación no es opcional: es lo que separa “bloquear un C2” de “tirar tu propio job de fine-tuning”. - El
.ai-jailse commitea; los secretos no. El TOML es política, no credenciales. Verifica que no metes rutas con datos sensibles enrw_maps. readOnlyRootFilesystemrompe apps que escriben en/tmp. Monta unemptyDiren/tmpademás del de trabajo.- NetworkPolicy sin regla de DNS deja al pod ciego. Abre el puerto 53 a
kube-systemo nada resuelve. - Kata no es gratis. Añade latencia de arranque y no todo workload con dispositivos especiales (GPU passthrough) encaja; resérvalo para lo no confiable, no para todo.
- El
/sandboxde Claude Code no cubre MCP ni hooks salvo que activessandbox-runtime. Si tu agente usa servidores MCP, asume que corren con permisos completos hasta que lo hagas. NotDAddrcon IPs literales envejece mal. Documenta la allowlist y revísala cuando cambie el registry o el endpoint de inferencia; considera CIDRs internos estables en vez de IPs sueltas.
Ver también
- El contratista con la llave maestra: aislar agentes de IA del workstation al cluster — el panorama que este runbook ejecuta: modelo de amenaza, las cinco familias de aislamiento y por qué cliente y cluster usan primitivas distintas.
- La puerta de la cocina que el maître no miró: Cilium eBPF y DRANET — la capa eBPF de Cilium sobre la que Tetragon engancha sus kprobes; el datapath que ya tienes en el cluster.
- Controles técnicos: ENS × ISO 42001 × EU AI Act — los eventos de Tetragon como evidencia técnica de
op.mon/op.exp; el enforcement como medida de protección. - Guardrails y safety en LLM — la mitigación en el plano del contenido; este runbook, la del plano de la ejecución.
- Siete fases de despliegue de una plataforma LLM on-premise — dónde encaja el endurecimiento de runtime en la secuencia de despliegue (F4 identidad/políticas, F5 plataforma).
Referencias
- ai-jail (Fabio Akita), GPL-3.0: https://github.com/akitaonrails/ai-jail
- bubblewrap: https://github.com/containers/bubblewrap
- Landlock LSM: https://landlock.io
- Tetragon — TracingPolicy: https://tetragon.io/docs/concepts/tracing-policy/
- Tetragon — enforcement (Sigkill/Override): https://tetragon.io/docs/concepts/enforcement/
- Kata Containers — Kubernetes RuntimeClass: https://katacontainers.io
- Kubernetes — Pod Security & seccomp: https://kubernetes.io/docs/tutorials/security/seccomp/
- Kubernetes — Network Policies: https://kubernetes.io/docs/concepts/services-networking/network-policies/
- Cilium: https://cilium.io