Analizando el aislamiento de filesystems en contenedores para cargas multi-tenant
14 min de lectura

Analizando el aislamiento de filesystems en contenedores para cargas multi-tenant

2873 palabras

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:

  1. El atacante descubre acceso al socket Docker
  2. Crea un contenedor privilegiado con el filesystem del host montado
  3. Lee archivos sensibles del host (/etc/shadow, claves SSH, secretos de aplicación)
  4. 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ía privileged: true o capacidad explícita)
  • Seccomp permite el syscall mount (desactivado por privileged: true)
  • Existe un montaje compartido en el host que el contenedor puede ver

Explotación paso a paso:

  1. El atacante crea un montaje bind dentro del contenedor
  2. El montaje se propaga al host
  3. 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.

  1. Crear un filesystem FUSE que miente sobre la propiedad del archivo (declara archivos de UID 0 con bit SUID)
  2. Usar FUSE como la capa inferior de un montaje overlay dentro de un namespace de usuario
  3. Disparar copia-up modificando el binario SUID falso
  4. 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
  5. Resultado: un binario SUID dueño de root real aparece en el directorio superior
  6. 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é:

  1. Filtrado seccomp: Docker/Podman bloquean mount() y otros syscalls peligrosos por defecto. Esta es la primera línea de defensa.
  2. 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.
  3. Endurecimiento del runtime: La mayoría de runtimes (runc, crun, containerd) explícitamente aplican MS_REC|MS_SLAVE a la raíz del contenedor.
  4. 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:

  1. Los bugs del kernel son game over: Una sola vulnerabilidad OverlayFS puede conceder root a cualquier contenedor que satisfaga las precondiciones del exploit.
  2. 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.
  3. MAC depende de la integridad del runtime: CVE-2023-28642 mostró que bugs de resolución de ruta en runc pueden bypass AppArmor.
  4. 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.
  5. La producción deriva de los valores por defecto: Los equipos añaden privileged: true para “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

Últimas Entradas

12 min

2449 palabras

I recently came across an exceptionally dense technical analysis about container security that’s worth sharing. The author started with a simple hypothesis: container filesystem isolation should be sufficient for multi-tenant workloads without virtual machines, if you sufficiently understand what’s happening at the syscall level.

After thorough investigation, the conclusion is more uncomfortable than expected: the defaults protect you well, but the moment you reach for “advanced” features like bidirectional mount propagation or SELinux relabeling, you’re one misconfiguration away from handing an attacker the keys to your host.

4 min

706 palabras

Hace unos días, trabajando con Claude Code, me topé con una herramienta que lleva bastante tiempo en el ecosistema Docker pero que no conocía: docker pushrm. Y la verdad es que me ha sorprendido lo útil que resulta para algo tan simple como mantener sincronizada la documentación de tus repositorios de contenedores.

El problema que resuelve

Cualquiera que haya trabajado con Docker Hub, Quay o Harbor conoce el típico flujo: actualizas el README de tu proyecto en GitHub, construyes y pusheas tu imagen, pero… el README del registro de contenedores sigue desactualizado. Tienes que ir manualmente al navegador, copiar y pegar el contenido, y hacer el update manualmente.

5 min

991 palabras

Vercel ha anunciado la disponibilidad general de Vercel Sandbox, una capa de ejecución diseñada específicamente para agentes de IA. Pero más allá del hype de los agentes, hay una pregunta interesante: ¿puede servirnos para ejecutar código de forma segura en diferentes lenguajes como PHP, Node o Go?

¿Qué es Vercel Sandbox?

Vercel Sandbox proporciona microVMs Linux bajo demanda. Cada sandbox está aislado, con su propio sistema de archivos, red y espacio de procesos. Obtienes acceso sudo, gestores de paquetes y la capacidad de ejecutar los mismos comandos que ejecutarías en una máquina Linux.

8 min

1546 palabras

The Problem We All Have (But Solve Poorly)

As a DevOps Manager, I spend more time than I should configuring ways for the team to show their development work. Client demos, webhooks for testing, temporary APIs for integrations… we always need to expose localhost to the world.

Traditional options are a pain:

  • ngrok: Works, but ugly URLs, limits on free plan, and every restart generates a new URL
  • localtunnel: Unstable, URLs that expire, and often blocked by corporate firewalls
  • SSH tunneling: Requires your own servers, manual configuration, and networking knowledge
  • Manual Cloudflare Tunnels: Powerful but… God, the manual configuration is hellish

And then I discovered Moley.

8 min

1579 palabras

El problema que todos tenemos (pero solucionamos mal)

Como DevOps Manager, paso más tiempo del que debería configurando formas para que el equipo pueda mostrar su trabajo en desarrollo. Demos para clientes, webhooks para testing, APIs temporales para integraciones… siempre necesitamos exponer localhost al mundo.

Las opciones tradicionales son un dolor:

  • ngrok: Funciona, pero URLs feas, límites en el plan gratuito, y cada reinicio genera una URL nueva
  • localtunnel: Inestable, URLs que expiran, y a menudo bloqueado por firewalls corporativos
  • SSH tunneling: Requiere servidores propios, configuración manual, y conocimiento de redes
  • Cloudflare Tunnels manuales: Potente pero… Dios, la configuración manual es infernal

Y entonces descubrí Moley.