Contenedores frente a máquinas virtuales: aislamiento contra peso
El eje que ordena toda la ejecución aislada moderna: contenedores que comparten el kernel del anfitrión mediante namespaces y cgroups frente a máquinas virtuales que traen su propio kernel tras la frontera del hardware, la superficie de ataque de cada modelo, y las tecnologías que buscan el punto medio: las microVMs de Firecracker, el kernel en espacio de usuario de gVisor y los contenedores dentro de VMs de Kata.
Todo el nivel ha construido máquinas virtuales: kernels enteros aislados tras la frontera del hardware. Pero existe otra forma de aislar procesos que no fabrica ninguna máquina, y que domina la nube: el contenedor. Un contenedor no arranca un kernel; toma el kernel del anfitrión y le pide que le mienta a un grupo de procesos sobre lo que ven —su árbol de procesos, su red, sus ficheros— usando namespaces y cgroups. Es ligerísimo y arranca en milisegundos, pero comparte el kernel con todos los demás, y esa frontera es de software. La máquina virtual es lo contrario: pesada, lenta de arrancar, pero separada por silicio. Este nivel de cierre traza el eje que ordena toda la ejecución aislada —aislamiento contra peso— y presenta las tecnologías que se niegan a elegir.
- Contrastar los dos modelos de aislamiento:
namespacesmáscgroupsfrente a hipervisor másEPT. - Comparar sus superficies de ataque y por qué determinan la fuerza del aislamiento.
- Entender las microVMs de Firecracker como síntesis de densidad y aislamiento por hardware.
- Situar gVisor y Kata en el equilibrio entre seguridad y rendimiento.
Dos formas de aislar
Un contenedor es un puñado de procesos normales del anfitrión a los que el kernel les recorta la visión. Los namespaces particionan lo que un proceso puede ver —un PID namespace le da su propio árbol de procesos donde él es el PID 1, un network namespace su propia pila de red, un mount namespace su propio árbol de ficheros— y los cgroups limitan lo que puede consumir de CPU, memoria y E/S. No hay kernel invitado: las llamadas al sistema de esos procesos van directas al mismísimo kernel del anfitrión que sirve a todos los demás.
# crear un contenedor a mano es crear namespaces: un arbol de procesos propio
unshare --pid --net --mount --fork --mount-proc /bin/sh
# dentro, este proceso se cree el PID 1 de su propio universo
ps aux # solo se ve a si mismo
lsns # desde el anfitrion: lista los namespaces existentes
Bajo el capó, unshare no es magia: llama a clone o a la syscall unshare con una bandera por cada dimensión que se quiere aislar. Un runtime de contenedores como runc hace exactamente esto por ti:
/* fabricar un contenedor es fabricar namespaces: una bandera por dimension */
int flags = CLONE_NEWPID /* arbol de procesos propio */
| CLONE_NEWNET /* pila de red propia */
| CLONE_NEWNS /* arbol de montajes propio */
| CLONE_NEWUTS /* hostname propio */
| CLONE_NEWIPC /* colas y semaforos IPC propios */
| CLONE_NEWUSER /* mapa de UID propio: root dentro, no fuera */
| SIGCHLD;
pid_t hijo = clone(fn_contenedor, pila_top, flags, NULL);
Lo que los namespaces particionan, los cgroups v2 lo contabilizan: cada controlador —cpu, memory, io, pids— cuelga de una jerarquía en /sys/fs/cgroup y pone techos a lo que el grupo consume, de modo que un contenedor no pueda asfixiar a sus vecinos agotando la memoria o la CPU del anfitrión compartido.
Una máquina virtual no recorta ninguna visión: fabrica una máquina entera. El invitado trae su propio kernel, que corre en modo non-root tras la frontera de VT-x/AMD-V (nivel 53.1), con su memoria aislada por EPT (nivel 53.2). Sus llamadas al sistema jamás tocan el kernel del anfitrión: las atiende su propio kernel invitado, y solo los VM exit —un conjunto pequeño y controlado de eventos— cruzan al hipervisor. Dos filosofías opuestas de la misma palabra, aislar: recortar la percepción de un proceso, o darle una máquina propia.
La superficie de ataque
La diferencia decisiva no es el peso, es la superficie de ataque, y se mide en cuánta frontera confidencial expones al código no confiable. Los procesos de un contenedor hablan con el kernel del anfitrión por la interfaz de llamadas al sistema entera: más de trescientas syscalls, cada una con sus argumentos y sus rincones. Un solo fallo explotable en cualquiera de ellas —una escalada de privilegios en el kernel— y el código del contenedor se escapa al anfitrión y a todos los contenedores vecinos. Se endurece con seccomp para vetar syscalls peligrosas, con capacidades reducidas y con SELinux, pero la superficie sigue siendo el kernel completo.
El código de una máquina virtual, en cambio, solo puede atacar al hipervisor a través de lo que provoca VM exits: unas pocas instrucciones privilegiadas, unos accesos de E/S, la interfaz virtio. Es una superficie estrechísima comparada con trescientas syscalls, y por eso el aislamiento por hardware se considera cualitativamente más fuerte: para escapar de una VM no basta un error en una syscall cualquiera, hace falta un fallo en el reducido código del hipervisor o en el propio silicio de virtualización.
Ese contraste no es binario sino un espectro. Entre el contenedor puro y la máquina virtual completa se han ido colando puntos intermedios que mueven la frontera sin saltar de un extremo al otro:
flowchart LR subgraph ligero [Mas ligero y denso, frontera de software] C[Contenedor comparte kernel] G[gVisor kernel en espacio de usuario] end subgraph pesado [Mas aislado, frontera de hardware] M[microVM Firecracker] V[Maquina virtual completa] end C --> G --> M --> V
Contenedor
Comparte el kernel del anfitrión. Aísla con namespaces y cgroups. Arranca en milisegundos, densidad altísima, casi sin sobrecoste. Superficie de ataque: toda la interfaz de syscalls.
Máquina virtual
Trae su propio kernel tras la frontera del hardware. Aísla con VT-x y EPT. Arranca en segundos, sobrecoste de memoria por invitado. Superficie de ataque: los VM exit del hipervisor.
microVMs: Firecracker
Durante años se aceptó el dilema como inevitable: densidad y velocidad de contenedor, o aislamiento de máquina virtual, pero no ambos. Firecracker, el Virtual Machine Monitor que AWS escribió en Rust para Lambda y Fargate, demuestra que era un falso dilema. Es una microVM: una máquina virtual real, sobre KVM, con aislamiento por hardware genuino, pero despojada de todo lo que hace pesada a una VM tradicional. No emula BIOS ni bus PCI ni tarjetas gráficas; su modelo de dispositivos se reduce a lo esencial paravirtualizado —virtio-net, virtio-blk, virtio-vsock— y un puerto serie. Arranca el kernel invitado por el protocolo de arranque de Linux directamente, sin firmware.
El resultado descoloca las intuiciones: una microVM arranca en torno a 125 ms, añade unos pocos megabytes de sobrecoste de memoria por instancia y permite empaquetar miles en un solo servidor, todo con la frontera de seguridad de una VM completa. Su configuración es minimalista, un objeto por dispositivo:
{
"boot-source": {
"kernel_image_path": "vmlinux",
"boot_args": "console=ttyS0 reboot=k panic=1 pci=off"
},
"drives": [{
"drive_id": "rootfs",
"path_on_host": "rootfs.ext4",
"is_root_device": true,
"is_read_only": false
}],
"machine-config": { "vcpu_count": 2, "mem_size_mib": 256 }
}
Firecracker es la prueba de que gran parte del peso de una VM no venía de la virtualización en sí, sino del equipaje de compatibilidad —firmware, buses heredados, hardware emulado— que arrastraban los hipervisores de propósito general. Quítalo y la máquina virtual se vuelve casi tan ágil como un contenedor sin renunciar a su frontera de hardware.
gVisor y Kata: los dos caminos al punto medio
Hay un enfoque opuesto y fascinante: en vez de hacer ligera la VM, hacer seguro el contenedor interponiendo un kernel en espacio de usuario. Eso es gVisor, de Google. Sus contenedores corren bajo un runtime, runsc, que intercepta las llamadas al sistema de la aplicación y las sirve él mismo, con un kernel Linux reimplementado en Go —el Sentry— que vive en el espacio de usuario. La aplicación cree hablar con el kernel del anfitrión, pero habla con el Sentry; y el Sentry, para hacer su trabajo, solo necesita un puñado reducido y vigilado de syscalls reales contra el anfitrión.
# gVisor: el contenedor corre sobre runsc, no sobre el kernel del anfitrion
docker run --runtime=runsc alpine cat /proc/version
# las syscalls de la app las atiende el Sentry, un kernel en espacio de usuario
Así se estrecha la superficie de ataque sin arrancar una VM: las trescientas syscalls que el contenedor podía atacar quedan reducidas a las poquísimas que el Sentry emite. El precio es el sobrecoste de interceptar y reimplementar cada syscall, y una compatibilidad que no siempre es total. Kata Containers elige el otro camino: cada contenedor corre dentro de una microVM ligera sobre KVM, con la experiencia y las herramientas de un contenedor OCI pero el aislamiento de una VM. Firecracker adelgaza la VM, gVisor blinda el contenedor, Kata mete el contenedor en una VM: tres respuestas a la misma pregunta de dónde poner la frontera.
La regla operativa que resume el eje: si dos cargas de trabajo comparten kernel, comparten destino. Un pánico o una vulnerabilidad de escalada en el kernel del anfitrión las afecta a todas por igual. Por eso la multitenencia hostil —ejecutar código no confiable de clientes distintos en la misma máquina— empujó a la nube hacia microVMs y sandboxes: cuando el vecino puede ser un atacante, el aislamiento de software del contenedor puro no basta.
Cierra el nivel entendiendo que contenedores y máquinas virtuales no son dos tecnologías rivales, sino dos puntos de un único eje continuo, y que ese eje no tiene un ganador porque codifica un intercambio que no se puede burlar. En un extremo, el contenedor comparte el kernel y por eso es ingrávido: no duplica nada, arranca al instante, cabe por millares, pero paga esa levedad con una frontera de software tan ancha como la interfaz de syscalls entera, y quien comparte kernel comparte destino. En el otro extremo, la máquina virtual duplica el kernel y por eso es sólida: su frontera es el estrecho puente del hipervisor sobre el silicio, pero paga esa solidez con el peso de arrastrar un sistema operativo entero por invitado. La aparente magia de Firecracker, gVisor y Kata no rompe el intercambio, lo reubica. Firecracker no elimina el coste de la VM, lo minimiza tirando el equipaje de compatibilidad que nunca fue esencial a la virtualización. gVisor no elimina la anchura de la superficie de syscalls, la traslada a un kernel en espacio de usuario que él controla y puede vigilar. Kata no funde ambos mundos, encapsula uno dentro del otro. Cada uno decide dónde trazar la línea entre lo confiable y lo hostil, y qué precio pagar por trazarla ahí: en ciclos, en memoria, en compatibilidad, en complejidad. Esa es la ley que gobierna no solo la virtualización sino la seguridad de sistemas entera, y con la que conviene salir de estos cincuenta y tres niveles: el aislamiento no es un bien que se compre en más cantidad, es una frontera que se coloca en algún sitio, y todo el arte consiste en elegir el sitio con los ojos abiertos al precio. No preguntes qué opción aísla más. Pregunta dónde está su frontera, qué la puede cruzar, y cuánto cuesta ponerla ahí.
- Crea un contenedor a mano con
unshare --pid --net --mount --forky comprueba conpsylsnsque tiene su propio árbol de procesos; explica qué recorta cada namespace. - Argumenta en cuatro líneas por qué la superficie de ataque de un contenedor es mayor que la de una máquina virtual, nombrando la interfaz concreta de cada una.
- Explica qué equipaje elimina Firecracker respecto a QEMU y por qué eso permite arrancar en unos
125 mssin perder el aislamiento por hardware. - Describe cómo el Sentry de gVisor estrecha la superficie de syscalls y qué coste introduce a cambio.
- Para tres escenarios —un microservicio interno de confianza, funciones de clientes anónimos, y una base de datos de máxima densidad— elige entre contenedor, microVM o gVisor y justifica dónde pones la frontera y qué precio aceptas.