cap_drop ALL para un agente IA en Docker
Resumen
Cómo no dejar NET_ADMIN y SYS_ADMIN en el bot: cap_drop ALL, y no cap_add salvo que un proxy lo pida. Rootfs read-only y USER node no quitan capabilities. Compose cap_drop y docker run --cap-drop=ALL. Docs oficiales del 3 de septiembre de 2026.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Linux capabilities son bits extra (NET_RAW, SYS_ADMIN). El default de Docker no es cero. Un agente que llama tools no necesita NET_ADMIN. USER y read-only no dropean caps. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La regla
cap_drop: [ALL]. Si algo se rompe, cap_add una y documenta por qué. El webhook HTTP no pide SYS_PTRACE.
docker run --cap-drop=ALL. Privileged: nunca.
Compose
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
no-new-privileges evita setuid. Junto a USER node.
Qué no hace
No sustituye firmar webhooks (firmas). No cierra 5432 público (red privada). Es el kernel del contenedor.
Workers/Vercel: no hay caps. No copies el yaml.
Fly / Railway
Si usas Dockerfile, las caps las pone el runtime. Fly no es un cap_drop de Compose idéntico. En VPS Compose sí. No asumas que fly.toml dropea SYS_ADMIN.
Tabla

| Cap | ¿Agente webhook? | Notas |
|---|---|---|
| ALL drop | sí | default que quieres |
| NET_BIND_SERVICE | no | 443 lo hace Caddy |
| SYS_ADMIN | no | nunca |
| NET_RAW | no | no ping como tool |
| CHOWN | raro | chown en entrypoint como root, luego drop |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
privileged: true | tutorial viejo | quítalo |
| Bind 443 en Node | NET_BIND_SERVICE | proxy :443 → :3000 |
cap_add: SYS_ADMIN | “por si fuse” | no |
| Solo USER, caps intactas | olvido | cap_drop ALL |
| Fly distinto a Compose | esperabas el yaml | documenta el runtime |
Relación con el resto
Checklist
-
cap_drop: [ALL] -
no-new-privileges - Cero
privileged - Puerto 3000, no 443 en Node
- Ensayo:
capsh --print/grep Cap /proc/1/statusmás bajo que default - No SYS_ADMIN “temporal”
FAQ
¿seccomp default de Docker? Sí, otra capa. cap_drop sigue haciendo falta.
¿Kubernetes drop: [ALL]? Idéntico criterio.
¿iptables en el agente? No. El proxy/host lo hace.
Cómo verificar
grep Cap /proc/1/status (CapPrm / CapEff). Con ALL drop los bits bajan. Compáralo con un contenedor default. docker inspect --format '{{.HostConfig.CapDrop}}'.
AppArmor/SELinux es otra capa. cap_drop no los reemplaza. En un VPS Ubuntu el default de Docker ya carga un perfil; igual dropea caps.
Un cap_add: NET_ADMIN “para debug un rato” se queda en git. El PR que lo añade debe incluir la fecha de quitarlo. Mejor no.
PID 1 y caps
tini como PID 1 (init) no cambia CapEff del hijo si el yaml ya dropeó. El inspect mira el contenedor, no el binario.
Bind 443 sin cap: usa Caddy en otro servicio con las caps que Caddy necesite, no hinches el agente. Un compose de dos servicios es más claro que un Node privilegiado.
security_opt: label=disable no es cap_drop. No lo copies de foros.
El ensayo: arranca con ALL drop; el webhook responde. Si un tool “necesita root cap”, ese tool no va en este contenedor.
Un bot de Telegram no es un sidecar de red. Si el compose tiene cap_add copiado de un stack de VPN, bórralo.
docker inspect → CapAdd / CapDrop. Si CapDrop no incluye ALL, el yaml no se aplicó (servicio mal nombrado, override).
El trio USER + read_only + cap_drop es aburrido y suficiente. El cuarto (seccomp custom) espera a un incidente real.
Default de Docker: allowlist, no cero
Engine security (consultado hoy): Docker arranca con un conjunto restringido. Droppea todo salvo lo que “hace falta”: allowlist, no denylist. El consejo oficial es quitar todas excepto las que el proceso pide por escrito.
Eso no es cap_drop ALL. El default deja bits. La página de docker run lista las que se conservan y se pueden dropear:
| Cap (default ON) | Para qué | ¿Webhook Node? |
|---|---|---|
| AUDIT_WRITE | log de auditoría del kernel | no |
| CHOWN | chown arbitrario | no |
| DAC_OVERRIDE | saltar permisos de archivo | no |
| FOWNER | dueño vs mode | no |
| FSETID | bits setuid/setgid al modificar | no |
| KILL | señales sin chequeo | no |
| MKNOD | nodos de dispositivo | no |
| NET_BIND_SERVICE | bind < 1024 | no: Caddy/host |
| NET_RAW | sockets RAW/PACKET | no |
| SETFCAP | caps en archivos | no |
| SETGID / SETUID / SETPCAP | uid/gid/caps del proceso | no si ya eres node |
| SYS_CHROOT | chroot(2) | no |
Las que no vienen y hay que cap_add a propósito: NET_ADMIN, SYS_ADMIN, SYS_PTRACE, SYS_MODULE, SYS_BOOT, SYS_TIME, SYSLOG, WAKE_ALARM, PERFMON, NET_BROADCAST, AUDIT_CONTROL. Un ip link add dummy0 sin NET_ADMIN sale Operation not permitted; con --cap-add=NET_ADMIN sí. FUSE pide las dos: --cap-add SYS_ADMIN y --device /dev/fuse. Un agente de Telegram no monta sshfs.
--cap-add / --cap-drop aceptan ALL y el prefijo CAP_ es opcional: SYS_ADMIN = CAP_SYS_ADMIN. Ejemplo oficial: --cap-add=ALL --cap-drop=MKNOD. Tú quieres el inverso: drop ALL, cero add.
Compose documenta cap_drop como secuencia de strings (NET_ADMIN, SYS_ADMIN en el ejemplo). cap_add y cap_drop son secuencias: un duplicado se aplana. privileged: es otra llave, específica de plataforma. No es un atajo de cap_add: [ALL].
Qué hace --privileged de verdad
La referencia de docker container run (hoy): el flag no es “un poco más de root”. Habilita todas las caps del kernel, apaga el seccomp default, apaga el AppArmor default, apaga el label SELinux del proceso, da acceso a todos los devices del host, pone /sys y los mounts de cgroup en read-write. El contenedor puede casi lo que el host. El caso de uso documentado es Docker-in-Docker. El warning: shell root en el host. Para un bot, es un incidente.
El default de Docker ya dropea CAP_SYS_ADMIN. Por eso mount -t tmpfs none /mnt dentro de Ubuntu sin flags sale permission denied; con --privileged el mount funciona. No copies ese snippet al compose del agente.
--device da un device concreto sin privileged. En privileged, Docker ignora los permisos rwm de --device.
no-new-privileges no es cap_drop
CLI: --security-opt no-new-privileges o no-new-privileges=true. Impide ganar privilegios nuevos (su/sudo dejan de servir) y aplica filtros seccomp después de ese chequeo. Compose: security_opt acepta option=value u option:value; en booleanos como no-new-privileges el valor se puede omitir y cuenta como enabled. Las tres formas son equivalentes según la doc de Compose.
security_opt: label=disable apaga el confinement de labels. Es lo contrario de endurecer. No lo copies de un gist de Fedora.
El perfil seccomp default se ajusta a las caps que dejaste: no tienes que tocar el JSON si solo dropeaste. seccomp=unconfined aparece en el ejemplo oficial de strace junto a --cap-add SYS_PTRACE y --pid=container:…. Eso es debug, no prod.
User namespaces: Docker 1.10 los soporta en el daemon; no vienen enabled by default. Mapean root del contenedor a un uid ≠ 0 en el host. Es otra capa. Rootless corre daemon y contenedor sin root, salvo newuidmap/newgidmap. No sustituye cap_drop ALL en un daemon rootful.
Kubernetes: Restricted ya exige drop ALL
Security Context (hoy): allowPrivilegeEscalation controla el flag no_new_privs. Queda siempre true si el contenedor es privileged o tiene CAP_SYS_ADMIN. readOnlyRootFilesystem es el primo de read-only. El ejemplo oficial compara bitmaps: sin extra, CapPrm/CapEff = 00000000a80425fb; con add: [NET_ADMIN, SYS_TIME] = 00000000aa0435fb (bit 12 = CAP_NET_ADMIN, bit 25 = CAP_SYS_TIME). En el manifest omites el prefijo CAP_.
Pod Security Standards, perfil Restricted (Linux, v1.22+, no Windows): hay que drop una lista que incluye ALL. Lo único que se puede add de vuelta es NET_BIND_SERVICE. Baseline es más laxo: no puedes añadir caps fuera de la allowlist tipo Docker default (AUDIT_WRITE, CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, MKNOD, NET_BIND_SERVICE, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT). Fíjate: Baseline no lista NET_RAW como add permitido. Un agente en K8s Restricted es exactamente drop: [ALL] sin add.
Workers y Vercel Functions no tienen este mapa. No traduzcas el yaml.
Siguiente paso: si las caps ya son cero y igual hay write raro, read-only. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



