Docker AppArmor y seccomp: el endurecimiento mínimo que importa

¿Te fue útil?

No necesitas “hacer seguridad” como un teatro de cumplimiento. Necesitas contenedores que se comporten cuando se ven comprometidos—porque tarde o temprano uno lo estará. La cuestión no es si tu aplicación tiene errores. La cuestión es si un error se convierte en un ticket aburrido de incidente o en una situación de rehenes de fin de semana con una explotación del kernel y un atacante muy satisfecho.

AppArmor y seccomp son dos de los pocos controles de endurecimiento de contenedores que rutinariamente cambian el radio de daño en producción. No son llamativos y no impresionarán a tu equipo de compras. Sin embargo, detienen técnicas reales: llamadas al sistema extrañas, accesos al sistema de ficheros raros y esos momentos de “¿por qué este contenedor de API necesita montar sistemas de ficheros?”.

Qué significa realmente “endurecimiento mínimo”

En seguridad de contenedores, “endurecimiento mínimo” no es “activar todas las funciones y rezar”. Es un pequeño conjunto de controles que:

  • Se aplican ampliamente a aplicaciones heterogéneas.
  • Fallan ruidosamente (puedes detectar roturas) y fallan de forma segura (no eliminan las protecciones en silencio).
  • Reducen la superficie de ataque del kernel—porque el kernel es el recurso compartido que no puedes parchear reimplementando un contenedor.
  • Son operativamente diagnosticables bajo presión a las 02:00.

AppArmor y seccomp cumplen esos requisitos. No sustituyen a ejecutar como usuario no root, eliminar capacidades de Linux, sistemas de archivos de solo lectura y una gestión adecuada de secretos. Pero si me pidieras elegir dos controles que cambian el resultado tras un compromiso, elegiría estos.

El endurecimiento mínimo que importa para la mayoría de entornos Docker se parece a esto:

  • Usar el perfil seccomp predeterminado de Docker (no lo desactives).
  • Usar un perfil AppArmor en modo enforce (no ejecutar sin confinamiento).
  • Evitar –privileged y evitar seccomp=unconfined de forma general.
  • Cuando una app necesite excepciones genuinas, que sean explícitas, documentadas y probadas.

Hay una razón por la que estos dos controles aparecen en post-mortems: a menudo son la diferencia entre “el atacante consiguió una shell dentro de un contenedor” y “el atacante consiguió root en el host”.

Hechos e historia que importan (breve y concretos)

Los controles de seguridad tienen más sentido cuando sabes por qué existen. Algunos puntos de referencia:

  1. AppArmor se lanzó a mediados de los 2000 y se convirtió en predeterminado en Ubuntu mucho antes de que los contenedores fueran populares; se diseñó para confinar demonios mediante reglas basadas en rutas.
  2. SELinux vs AppArmor es en gran medida una diferencia de modelo de política y de herramientas; Docker se integró con ambos, pero AppArmor suele ser operativamente más simple en flotas centradas en Debian/Ubuntu.
  3. seccomp comenzó como “secure computing mode” alrededor de Linux 2.6.12, inicialmente muy restrictivo; seccomp-bpf lo hizo práctico al permitir filtros de syscalls basados en BPF.
  4. Docker habilitó seccomp por defecto hace años; muchos equipos lo desactivan durante fases de “simplemente que funcione” y olvidan volver a activarlo.
  5. El aislamiento de contenedores no es un límite de VM; los contenedores comparten el kernel del host. Por eso el filtrado de syscalls tiene un valor desproporcionado.
  6. Namespacing y cgroups no son políticas de seguridad; son mecanismos de aislamiento. AppArmor y seccomp son políticas que pueden decir “no”.
  7. La mayoría de las escapadas de contenedores están orientadas al kernel; si puedes reducir la superficie de syscalls y bloquear interfaces raras del kernel, mejoras tus probabilidades.
  8. AppArmor es “basado en rutas”, lo que es a la vez su fortaleza (legible) y una trampa (trucos con rename/mount si eres descuidado con montajes y mediación).
  9. Los filtros seccomp son por proceso; si un proceso se inicia sin el filtro (o lo pones como unconfined), no hay una segunda oportunidad luego.

Modelo de amenazas: qué detienen estos controles (y qué no)

Para qué es bueno AppArmor

AppArmor es Control de Acceso Obligatorio. Restringe lo que un proceso puede hacer incluso si ese proceso cree que es root. En el mundo de contenedores, eso es útil cuando una escapada empieza con “obtener root dentro del contenedor” (lo cual suele ser trivial) y continúa con “tocar recursos del host”. AppArmor puede bloquear lecturas/escrituras de archivos, montajes, ptrace, señales e interacciones con interfaces del kernel específicas.

También es un buen “tripwire de política”. Cuando ves una denegación, normalmente se correlaciona con un comportamiento que al menos deberías entender.

Para qué es bueno seccomp

seccomp filtra syscalls: puede permitir, denegar o atrapar llamadas al sistema. No se trata de detener “rm -rf”. Se trata de bloquear syscalls que son comunes en cadenas de explotación: creación de namespaces, keyrings, manipulación de paquetes raw, eventos perf, montaje de sistemas de ficheros, y similares. El perfil seccomp predeterminado de Docker bloquea una serie de syscalls de alto riesgo que las aplicaciones típicas no necesitan.

Lo que no hacen

  • No corrigen las vulnerabilidades de tu app. Las contienen.
  • No reemplazan el principio de menor privilegio. Si das al contenedor montajes del host o modo privilegiado, puedes sortear gran parte del valor.
  • No evitan la exfiltración de datos por canales permitidos (HTTP saliente, DNS, etc.). Eso es política de red y controles de egreso.
  • No detienen el abuso lógico como “exportar todos los datos de clientes” si tu API lo permite.

Idea parafraseada (atribuida): Werner Vogels es conocido por impulsar la idea de que “todo falla, todo el tiempo”, así que diseñas para contención y recuperación. Esa mentalidad se aplica aquí: asume compromiso y diseña el radio de daño.

AppArmor en Docker: perfiles, modos y valores sensatos

Cómo usa Docker AppArmor

En hosts con AppArmor habilitado, Docker puede aplicar un perfil AppArmor a cada contenedor. Si no haces nada, a menudo obtienes un perfil por defecto como docker-default (dependiendo de la distro y del empaquetado de Docker). Si AppArmor está deshabilitado, o Docker no puede cargar perfiles, los contenedores pueden ejecutarse unconfined.

“Unconfined” no significa “está bien”. Significa que has eliminado uno de los pocos frenos que funcionan incluso después de que el proceso tenga root.

Enforce vs complain

AppArmor tiene dos modos operativos relevantes:

  • enforce: negar las violaciones. Esto es lo que quieres en producción.
  • complain: permitir pero registrar. Así se recopilan datos para escribir un perfil sin romper cargas de trabajo.

Si intentas implementar AppArmor, empieza con complain en un entorno de staging que reciba tráfico real. Luego promueve a enforce con una ventana de cambios y un plan de reversión.

Cómo es un perfil “mínimamente útil”

Para Docker, lo mínimo útil suele significar: mantener el predeterminado de Docker y solo aflojar donde tengas evidencia sólida. Los perfiles personalizados son una capacidad; también son una obligación de mantenimiento.

Si debes escribir el tuyo, manténlo estricto en montajes, dispositivos raw, ptrace y rutas de archivos peligrosas. Sé explícito sobre directorios escribibles. Los contenedores no deberían poder escribir en partes arbitrarias del sistema de ficheros; si pueden, eventualmente lo aprenderás por las malas.

Signos operativos de que AppArmor no te está protegiendo

  • docker info muestra AppArmor deshabilitado.
  • Los contenedores muestran AppArmorProfile: "" o unconfined.
  • No aparece ninguna denegación en ningún lado, incluso durante pruebas claramente malas (esto es sospechoso, no reconfortante).

Broma #1: AppArmor en modo complain es como un guardia de seguridad que solo escribe entradas de diario muy contundentes. Genial para investigación, no para detener a nadie.

seccomp en Docker: filtrado de syscalls sin hacerse daño

El perfil seccomp predeterminado de Docker

Docker incluye un perfil seccomp predeterminado que bloquea syscalls históricamente riesgosos o raramente necesarios para cargas de trabajo típicas de contenedores. No es perfecto ni mínimo; es práctico. El perfil predeterminado suele romper solo cargas especializadas (algunos trazadores, ciertos runtimes inusuales, herramientas de ajuste de bases de datos, trucos con contenedores anidados) y aun así normalmente puedes resolverlo sin desactivar totalmente la restricción.

Cómo aparecen los fallos de seccomp

Cuando seccomp bloquea algo, normalmente ves una de estas situaciones:

  • El proceso recibe EPERM o EACCES y registra un error tipo “operación no permitida”.
  • El proceso es terminado con SIGSYS (“bad system call”). Esto suele ocurrir cuando el filtro está configurado para matar.
  • Los logs de auditoría muestran eventos de seccomp si la auditoría está configurada.

El truco de depuración es correlacionar errores de la app con logs del kernel/auditoría y la configuración del contenedor. Si empiezas deshabilitando seccomp al azar, “arreglarás” el síntoma creando una regresión de seguridad que nadie recordará dentro de seis semanas.

Cuándo deberías personalizar seccomp

Personaliza seccomp solo cuando tengas una carga de trabajo estable y una razón que puedas articular en una frase: “Este contenedor requiere la syscall X porque la función Y la usa”. Si no puedes explicarlo, estás adivinando. Adivinar es cómo terminas con unconfined por todas partes.

Broma #2: Ejecutar contenedores con seccomp=unconfined es como quitarse el cinturón de seguridad porque arruga la camisa.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las tareas básicas que ejecutas en hosts reales. Cada una incluye: un comando, salida representativa, qué significa y qué decisión tomas a continuación.

Task 1 — Check if Docker thinks AppArmor and seccomp are enabled

cr0x@server:~$ docker info --format 'apparmor={{.SecurityOptions}}'
apparmor=[name=apparmor] seccomp=[name=seccomp,profile=default] cgroupns

Qué significa: Docker ve AppArmor y seccomp, y seccomp está usando el perfil por defecto.

Decisión: Buena línea base. Si AppArmor o seccomp faltan aquí, detente y corrige la configuración del host antes de “endurecer contenedores”.

Task 2 — Confirm AppArmor kernel support and status

cr0x@server:~$ sudo aa-status
apparmor module is loaded.
55 profiles are loaded.
52 profiles are in enforce mode.
3 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

Qué significa: El módulo del kernel está cargado y la mayoría de perfiles están en enforce.

Decisión: Si el módulo no está cargado o los perfiles no están en enforce, corrígelo primero. “Flags de Docker” no ayudarán si AppArmor no está activo.

Task 3 — Check if any containers are unconfined (AppArmor)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} AppArmor={{.AppArmorProfile}}'
/api AppArmor=docker-default
/worker AppArmor=docker-default
/metrics AppArmor=unconfined

Qué significa: Un contenedor se está ejecutando sin confinamiento AppArmor.

Decisión: Trátalo como un defecto. Averigua quién puso --security-opt apparmor=unconfined (o por qué Docker no pudo aplicar un perfil) y remedia.

Task 4 — Check seccomp mode per container

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Seccomp={{.HostConfig.SecurityOpt}}'
/api Seccomp=[]
/worker Seccomp=[]
/metrics Seccomp=[seccomp=unconfined]

Qué significa: Dos contenedores usan los valores por defecto; uno tiene seccomp explícitamente unconfined.

Decisión: Elimina la excepción a menos que la puedas justificar. Si debes mantenerla, reemplázala por un perfil seccomp personalizado que añada solo las syscalls necesarias.

Task 5 — Find how the container was started (to track the change source)

cr0x@server:~$ docker inspect metrics --format 'Image={{.Config.Image}} SecurityOpt={{.HostConfig.SecurityOpt}} Privileged={{.HostConfig.Privileged}}'
Image=corp/metrics-exporter:2.7.1 SecurityOpt=[seccomp=unconfined apparmor=unconfined] Privileged=false

Qué significa: Alguien deshabilitó explícitamente ambos mecanismos.

Decisión: Trátalo como deshabilitar la verificación TLS. Crea un issue contra el manifiesto de despliegue y requiere justificación más propietario.

Task 6 — Validate container capabilities weren’t “helpfully” broadened

cr0x@server:~$ docker inspect api --format 'CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}}'
CapAdd=[] CapDrop=[ALL]

Qué significa: Este contenedor elimina todas las capacidades (bueno). Aun así puede mantener algunos valores por defecto según cómo se ejecute, pero es una postura explícita.

Decisión: Cuando reduces capacidades, AppArmor/seccomp son aún más efectivos. Mantén este patrón.

Task 7 — Detect privileged containers (because they punch holes in everything)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Privileged={{.HostConfig.Privileged}}'
/api Privileged=false
/worker Privileged=false
/buildkit Privileged=true

Qué significa: Un contenedor es privilegiado. Puede ser intencional (por ejemplo, herramientas de build) pero es de alto riesgo.

Decisión: Si es un sistema de build, aíslalo: nodos dedicados, sin secretos de producción, monitorización agresiva y, idealmente, usar rootless o un enfoque de build más seguro.

Task 8 — Check kernel logs for AppArmor denials

cr0x@server:~$ sudo dmesg -T | grep -i apparmor | tail -n 5
[Sat Jan  3 10:11:14 2026] audit: type=1400 audit(1704276674.123:781): apparmor="DENIED" operation="open" profile="docker-default" name="/proc/kcore" pid=29144 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

Qué significa: Un proceso dentro de un contenedor confinado por docker-default intentó leer /proc/kcore y fue denegado.

Decisión: Esto suele ser buena noticia: indica que la confinación está activa. Investiga si el acceso era esperado (raro) o sospechoso (a menudo).

Task 9 — Check audit logs for seccomp violations

cr0x@server:~$ sudo ausearch -m SECCOMP -ts recent | tail -n 3
time->Sat Jan  3 10:12:01 2026
type=SECCOMP msg=audit(1704276721.443:812): auid=4294967295 uid=0 gid=0 ses=4294967295 subj==unconfined pid=29210 comm="node" exe="/usr/local/bin/node" sig=31 arch=c000003e syscall=319 compat=0 ip=0x7f2c8e2f1a9d code=0x0

Qué significa: Un proceso disparó un evento seccomp en la syscall 319 (ejemplo). Mapearás syscalls a nombres si es necesario, pero el evento es la pista.

Decisión: Correlaciona con el contenedor y el despliegue. Si es tu app fallando, necesitas un ajuste de seccomp dirigido, no una desactivación global.

Task 10 — Check what seccomp profile Docker is using by default (daemon level)

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "journald",
  "live-restore": true
}

Qué significa: No hay override; Docker usará su seccomp predeterminado a menos que un contenedor solicite otra cosa.

Decisión: Bien. Si ves "seccomp-profile": "unconfined" o patrones similares (según las herramientas), trátalo como una reversión de postura de seguridad y actúa en consecuencia.

Task 11 — Run a canary container to verify enforcement isn’t silently broken

cr0x@server:~$ docker run --rm --name aa-canary alpine:3.19 sh -c 'cat /proc/kcore; echo ok'
cat: can't open '/proc/kcore': Permission denied
ok

Qué significa: La denegación es esperada bajo los valores típicos de Docker/AppArmor; el contenedor continúa ejecutándose.

Decisión: Mantén esto como una prueba rápida durante el arranque de nodos o después de actualizaciones del kernel. Si de repente tiene éxito, probablemente perdiste la confinación AppArmor.

Task 12 — Prove to yourself that seccomp is active with a syscall that’s usually blocked

cr0x@server:~$ docker run --rm alpine:3.19 sh -c 'apk add --no-cache strace >/dev/null; strace -e trace=bpf true'
strace: syscall bpf denied

Qué significa: La syscall bpf suele estar bloqueada por el perfil seccomp por defecto; strace reporta denegación.

Decisión: Si esto tiene éxito inesperadamente, alguien deshabilitó seccomp o cambió el perfil. Si tu app legítima necesita bpf, estás en un territorio nicho—documenta y aísla.

Task 13 — Identify the AppArmor profile applied to a running container process

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' api); sudo cat /proc/$pid/attr/current
docker-default (enforce)

Qué significa: El kernel muestra que el proceso está confinado bajo docker-default en modo enforce.

Decisión: Este es un paso de verificación fuerte cuando los metadatos de Docker son engañosos o sospechas diferencias en tiempo de ejecución.

Task 14 — Detect containers with risky mounts that undermine policy

cr0x@server:~$ docker inspect api --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/api/config","Destination":"/app/config","Mode":"ro","RW":false,"Propagation":"rprivate"}]

Qué significa: Se usa un bind mount de solo lectura para la configuración. Eso es sensato.

Decisión: Si ves binds a /, /var/run/docker.sock, /proc o rutas host escribibles con alcance amplio, corrígelas primero; ningún ajuste de perfil las hará seguras.

Task 15 — Spot the “docker.sock inside container” foot-gun

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} {{range .Mounts}}{{.Destination}} {{end}}' | grep -F '/var/run/docker.sock'
/ci-runner /var/run/docker.sock

Qué significa: Un contenedor tiene acceso al socket de Docker, que es efectivamente root en el host en la mayoría de configuraciones.

Decisión: Trátalo como una carga de trabajo privilegiada. Aísla, restringe quién puede desplegarla y evita mezclarla con rutas de datos de producción.

Guion de diagnóstico rápido

Cuando algo se rompe tras habilitar AppArmor/seccomp, tu objetivo es encontrar el cuello de botella en minutos, no en horas. Este es el orden que ahorra tiempo.

Primero: confirma que realmente es AppArmor/seccomp (no red, no DNS, no permisos)

  • Busca “Operation not permitted” o “Bad system call” en logs del contenedor.
  • Revisa los security opts y perfiles del contenedor (docker inspect).
  • Revisa logs del kernel/auditoría por denegaciones (dmesg, ausearch).

Segundo: identifica qué acción está bloqueada

  • Las denegaciones de AppArmor suelen mencionar una operación y una ruta (p. ej., operation="open" name="/proc/kcore").
  • Los eventos de seccomp mencionan un número de syscall (y a veces el nombre según herramientas) y a menudo SIGSYS.

Tercero: decide la categoría de corrección adecuada

  • Arreglar la app si está haciendo algo innecesario (común).
  • Arreglar la configuración del contenedor (montajes, capacidades, usuario) si el comportamiento de la app es normal pero el contenedor está sobreajustado en el lugar equivocado.
  • Arreglar el perfil solo si es una necesidad estable y justificada.
  • Aislar la carga de trabajo si necesita poderes peligrosos (sistemas de build, herramientas eBPF, runtimes anidados).

Cuarto: prueba con la excepción más pequeña posible

Nunca saltes del predeterminado a unconfined. Para seccomp, permite una syscall. Para AppArmor, permite una ruta o capacidad. Luego redeploya y verifica que la denegación desaparezca y que nada más retroceda.

Tres mini-historias corporativas desde el frente

1) Incidente causado por una suposición equivocada: “Está dentro de un contenedor, así que no puede tocar el host”

La empresa ejecutaba un conjunto de servicios “solo internos” en Docker en algunos hosts Linux potentes. Uno de esos servicios era una API de conversión de PDF. Ya puedes imaginar el resto: documentos suministrados por usuarios, un parser con historial de fallos y un modelo de amenazas descrito como “no expuesto a internet”. En realidad, era alcanzable desde varios lugares porque las redes internas son solo internet con mejor café.

El equipo asumió que la contenedorización por sí sola era suficiente. AppArmor estaba instalado en la distro pero no se aplicaba realmente a los contenedores; seccomp se había deshabilitado meses antes porque otro servicio lo había provocado durante una migración urgente. Nadie apuntó el motivo. La desactivación se convirtió en la configuración por defecto.

Un atacante usó un bug del parser para obtener ejecución de código dentro del contenedor. Desde allí enumeraron montajes y encontraron un bind mount escribible hacia un directorio del host usado para cargas compartidas. Ese directorio contenía una llave SSH dejada por un antiguo script de automatización. La llave pertenecía a una cuenta que, aunque no era root, estaba en el grupo que podía hablar con el socket de Docker en algunas máquinas.

De “RCE en un contenedor” pasaron a “controlar el daemon de Docker”, y desde ahí fue el fin del juego: arrancar un contenedor privilegiado, montar el filesystem del host, dejar un backdoor. El análisis post-mortem volvía siempre a una línea: “Asumimos que los contenedores eran un límite”.

Después, la corrección no fue heroica. Fue aburrida: volver a habilitar el seccomp por defecto, aplicar AppArmor en enforce, eliminar montajes de docker.sock y empujar una política que requiriera propietario y fecha de expiración para cualquier excepción. El siguiente intento de explotación aún consiguió una shell en un contenedor, pero no pudo dar el segundo salto. Esa es la victoria.

2) Optimización que se volvió en su contra: “Nuestro perfil seccomp personalizado es más rápido”

Un grupo enfocado en rendimiento decidió que el perfil seccomp predeterminado de Docker era “demasiado grande” y por ende “debe ser lento”. Escribieron un perfil personalizado para permitir solo las syscalls que sus servicios Go usaban. Las pruebas iniciales se vieron bien: los benchmarks de latencia mejoraron ligeramente en cargas sintéticas, y nadie vio errores en staging.

Luego llegó producción. El primer síntoma no fue de seguridad; fue de disponibilidad. Un subconjunto de contenedores empezó a caer durante picos de carga, pero solo en nodos con kernel más nuevo. La firma del fallo fue una mezcla de “bad system call” y errores de runtime crípticos. El ingeniero on-call hizo lo que hacen bajo estrés: deshabilitó seccomp para los servicios que caían para restaurar la disponibilidad. Los incidentes cesaron. El “arreglo” quedó.

La causa raíz fue aburrida: el runtime de Go y libc hicieron syscalls que no aparecían en su prueba estrecha, incluidas llamadas usadas para gestión de threads e interfaces de kernel más nuevas. Su perfil había sido “optimizado” contra una foto puntual de comportamiento, no contra el comportamiento real a través de versiones de kernel, cambios en libc y modos operativos como variaciones en DNS y TLS.

La lección a largo plazo no fue “nunca personalices”. Fue “no personalices a menos que puedas mantenerlo”. Revirtieron al perfil seccomp por defecto de Docker para cargas generales y mantuvieron un perfil personalizado cuidadosamente gestionado para un servicio especializado, con dueño y pruebas en CI contra múltiples kernels. La ligera ganancia de benchmark no valió la fragilidad operativa.

3) Práctica aburrida pero correcta que salvó el día: pruebas canary y defaults aplicados

Otra organización operaba docenas de clusters Kubernetes pero también tenía una pequeña flota de hosts solo Docker para jobs batch heredados. Esos hosts se actualizaban con frecuencia—parches del kernel, actualizaciones de Docker, la habitual rotación. El equipo SRE tenía un hábito simple: cada bootstrap de nodo ejecutaba un contenedor canary que intentaba un puñado de acciones “que deberían ser denegadas” y confirmaba que las denegaciones esperadas aparecieran en logs.

Un semana, tras una actualización rutinaria del SO, el canary empezó a pasar acciones que deberían haber sido bloqueadas. Aún no habían saltado alarmas porque los jobs de producción seguían corriendo. Pero la comprobación canary falló el paso de admisión del nodo, así que no se programaron nuevas cargas allí. El host quedó automáticamente en cuarentena.

Resultó que AppArmor se había deshabilitado al arrancar por un desajuste en parámetros del kernel durante la actualización. Docker seguía ejecutándose, los contenedores seguían corriendo y todos habrían asumido “está bien”. Pero “estar bien” era verdad solo hasta el primer compromiso. El canary detectó una regresión silenciosa que habría durado meses.

El equipo corrigió la configuración de arranque, volvió a habilitar AppArmor y levantó la cuarentena. Sin informe de incidente, sin impacto al cliente. Solo una de esas victorias invisibles que existen porque alguien insistió en un bucle de control aburrido y se negó a aceptar “probablemente funciona”.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: el contenedor sale inmediatamente con “Bad system call”

Causa raíz: seccomp bloqueó una syscall y la acción está configurada para matar o el proceso no maneja EPERM de forma elegante.

Solución: Confirma que seccomp está activo e inspecciona los logs de auditoría (ausearch -m SECCOMP). Añade la syscall específica a un perfil seccomp personalizado para esa carga, o actualiza la app/runtime. No pongas seccomp=unconfined como arreglo “temporal” a menos que también planifiques su eliminación.

2) Síntoma: la app no puede leer un archivo que antes leía (pero los permisos Unix parecen correctos)

Causa raíz: AppArmor negó un acceso basado en ruta aunque los permisos Unix lo permitan.

Solución: Revisa dmesg por denegaciones de AppArmor, identifica el perfil, y luego ajusta el perfil o mueve el archivo a una ruta permitida (a menudo mejor). Verifica montajes: una ruta diferente dentro del contenedor puede causar un desajuste de política.

3) Síntoma: “mount: permission denied” dentro del contenedor

Causa raíz: AppArmor y/o seccomp bloquearon operaciones relacionadas con mount; también es probable que falten capacidades como CAP_SYS_ADMIN.

Solución: Pregunta por qué el contenedor necesita montar algo. La mayoría no debería. Para casos legítimos (raros), aísla la carga y concede explícitamente lo que necesita con un alcance estricto.

4) Síntoma: las herramientas de depuración no funcionan (strace, perf, ptrace)

Causa raíz: Este es a menudo el punto. AppArmor y seccomp restringen interfaces de trazado porque son poderosas en explotaciones.

Solución: Usa imágenes de depuración dedicadas en entornos aislados, o ejecuta herramientas de depuración en el host con el acceso apropiado. Si debes habilitar trazado en producción, hazlo para un contenedor/pod de corta vida con acceso auditado y un plan claro de teardown.

5) Síntoma: “funciona en un nodo, falla en otro” tras endurecer

Causa raíz: Diferencias en versiones del kernel, versiones de Docker o conjuntos de perfiles AppArmor. Los perfiles seccomp personalizados son especialmente sensibles a la variabilidad en tiempo de ejecución.

Solución: Estandariza imágenes de nodo, fija y prueba perfiles, y añade comprobaciones canary. Trata la deriva del host como un problema de seguridad y fiabilidad, no solo de higiene.

6) Síntoma: todo corre, pero no ves ni una sola denegación en ninguna parte

Causa raíz: La auditoría/logging no está habilitada o AppArmor/seccomp no se aplican realmente (contenedores unconfined).

Solución: Verifica vía /proc/<pid>/attr/current, docker info y pruebas canary. El silencio no es prueba de seguridad.

7) Síntoma: alguien “arregla” un incidente añadiendo –privileged

Causa raíz: Falta de disciplina operativa y ausencia de un camino de escalado para ajuste de perfil. El modo privilegiado se convierte en la puerta de escape.

Solución: Hacer que los despliegues privilegiados requieran aprobación explícita, aislar cargas privilegiadas y construir una vía rápida para cambios de perfil dirigidos con pruebas.

Listas de verificación / plan paso a paso

Paso a paso: implementación mínima que no arruinará tu semana

  1. Inventaria la postura actual. Identifica contenedores con apparmor=unconfined, seccomp=unconfined, modo privilegiado y montajes de docker.sock.
  2. Arregla lo peor primero. Quita montajes de docker.sock de cargas que no son de build. Elimina contenedores privilegiados en planos de datos de producción.
  3. Habilita controles en el host. Asegura que AppArmor esté cargado y en enforce. Asegura que Docker reporte seccomp por defecto habilitado.
  4. Prueba canary en cada nodo. Usa chequeos rápidos de denegación (como /proc/kcore y una syscall bloqueada) durante el bootstrap y después de actualizaciones.
  5. Despliega los defaults de seccomp ampliamente. El seccomp predeterminado debe estar activado en todas partes salvo excepciones documentadas.
  6. Despliega AppArmor en enforce. Asegura que los contenedores no estén unconfined. Prefiere docker-default a menos que tengas una razón fuerte.
  7. Maneja excepciones con propiedad. Todo unconfined o perfil personalizado necesita un propietario, una razón y una revisión periódica.
  8. Automatiza la verificación. Chequeos en CI para manifiestos de despliegue: fallar si establecen unconfined sin una lista de permitidos.
  9. Operationaliza la depuración. Enseña al on-call dónde encontrar denegaciones y eventos seccomp; escribe un runbook que empiece por logs, no por conjeturas.

Checklist: opciones de lanzamiento de contenedores que importan

  • No uses --privileged a menos que el nodo esté dedicado y la carga sea explícitamente de alta confianza.
  • No uses --security-opt seccomp=unconfined salvo como mitigación temporal con ticket y expiración.
  • No uses --security-opt apparmor=unconfined en producción. Arregla el perfil en su lugar.
  • Prefiere --read-only con montajes escribibles explícitos para estado.
  • Elimina capacidades por defecto; añade solo las que puedas explicar.
  • Evita los namespaces de PID/red del host a menos que estés construyendo agentes de nodo y comprendas las implicaciones.

Checklist: qué capturar en una solicitud de excepción

  • Imagen del contenedor y versión exactas.
  • Logs exactos de denegación (AppArmor) o números/eventos de syscall (seccomp).
  • Justificación de negocio para el comportamiento.
  • La excepción más pequeña propuesta (una ruta, una syscall, una capacidad).
  • Plan de aislamiento (nodos dedicados, menos secretos, red restringida).
  • Plan de pruebas y plan de reversión.

Preguntas frecuentes

1) ¿Necesito AppArmor y seccomp? ¿No basta con uno?

Usa ambos. Cubren capas diferentes: AppArmor trata sobre control de acceso (archivos, capacidades, operaciones), seccomp sobre la superficie de syscalls. La superposición está bien; la redundancia es el punto.

2) Si mi contenedor corre como non-root, ¿sigo necesitando esto?

Sí. Non-root ayuda mucho, pero la explotación del kernel y las escapadas de contenedores no requieren que tu proceso inicie como root. La defensa en profundidad importa porque los atacantes encadenan debilidades.

3) ¿Por qué no ejecutar todo en modo privilegiado y confiar en controles de red?

Porque los controles de red no detienen la escalada local de privilegios, errores del kernel ni daños accidentales al host. Los contenedores privilegiados son básicamente “procesos con departamento de marketing”.

4) ¿Cuál es la postura seccomp más segura por defecto en Docker?

Usa el perfil seccomp predeterminado de Docker y devía solo para cargas con necesidades documentadas. El predeterminado es un balance pragmático entre muchas cargas.

5) ¿Cómo digo si un contenedor está realmente confinado (no solo “configurado”)?

Revisa el atributo del proceso: /proc/<pid>/attr/current para AppArmor. Para seccomp, busca eventos de auditoría SECCOMP durante una acción conocida bloqueada, o inspecciona la config del runtime y prueba con un canary.

6) Mi app necesita ptrace/strace en producción. ¿Qué hago ahora?

Primera pregunta: ¿realmente lo necesita? Normalmente necesitas mejor observabilidad, no ptrace. Si realmente lo necesitas, aísla la carga, delimítala fuertemente y acepta el riesgo aumentado. No amplíes la seguridad de la flota a la ligera.

7) ¿Habilitar estos controles afectará el rendimiento?

En la mayoría de cargas reales, la sobrecarga es insignificante frente a red, disco y lógica de la aplicación. El coste operativo viene de excepciones y depuración, no de ciclos de CPU.

8) ¿Por qué aparecen denegaciones solo después de una actualización de librería?

Porque los runtimes evolucionan. Libc más nueva, JVMs, versiones de Go o Node pueden usar syscalls distintas o acceder a interfaces de kernel diferentes. Por eso los perfiles seccomp “mínimos” requieren mantenimiento y pruebas entre kernels.

9) ¿AppArmor basta si estoy en Ubuntu, y SELinux si estoy en RHEL?

Usa lo que la plataforma soporte bien. AppArmor es común en Ubuntu/Debian; SELinux en sistemas derivados de RHEL. Lo clave es la aplicación y madurez operativa, no la ideología.

10) ¿Cuál es la ganancia más rápida si solo puedo arreglar una cosa esta semana?

Deja de ejecutar contenedores sin confinamiento y deja de usar modo privilegiado. Aplica el seccomp predeterminado en todas partes. Esos cambios cierran muchas puertas de escape “fáciles”.

Siguientes pasos que realmente reducen el riesgo

Si quieres el endurecimiento mínimo que importa, no empieces escribiendo una política de 500 líneas. Empieza por asegurarte de que los valores por defecto sean reales y estén aplicados:

  • Verifica que AppArmor esté cargado y en enforce en cada nodo.
  • Verifica que Docker use el seccomp por defecto y que los contenedores no estén unconfined.
  • Elimina contenedores privilegiados y montajes de docker.sock del plano de datos de producción.
  • Añade una prueba canary simple al bootstrap del nodo que demuestre que ocurren denegaciones.
  • Operationaliza el diagnóstico: enseña al on-call a leer denegaciones y eventos seccomp antes de recurrir a unconfined.

Haz esas cinco cosas y convertirás “seguridad de contenedores” de una diapositiva en un bucle de control. Ese es el tipo de aburrimiento que mantiene los sistemas—y los fines de semana—íntactos.

← Anterior
Bloques pequeños especiales de ZFS: Cómo acelerar cargas de trabajo con archivos pequeños
Siguiente →
Planificación de capacidad ZFS: diseñar para el crecimiento sin reconstruir

Deja un comentario