Blog

¿Debería cada inquilino tener su propia VM?

Elige entre contenedores por inquilino, máquinas virtuales y configuraciones híbridas con un marco de decisión basado en riesgos y los pasos de endurecimiento que hacen defendible cada opción.

Resumen

El alojamiento multiinquilino te obliga a elegir hasta dónde pueden alcanzar los inquilinos entre sí. Los contenedores utilizan namespaces de Linux y cgroups para aislar procesos y recursos, pero comparten el kernel del host. Las máquinas virtuales añaden un límite a nivel de hardware, a costa de velocidad y carga operativa. Un enfoque híbrido —contenedores dentro de VM— puede darte ambos, pero duplica la superficie que debes parchear. Este artículo te guía a través de una decisión basada en riesgos, una comparación lado a lado y los pasos de endurecimiento de Docker que importan incluso dentro de una VM. Al final, sabrás qué modelo de aislamiento se ajusta a tus inquilinos y qué configurar antes del lanzamiento.

Tu aplicación multiinquilino está casi lista. Tienes un archivo de Docker Compose que levanta una pila por cliente, y es rápido. Entonces un amigo que dirige una empresa de hosting pregunta: "¿Le estás dando a cada inquilino su propia VM?" Te quedas helado. No habías planeado esa pregunta. Este artículo te da una forma de responderla hoy, sin un equipo de seguridad. Haces esto solo, así que la decisión debe ser lo suficientemente simple para defenderla a las 2 a.m.

Deja de intentar encontrar el modelo 'mejor'. Empieza por escribir qué sucede si el código de un inquilino toma el control de tu host. Define el radio de explosión antes de elegir cualquier herramienta. Ese ejercicio te dirá más que cualquier benchmark.

El kernel es el compañero de piso que no puedes desalojar

Los contenedores son eficientes porque comparten el kernel del host. Esa compartición es todo el truco, y todo el riesgo. Los namespaces de Linux le dan a cada contenedor su propia vista de procesos, red y sistema de archivos. Los grupos de control (cgroups) te permiten limitar CPU, memoria y E/S de disco para que un inquilino no pueda privar a los demás. Pero ninguno crea un muro de hardware.

Piensa en un contenedor como un proceso con una identificación falsa muy buena. Cree que está en su propia máquina. Sin embargo, el kernel es una copia de Linux que se ejecuta en tu host. Si un inquilino explota una vulnerabilidad del kernel, los namespaces se convierten en metadatos y nada más. Un atacante que pueda llamar funciones del kernel puede alcanzar otros namespaces en el mismo kernel. Ese es el escape de contenedor del que sigues oyendo hablar.

Supongamos que alojas una pequeña herramienta B2B con un contenedor por cliente. Un cliente instala un plugin sospechoso con un bug de ejecución remota de código. Con la configuración predeterminada de Docker, ese proceso se ejecuta como root dentro del contenedor. Root en un contenedor sigue siendo UID 0, y el kernel no distingue ese UID del root del host a menos que asignes usuarios explícitamente. El atacante puede intentar escapar, y el kernel compartido es su objetivo.

El fallo no necesita ser dramático. Un solo inquilino con fugas de memoria puede empujar al host al swap, ralentizando a todos los demás inquilinos. Sin límites de cgroup, un bucle mal comportado es un ataque de disponibilidad. Con ellos, es un proceso bloqueado y una alerta.

¿Significa esto que los contenedores no son seguros? No. Significa que debes tratar el kernel como una zona de confianza compartida. Antes de elegir, escribe una declaración de riesgo de un párrafo: 'Si el contenedor de un inquilino se ve comprometido, el atacante puede acceder a: [lista]. El costo comercial sería: [monto o impacto].' Si ese párrafo te asusta, no eres paranoico. Eres honesto.

Para una mirada más profunda al espectro de aislamiento, desde contenedores compartidos hasta pilas completamente separadas, consulta nuestra guía sobre cómo diseñar una arquitectura Docker multiinquilino.

Tres formas de dividirlo (elige una antes de implementar)

Realmente hay tres arquitecturas para el aislamiento multiinquilino. Cada 'mejor práctica' es una combinación de estas.

EnfoqueBarrera de aislamientoMejor cuandoAdvertencia más difícil
Contenedores por inquilinoNamespaces del kernel + cgroupsMuchos inquilinos pequeños, bajo riesgo por inquilino, necesidad de densidadUn exploit del kernel puede romper todos los inquilinos en ese host
Una VM por inquilinoVirtualización de hipervisor/hardwareDatos regulados, inquilinos hostiles, alto valor por inquilinoMás pesado, más lento de aprovisionar, parcheas un SO por inquilino
Contenedores dentro de VMsLímite de VM alrededor de cargas de trabajo contenedorizadasDensidad más un caparazón duro entre gruposCostos y sobrecarga operativa casi se duplican

Contenedores por inquilino. Este es el predeterminado para la mayoría de los fundadores de SaaS. Cada inquilino obtiene su propio contenedor o pequeña pila Compose. El aprovisionamiento es instantáneo, las imágenes son pequeñas, CI/CD es sencillo. Los límites de recursos evitan que los vecinos ruidosos devoren el servidor. La compensación es el kernel compartido. Si puedes mantener las cargas de trabajo sin privilegios y parchear el host regularmente, esta suele ser la primera jugada correcta.

No pongas dos inquilinos en el mismo contenedor. Eso es un kernel compartido más un runtime compartido más un sistema de archivos compartido. Si un inquilino sube un archivo que crea un proceso, el otro inquilino ya está en la misma tabla de procesos. Un contenedor es tu unidad de aislamiento; hazlo un inquilino por contenedor.

¿Y la base de datos? Si cada inquilino se conecta a una instancia de MongoDB o PostgreSQL con las mismas credenciales, ya has añadido un componente compartido enorme. Dale a cada inquilino credenciales separadas e idealmente una base de datos o esquema separado. Los contenedores aíslan la aplicación; la base de datos suele ser la primera fuga que un atacante probará.

Una VM por inquilino. Dale a cada inquilino una máquina virtual completa. El hipervisor añade un límite a nivel de hardware, que es exactamente lo que un exploit del kernel debe cruzar para llegar al host. Esto importa para entornos regulados o cuando los inquilinos no son confiables. El costo es densidad y tiempo. Ahora gestionas un parque de sistemas operativos, no solo contenedores. Cada VM necesita actualizaciones, agentes de seguridad y monitoreo. Para un fundador solo, eso es trabajo real.

Patrones que funcionan en este nivel: usa infraestructura como código para crear una VM desde la misma imagen base, incorpora actualizaciones en nuevas imágenes en lugar de parchear sistemas en vivo, y termina cargas de trabajo que no reconozcas. Mantén el puerto de administración de la VM cerrado a internet.

Contenedores dentro de VMs. Este híbrido rara vez se discute en tutoriales para principiantes. Pones una pequeña VM alrededor de cada inquilino (o pequeño grupo de inquilinos), y luego ejecutas contenedores dentro de esa VM. La VM es un contenedor de radio de explosión; los contenedores son solo unidades desplegables. Esto te da el borde duro de la virtualización y la reproducibilidad de las imágenes. Cuesta más, porque pagas por la sobrecarga de virtualización y la flexibilidad de los contenedores, pero puede ser el modelo más sensato a largo plazo cuando no puedes confiar completamente en los inquilinos.

Un microejemplo común: un inquilino ejecuta una API de Node y un trabajador en segundo plano. En lugar de un contenedor enorme con ambos procesos, usa una VM, luego dos contenedores con diferentes límites de recursos, una red compartida, y sin exposición directa a internet para el trabajador. La VM proporciona el borde duro; los contenedores proporcionan estructura.

¿Cuál deberías elegir? La tabla es tu lista corta. Las siguientes secciones hacen la decisión concreta.

Si eliges contenedores, haz estas seis cosas o no te molestes

Los contenedores por inquilino están bien si tratas cada contenedor como un atacante potencial. Eso comienza con la configuración, no con pensamientos ilusorios.

0. Limita los recursos antes de confiar en alguien. Los cgroups son un mecanismo de equidad y una defensa de disponibilidad. Configura --memory y --cpus por contenedor. Un inquilino con fugas de memoria debería alcanzar su propio límite, no el de tu servidor. Esto no es un límite de seguridad, pero un vecino ruidoso es un ataque sin una sola línea de código. Un inicio práctico: --memory 512m --cpus 0.5. Para un proceso trabajador, comienza más bajo y escala.

1. Ejecuta como usuario no root. Nunca permitas que el proceso del contenedor use UID 0 a menos que absolutamente lo necesites. Configura un usuario en el Dockerfile y pasa --user como guardia adicional. Un exploit que se ejecuta como usuario sin privilegios tiene muchos menos caminos hacia el kernel. En tu Dockerfile, crea un usuario: RUN useradd -u 10001 app y USER app. No te saltes esto para ahorrar tiempo.

2. Elimina toda capacidad que no necesites. Las capacidades de Linux dividen el poder de root en pequeñas piezas. La mayoría de las aplicaciones web no necesitan casi ninguna. Comienza con --cap-drop=ALL y añade solo lo que sepas que necesitas. Un contenedor sin CAP_SYS_ADMIN es mucho más difícil de usar para trucos de namespaces. Si tu aplicación intenta vincular un puerto privilegiado, ejecútalo en un puerto alto y pon un proxy al frente en lugar de otorgar NET_BIND_SERVICE.

3. Haz que el sistema de archivos sea de solo lectura. Tu aplicación no debería escribir en su propia capa de contenedor. Monta un tmpfs para el estado. Un atacante que no puede escribir en disco tiene mucho más difícil plantar persistencia. Una aplicación PHP comprometida que intenta escribir una webshell fallará cuando el sistema de archivos raíz sea de solo lectura. Puedes montar un volumen con nombre para un directorio escribible que tu aplicación realmente necesite.

4. Aplica seccomp y AppArmor o SELinux. Estos envían syscalls riesgosas a la pila de descarte. Docker incluye un perfil seccomp predeterminado; úsalo. Añade un perfil de AppArmor para otra capa. No necesitas dominar cada syscall. Necesitas denegar lo que un trabajador web normal nunca requiere. Nunca ejecutes con --privileged. Ese flag desactiva casi todas las defensas que acabas de configurar.

5. Segmenta la red. No le des a cada contenedor una ruta a todos los demás contenedores. Denegación predeterminada, luego abre solo los puertos que necesitas. Un contenedor de base de datos comprometido no debería poder escanear tu panel de administración. Si los inquilinos están en redes separadas, una brecha en una red no puede propagarse lateralmente.

Un inicio práctico:

docker run --user 10001 --cap-drop=ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,size=64M --security-opt seccomp=default.json --memory 512m --cpus 0.5 myimage

Pon los mismos flags en un archivo Compose y aplícalos a cada inquilino. Esto no es completo, pero es un predeterminado mucho más fuerte que lo que docker run te da de fábrica.

Para un tutorial más profundo, usa nuestra guía paso a paso de endurecimiento para contenedores Docker en alojamiento multiinquilino.

El aislamiento de contenedores mejorado de Docker es la excepción que deberías conocer

Si ejecutas dentro de un entorno Docker gestionado, busca el Aislamiento de Contenedores Mejorado de Docker (ECI, por sus siglas en inglés). Utiliza aislamiento de namespaces de usuario y un runtime de contenedores seguro bajo el capó. Root dentro de un contenedor se mapea a un usuario sin privilegios en el host, por lo que incluso un contenedor que se ejecuta como root no obtiene privilegios de root del host. También bloquea capacidades y syscalls peligrosas por defecto. Esto no es algo que puedas recrear con unos pocos flags en Docker vanilla. Si tu plataforma lo soporta, actívalo. No elimina la necesidad de usuarios no root y límites de recursos, pero cambia las matemáticas del riesgo.

Puedes aproximar parte de esto con el re-mapeo de namespaces de usuario (userns-remap) en el daemon de Docker. Eso no es tan completo como un runtime seguro, pero es mejor que nada. Si lo usas, verifica que el mapeo de UID funcione antes de confiar en él.

La falacia de la VM: mudarse a máquinas virtuales no es endurecimiento

Aquí está la parte contraria, y es la parte que la mayoría de la gente se salta. Si te mudas a una VM por inquilino y luego despliegas tus contenedores normales dentro de ella, no has eliminado tu problema de seguridad de contenedores. Has añadido una jaula amplia. El escape del contenedor todavía funciona; el atacante simplemente aterriza en la VM en lugar del host. Eso es una mejora real, pero aún necesitas los seis pasos.

La otra trampa es asumir que la VM en sí es segura. Una imagen predeterminada con una contraseña SSH débil, paquetes base sin parchear, o un puerto de administración abierto es un regalo. El límite del hipervisor solo importa si el invitado está endurecido y actualizado. De lo contrario, tu 'VM segura' es un camino más rápido hacia el compromiso porque te sientes seguro y dejas de verificar.

Lo que una VM te da es un radio de explosión reducible. El desastre de un inquilino se queda en una VM. Lo que te cuesta es tu tiempo. Te conviertes en el administrador de sistemas de tantos sistemas operativos como inquilinos tengas. Si eres un fundador solo lanzando un producto, pregúntate si tienes las horas para parchear y monitorear un parque. Si sí, VM por inquilino puede ser la decisión correcta. Si no, contenedores con fuerte endurecimiento pueden ser más honestos.

También recuerda que tu host de hipervisor es un objetivo crítico. Un hipervisor comprometido puede ver todos los invitados. Parchea el host, no solo los invitados. La VM no te excusa de parchear el host; eleva las apuestas por no hacerlo.

Una advertencia sobre el híbrido: no asumas que los contenedores dentro de una VM te dan 'dos capas de seguridad' gratis. La VM añade un límite; el contenedor aún necesita no root, capacidades y seccomp. De lo contrario, la primera capa es tan fuerte como el contenedor más débil.

Cuatro preguntas que resuelven el debate en diez minutos

No optimices en abstracto. Hazte estas cuatro preguntas en orden. Escribe las respuestas.

1. ¿A qué tiene acceso mi inquilino? Si un inquilino solo puede alcanzar su propia aplicación web y base de datos, los contenedores por inquilino con reglas de red estrictas son defendibles. Si los datos de un inquilino están regulados o son financieramente sensibles, muévete hacia VMs.

2. ¿Cuánto me costaría el compromiso de un inquilino? Suma clientes perdidos, exposición legal y confianza. Si el número es mayor que el costo de ejecutar VMs, gasta el dinero. Si no, los contenedores son una elección racional.

3. ¿Cuántos inquilinos tengo y cuánto pagan? Muchos suscriptores pequeños: la densidad de contenedores importa. Un puñado de cuentas grandes: dale a cada una una VM y factura en consecuencia. Los inquilinos que te pagan menos que un café no deberían requerir cada uno un SO que gestionar.

4. ¿Puedo parchear cosas en un horario? Los contenedores comparten un kernel de host, así que parchear el host protege a todos. Las VMs multiplican tus objetivos de parche. Si sabes que te saltarás las actualizaciones, elige la arquitectura con menos partes móviles y predeterminados más duros.

Tus respuestas se agruparán. Dos o más respuestas enfocadas en VM significa que no deberías estar usando contenedores por inquilino por defecto. Tres o más respuestas enfocadas en contenedores significa que las VMs son prematuras. Un resultado contraintuitivo: un inquilino de bajos ingresos con acceso a datos sensibles aún necesita la VM, porque el costo regulatorio no tiene nada que ver con cuánto pagan.

Lanza lo mínimo que puedas confiar, luego gana más aislamiento

Tu primera arquitectura no tiene que ser la final. Comienza con la configuración más ajustada que realmente puedas mantener, luego añade aislamiento según tu base de inquilinos lo justifique. Para la mayoría de los operadores solitarios, eso significa contenedores por inquilino con no root, capacidades limitadas, sistemas de archivos de solo lectura, seccomp y segmentación de red. Para inquilinos regulados o de alto valor, salta directamente a una VM por inquilino, con contenedores solo como capa de empaquetado dentro.

Sea lo que sea que elijas, escribe la decisión y revísala trimestralmente. Cuando recibas tu primera pregunta '¿deberíamos mover este inquilino a una VM?', tendrás una respuesta, y tendrás la lista de verificación para respaldarla. Eso es lo que realmente significa aislamiento: una compensación que gestionas, no una tecnología que compras.

Antes del lanzamiento, revisa nuestra lista de verificación práctica de seguridad de aislamiento Docker — convierte estas decisiones en una lista que puedes verificar antes de mostrar una página a un cliente.

Sources (5)