Recientemente me encontré con un análisis técnico excepcionalmente denso sobre seguridad de contenedores que merece la pena ser compartido. El autor empezó con una hipótesis simple: el aislamiento de filesystems de los contenedores debería ser suficiente para cargas de trabajo multi-tenant sin necesidad de máquinas virtuales, si se entiende suficientemente bien qué está pasando a nivel de syscall.
Después de una investigación exhaustiva, la conclusión es más incómoda de lo que esperaba: los valores por defecto te protegen bien, pero en el momento que empiezas a usar características “avanzadas” como la propagación de montajes bidireccional o el reetiquetado de SELinux, estás a un paso de entregarle las llaves de tu host a un atacante.
Por qué importa el aislamiento de filesystems
La economía de la multitenencia
Ejecutar múltiples tenants en infraestructura compartida solo funciona si puedes garantizar que el compromiso del Tenant A no pueda afectar al Tenant B. Los namespaces de Linux proporcionan la base; los namespaces de montaje dan a cada contenedor su propia vista del árbol de filesystems. Sin este aislamiento, la economía cloud que hace atractivos a los contenedores se desmorona.
Superficie de ataque
Los contenedores restringen lo que un proceso en ejecución puede ver y hacer aprovechando las características de namespaces del kernel. Pero “restringir” no significa “eliminar”. La superficie de ataque del kernel compartido significa que una vulnerabilidad en OverlayFS (como CVE-2023-0386) puede escalar privilegios desde dentro de un contenedor que satisfaga los precondiciones del exploit. El kernel es el recurso compartido último.
Las seis columnas del aislamiento de filesystems
1. Namespaces de montaje y OverlayFS
Los namespaces de montaje (CLONE_NEWNS) dan a cada contenedor una vista privada del árbol de montaje. OverlayFS superpone un upperdir escribible sobre capas lowerdir de solo lectura, implementando semántica de copia-en-escritura.
El mecanismo de copia-en-escritura funciona así: cuando un contenedor modifica un archivo de una capa inferior, el kernel lo copia primero al upperdir. Todas las operaciones subsiguientes apuntan a esta copia, dejando la imagen base intacta.
El problema del kernel compartido: OverlayFS se ejecuta en espacio del kernel. CVE-2023-0386 demostró un fallo en cómo el kernel maneja la validación de mapeo UID/GID cuando copia un archivo con capacidades de un montaje nosuid a otro durante el copia-up de OverlayFS. Esto habilita escalación de privilegios local.
2. FUSE (Filesystem in Userspace)
FUSE te permite implementar un filesystem como un demonio en espacio de usuario en lugar de código del kernel. Cuando una aplicación hace un syscall en un montaje FUSE, la petición viaja: VFS → módulo del kernel FUSE → cola /dev/fuse → demonio en espacio de usuario → respuesta de vuelta por el mismo camino.
Por qué FUSE importa para contenedores: El montaje del kernel overlayfs requiere CAP_SYS_ADMIN en el namespace relevante. Cuando ese soporte no está disponible, los runtimes como Podman caen en fuse-overlayfs, que implementa semántica de overlay en espacio de usuario sin requerir privilegios elevados.
El demonio lo controla todo: Un demonio FUSE malicioso puede mentir sobre la propiedad, permisos y contenido de los archivos. Si montas filesystems FUSE no confiables, el control de acceso es teatro.
3. Propagación de montajes
Aquí es donde la mayoría nos quemamos. La propagación de montajes determina si los eventos de montaje cruzan fronteras de namespaces.
Los cuatro tipos de propagación son:
- Shared: Los eventos de montaje se propagan bidireccionalmente
- Slave: Los eventos se propagan solo una dirección
- Private: Los eventos no se propagan
- Unbindable: El montaje no puede ser enlazado
La protección del kernel para namespaces menos privilegiados: Cuando creas un namespace de montaje que es menos privilegiado que su padre; cuando el propietario del namespace de usuario del nuevo namespace difiere del del namespace de montaje padre, el kernel automáticamente demota los montajes compartidos heredados a MS_SLAVE.
Esto significa que crear un namespace de montaje con un namespace de usuario propietario diferente dispara la demotación automática. Crear uno como root (sin --user) no demota - el montaje se mantiene compartido.
4. Cumplimiento MAC (AppArmor/SELinux)
El Control de Acceso Mandatorio (MAC) proporciona una segunda capa de defensa. Incluso si un proceso gana capacidades, las políticas MAC pueden denegar operaciones específicas.
El perfil AppArmor por defecto de Docker (docker-default) deniega operaciones de montaje excepto para tipos específicos permitidos.
Rectoriquetado SELinux (:z y :Z): Esto es donde encontré comportamiento sorprendente. Cuando ejecutas:
podman run -v /host/path:/container/path:Z myimage
La opción :Z instruye al runtime del contenedor a reetiquetar /host/path con una etiqueta MCS que coincida con el contenedor. Esto es un reetiquetado recursivo de objetos de archivo bajo la ruta de montaje - el runtime recorre el árbol del filesystem del host y cambia la etiqueta en cada archivo vía libselinux.
Esto sucede antes de que el contenedor arranque, en el host. No está sandboxeado. Si reetiquetas un directorio compartido por otros servicios del host (como /var/log), rompes esos servicios. Esto es una operación del lado del host maquillada como opción de configuración de contenedor.
Advertencia crítica: Nunca usar :Z en:
- Directorios de estado del runtime del contenedor (
/var/lib/docker,/var/lib/containers) - Directorios del sistema compartidos (
/var/log,/tmp,/var/run) - Directorios montados por múltiples contenedores
- Filesystems de red (NFS, CIFS) donde el reetiquetado puede fallar o corromper estado remoto
5. Seccomp (El portero de syscalls)
Aquí hay algo que inicialmente subestimé: para la mayoría de despliegues de contenedores, seccomp es la primera línea de defensa contra ataques basados en montaje, no AppArmor o SELinux. Los perfiles seccomp por defecto de Docker y Podman bloquean el syscall mount completamente para contenedores no privilegiados.
Cómo funciona el filtrado seccomp: Cuando un contenedor arranca, el runtime instala un programa BPF (Berkeley Packet Filter) que intercepta cada syscall. El filtro examina el número de syscall y, opcionalmente, sus argumentos, luego decide: permitir, denegar (EPERM), matar el proceso, o atrapar a espacio de usuario.
El perfil seccomp por defecto de Docker bloquea aproximadamente 44 syscalls por defecto, incluyendo mount, kexec, swapoff, pivot_root, entre otros.
La implicación práctica: Incluso si de alguna forma concedes CAP_SYS_ADMIN a un contenedor (mala idea), el perfil seccomp por defecto todavía bloquea mount(). Necesitarías ambas la capacidad y un perfil seccomp permisivo (o --security-opt seccomp=unconfined) para que los ataques de montaje funcionen.
6. Cgroups v2 y aislamiento de recursos I/O
Ahora, este es mi otro miedo cuando se trata de multitenencia; las operaciones de filesystem consumen recursos compartidos. Un contenedor realizando I/O ilimitado puede inanitar otros contenedores y servicios del host, incluso con aislamiento de namespace perfecto. Esto no es aislamiento de integridad de filesystem, es aislamiento de disponibilidad de filesystem.
El problema: Por defecto, la mayoría de despliegues de contenedores no configuran límites I/O. Los contenedores comparten la capacidad I/O del host sin restricciones. Un contenedor malicioso o mal comportado puede saturar el I/O de disco, causando degradación de rendimiento o caídas para otros contenedores y servicios del host.
Por qué esto importa para la multitenencia: Incluso si tus contenedores están perfectamente aislados a nivel de namespace y MAC, un solo contenedor puede degradar el rendimiento para todos en el nodo. En clústeres compartidos, este es un vector de denegación-de-servicio que no requiere ninguna escalación de privilegios.
Cadenas de ataque completas
Cadena de ataque 1: Montaje del socket Docker → Compromiso completo del host
Escenario: Un contenedor tiene /var/run/docker.sock montado (común para pipelines CI/CD, herramientas de monitoreo y patrones “Docker-in-Docker”).
Precondiciones:
- El contenedor tiene acceso al socket Docker (montaje bind)
- El demonio Docker se ejecuta como root en el host
- Sin restricciones adicionales (este es el defecto cuando montas el socket)
Explotación paso a paso:
- El atacante descubre acceso al socket Docker
- Crea un contenedor privilegiado con el filesystem del host montado
- Lee archivos sensibles del host (
/etc/shadow, claves SSH, secretos de aplicación) - Hace chroot en el filesystem del host y consigue una shell de root
Desde aquí podrían:
- Añadir claves SSH a
/root/.ssh/authorized_keys - Crear nuevos usuarios root en
/etc/passwd - Instalar backdoors o cryptominers
- Pivotar a otros sistemas en la red
Lección: Nunca montar el socket Docker en contenedores no confiables. Si debes, usar un proxy de socket Docker con filtrado API.
Cadena de ataque 2: Propagación de montaje bidireccional → Manipulación de montajes del host
Escenario: Un contenedor tiene propagación de montaje Bidireccional habilitada (requerido para algunos drivers CSI) y CAP_SYS_ADMIN.
Precondiciones:
- Pod/contenedor tiene
mountPropagation: Bidirectional - El contenedor tiene
CAP_SYS_ADMIN(víaprivileged: trueo capacidad explícita) - Seccomp permite el syscall
mount(desactivado porprivileged: true) - Existe un montaje compartido en el host que el contenedor puede ver
Explotación paso a paso:
- El atacante crea un montaje bind dentro del contenedor
- El montaje se propaga al host
- Puede sombrear (ocultar) contenido legítimo del host montando sobre él
Cualquier proceso del host leyendo esta ruta ve contenido controlado por el atacante.
Lección: Propagación bidireccional + CAP_SYS_ADMIN = control de montajes del host. Es por eso que Kubernetes restringe Bidireccional solo a pods privilegiados.
Cadena de ataque 3: OverlayFS CVE → Escalación de privilegios del kernel
Escenario: Explotando CVE-2023-0386 para escalar de contenedor a root del host.
Precondiciones:
- Kernel sin parchear (OverlayFS vulnerable)
- Namespaces de usuario no privilegiados habilitados
- Capacidad de montar OverlayFS en un namespace de usuario
- Topología de montaje con capa inferior nosuid y capa superior no-nosuid
El exploit real añade un componente crítico: FUSE.
- Crear un filesystem FUSE que miente sobre la propiedad del archivo (declara archivos de UID 0 con bit SUID)
- Usar FUSE como la capa inferior de un montaje overlay dentro de un namespace de usuario
- Disparar copia-up modificando el binario SUID falso
- Fallo del kernel vulnerable: durante copia-up, el kernel no verificó que UID 0 en el namespace de usuario mapea a un UID válido en el host
- Resultado: un binario SUID dueño de root real aparece en el directorio superior
- Ejecutar el binario → escalación de privilegios local a root
Lección: Parchea tu kernel. Esta es la única solución real.
Dónde mis suposiciones fallaron
Suposición 1: “FUSE es el eslabón más débil”
Fui a esta investigación esperando que FUSE fuera el vector de vulnerabilidad primario. Cambio de contexto usuario-kernel, respuestas controladas por demonio, sobrecarga de rendimiento… todo gritaba “superficie de ataque.”
Lo que encontré: FUSE es debilidad en disponibilidad (DoS) y exposición de datos cross-usuario, no escalación de privilegios del host. Un demonio FUSE malicioso puede colgar procesos, desperdiciar recursos o servir datos inconsistentes a diferentes usuarios, pero no puede escalar directamente privilegios al host.
El eslabón más débil real es la mala configuración de propagación de montaje. El modo MS_SHARED crea un canal bidireccional directo entre tablas de montaje de contenedor y host. A diferencia de FUSE, esto no es un demonio en espacio de usuario que puedes terminar, es comportamiento forzado por el kernel que es fácil habilitar y difícil de notar.
Suposición 2: “El reetiquetado SELinux está sandboxeado”
Asumí que las opciones :z y :Z eran operaciones del lado del contenedor, quizás usando alguna capacidad o truco de namespace para reetiquetar archivos desde la perspectiva del contenedor.
Lo que encontré: El reetiquetado sucede en el host, antes de que el contenedor arranque. El runtime del contenedor recorre recursivamente el árbol del filesystem e invoca reetiquetado SELinux vía libselinux.
Esto significa que si montas /var/log:Z en un contenedor, acabas de reetiquetar cada archivo en /var/log con una etiqueta MCS específica del contenedor. Otros servicios del host leyendo esos archivos pueden ahora fallar con errores de permiso.
Esto no es un bug; es comportamiento documentado. Pero es sorprendente si asumes que las opciones de contenedor se quedan dentro del contenedor.
Suposición 3: “Los bugs de filesystem del kernel son raros”
Sabía que los contenedores comparten el kernel, pero asumí que el código del filesystem principal estaba suficientemente probado en batalla para que bugs explotables fueran raros.
Lo que encontré: CVE-2023-0386 (OverlayFS) explota el mecanismo de copia-up. El bug; validación de mapeo UID/GID defectuosa durante el manejo de capacidades de archivo fue bastante sutil para sobrevivir años de uso en producción. CISA lo añadió a su catálogo KEV en junio 2025, confirmando explotación activa dos años después de que el fusil se mergeara.
El kernel compartido no es un riesgo teórico. Es un vector de ataque práctico con explotación en curso.
Alternativas: cuando los contenedores no son suficientes
El problema del kernel compartido
La demostración de CVE-2023-0386 ilustra una restricción fundamental: los contenedores comparten el kernel del host, así que una vulnerabilidad del kernel afecta a cada contenedor simultáneamente. Ninguna cantidad de filtrado seccomp, dropping de capacidades o política MAC puede proteger contra un bug en la implementación del filesystem del kernel propio.
Esto no es un problema de configuración que pueda arreglar. Es una frontera arquitectónica. La pregunta se vuelve: ¿cuál es el primitivo de aislamiento mínimo que elimina esta clase de vulnerabilidad?
Análisis de frontera de aislamiento
Cada alternativa interpónese una frontera diferente entre cargas de trabajo de contenedor y el kernel del host. La métrica clave es la Base de Computación Confiable (TCB), el conjunto de componentes que deben ser correctos para que el aislamiento se mantenga.
gVisor toma un enfoque fundamentalmente diferente: en lugar de filtrar syscalls, reimplementa Linux en Go seguro para la memoria. El componente Sentry es esencialmente un kernel en espacio de usuario, maneja syscalls, gestión de memoria, filesystems, networking, todo.
Esto reduce drásticamente la superficie de ataque del host. El Sentry necesita solo 53 syscalls del host sin networking, 68 con él. Comparado con las ~350 syscalls en Linux 5.3, eso es una reducción del 80% en exposición del kernel del host.
Lo que aprecio de la arquitectura es la defensa-en-profundidad: incluso el Sentry se ejecuta dentro de seccomp-bpf, namespaces y cgroups como fronteras secundarias. Las operaciones de filesystem van por el VFS de Sentry, luego a un proceso Gofer separado que maneja acceso al filesystem del host vía LISAFS.
Firecracker toma el enfoque opuesto: en lugar de reimplementar el kernel, simplemente da a cada carga su propio kernel. Cada microVM ejecuta un guest Linux real, así que una vulnerabilidad del kernel del host requiere escapar KVM primero, un objetivo mucho más difícil.
Kata Containers te da aislamiento de VM con UX de contenedor, pero el TCB varía dramáticamente por elección de hypervisor. QEMU tiene casi 2 millones de líneas de C con décadas de código de emulación de dispositivos, mucha superficie de ataque.
¿Fue correcta mi hipótesis?
Mi hipótesis original: “El aislamiento de filesystems de contenedores es suficiente para cargas de trabajo multi-tenant sin VMs, si entiendes exactamente qué está pasando a nivel de syscall.”
Veredicto: Parcialmente correcto, con salvedades importantes.
Dónde los valores por defecto funcionan
Para cargas de trabajo internas confiables, encontré que el modelo de aislamiento por defecto funciona mejor de lo que esperaba. Esto es por qué:
- Filtrado seccomp: Docker/Podman bloquean
mount()y otros syscalls peligrosos por defecto. Esta es la primera línea de defensa. - Comportamiento del kernel para namespaces menos privilegiados: Cuando un runtime crea un namespace de usuario, los montajes compartidos heredados se automáticamente demoton a slave.
- Endurecimiento del runtime: La mayoría de runtimes (runc, crun, containerd) explícitamente aplican
MS_REC|MS_SLAVEa la raíz del contenedor. - Políticas MAC: AppArmor/SELinux proporcionan defensa-en-profundidad incluso si otras capas fallan.
Juntas, estas capas significan que los contenedores out-of-the-box de Docker, Podman o Kubernetes tienen aislamiento de filesystem razonable sin configuración especial.
Dónde se rompe
Para código multi-tenant no confiable (ejecutando código de otros), encontré huecos que no se pueden configurar:
- Los bugs del kernel son game over: Una sola vulnerabilidad OverlayFS puede conceder root a cualquier contenedor que satisfaga las precondiciones del exploit.
- La complejidad de configuración crea riesgo: Propagación
MS_SHARED, montajes de ruta del host, contenedores privilegiados - cada característica “avanzada” agujerea el aislamiento. - MAC depende de la integridad del runtime: CVE-2023-28642 mostró que bugs de resolución de ruta en runc pueden bypass AppArmor.
- El aislamiento de recursos es opt-in: Los límites I/O de cgroup, cuotas de almacenamiento y límites PID no están habilitados por defecto.
- La producción deriva de los valores por defecto: Los equipos añaden
privileged: truepara “hacer que las cosas funcionen”, montan sockets Docker para CI/CD, y acumulan configuraciones peligrosas con el tiempo.
Reflexiones finales
El aislamiento de filesystems de contenedores no es una pared, es un conjunto interconectado de mecanismos del kernel que trabajan juntos para crear una frontera de seguridad. Filtros seccomp, capacidades, namespaces de montaje, OverlayFS, reglas de propagación, políticas MAC y límites de recursos cgroup - cada uno contribuye una pieza.
Los valores por defecto funcionan mejor de lo que inicialmente esperaba. Seccomp bloqueando mount() out of the box significa que la mayoría de ataques basados en montaje fallan antes de empezar. La demotación compartido→esclavo del kernel para namespaces menos privilegiados, combinada con endurecimiento del runtime, proporciona protección significativa.
Pero el poder de romper el aislamiento vive en opciones de configuración que prometen conveniencia. La propagación de montaje bidireccional, el reetiquetado de ruta del host SELinux, contenedores privilegiados, montajes de socket Docker - estas son características documentadas, no bugs. Funcionan exactamente como se diseñaron. El problema es que su diseño comercia aislamiento por funcionalidad, y los entornos de producción acumulan estos comercios con el tiempo.
La línea entre multi-tenant segura y un host comprometido se dibuja donde eliges anular los valores por defecto. Y si estás ejecutando código verdaderamente no confiable, considera si los contenedores son la abstracción correcta en absoluto.
gVisor, Firecracker y Kata existen porque a veces la respuesta a “¿es suficiente el aislamiento de contenedores?” es simplemente “no”.










Comentarios