gVisor: sandboxing para contenedores multi-tenant

Logotipo oficial del proyecto gVisor alojado en el repositorio público de Google en GitHub, runtime de contenedores que implementa un kernel en espacio de usuario llamado Sentry para interceptar las llamadas al sistema del proceso invitado y reducir la superficie de ataque hacia el kernel del anfitrión, pieza central en despliegues multi-tenant donde el aislamiento clásico de contenedores no alcanza el nivel de confianza exigido por funciones serverless o por clientes compartiendo infraestructura

Los contenedores comparten el kernel del anfitrión y esa propiedad, que explica buena parte de su ligereza, también marca su techo en entornos multi-tenant donde la confianza entre cargas es baja. gVisor nació dentro de Google precisamente para resolver ese techo: interponer un kernel escrito en Go entre el proceso del contenedor y el kernel real del anfitrión, reducir la superficie de llamadas al sistema expuesta y ofrecer una forma de aislamiento intermedia entre el contenedor tradicional y la máquina virtual ligera. A finales de 2025, con años de uso en App Engine y Cloud Run, adopción en plataformas serverless de terceros y una base de usuarios técnicos suficientemente grande como para tener opiniones bien formadas, toca repasar qué hace bien, qué hace mal y en qué escenarios compensa.

Qué es gVisor y por qué se construyó

gVisor implementa un runtime compatible con OCI llamado runsc que sustituye a runc. La diferencia decisiva es que runsc no deja al proceso del contenedor hablar con el kernel del anfitrión a través de la tabla de llamadas al sistema. En su lugar, un componente llamado Sentry, que es un kernel de Linux reimplementado en Go en espacio de usuario, intercepta esas llamadas y responde a la mayoría por sí mismo, hablando con el anfitrión solo cuando no queda otra opción y siempre a través de un perímetro muy acotado. El resultado es que un exploit del kernel que normalmente escalaría del contenedor al anfitrión tiene que atravesar primero la implementación de Sentry, que es mucho más pequeña y está escrita en un lenguaje con seguridad de memoria.

Google abrió el código en 2018 y lo viene usando en producción para ejecutar código de clientes arbitrarios en plataformas donde el aislamiento compartido no es suficiente. App Engine, Cloud Run y Cloud Functions apoyan parte de su modelo de confianza en gVisor, y varios proveedores de serverless y edge han integrado runsc como alternativa o complemento a Firecracker. La motivación operativa es clara: tener un sandbox con tiempos de arranque cercanos a los de un contenedor clásico, pero con un modelo de amenaza más estrecho que el de compartir kernel sin más.

Arquitectura: Sentry, Gofer y el modo de plataforma

Cuando un contenedor arranca con runsc, el runtime crea dos procesos principales en el anfitrión. Sentry es el kernel en espacio de usuario y ejecuta el código del contenedor; Gofer es un proceso separado que actúa como intermediario para el acceso al sistema de archivos, hablando con el anfitrión en nombre del contenedor cuando éste necesita leer o escribir. Esta separación es deliberada: incluso si un atacante compromete Sentry, todavía tiene que atravesar Gofer para tocar el disco, y ni Sentry ni Gofer tienen capacidades privilegiadas más allá de lo estrictamente necesario.

La intercepción de llamadas al sistema se hace mediante dos modos de plataforma. El modo ptrace es portátil pero lento, y rara vez se usa en producción. El modo KVM aprovecha las extensiones de virtualización de hardware para atrapar las llamadas al sistema sin el sobrecoste de ptrace, con un rendimiento notablemente mejor pero con el requisito de hardware compatible y permisos para usar /dev/kvm. En 2024 llegó el modo systrap, que usa seccomp con filtros de notificación para interceptar llamadas sin depender de KVM ni de ptrace, y que es el que recomiendan los desarrolladores por defecto en 2025: buen rendimiento, portable y sin requisitos especiales de hardware.

Un detalle importante es que Sentry no implementa todas las llamadas al sistema de Linux. Cubre la mayoría de lo que un programa típico necesita, pero hay llamadas oscuras o muy específicas que no están y que fallarán con errores si el contenedor intenta usarlas. Esto es deliberado: cada llamada implementada es superficie de ataque potencial, y el proyecto prioriza cobertura útil sobre cobertura total.

Rendimiento: dónde gana y dónde pierde

La pregunta obligada con cualquier sandbox es cuánto cuesta. Para cargas con uso intenso de CPU y poco contacto con el kernel, gVisor está muy cerca de un contenedor nativo, porque la ejecución ocurre directamente en el procesador y Sentry solo entra en escena en los bordes. Pruebas comparativas publicadas por Google y reproducidas por terceros muestran diferencias del orden del 5 al 10 por ciento en cargas puras de cálculo sobre systrap, y aún menores sobre KVM.

La historia cambia con cargas pesadas en entrada-salida. Cualquier operación que cruce a Sentry paga el coste de la intercepción, y si además toca el disco pasa también por Gofer. Una base de datos tradicional bajo gVisor rinde significativamente peor que bajo runc: las pruebas que conozco muestran penalizaciones del 20 al 50 por ciento en transacciones por segundo según el patrón de acceso. Redis sufre menos porque sus operaciones tocan poco el sistema de archivos; Postgres o MySQL con escritura constante sufren mucho más. La red tiene un tratamiento parecido: gVisor trae su propia pila TCP escrita en Go, que funciona bien para tráfico moderado pero no iguala al kernel Linux en rendimiento bruto ni en cobertura de funciones avanzadas.

La lección operativa es que gVisor es una buena opción para cargas donde el aislamiento importa más que el último escalón de rendimiento: APIs HTTP, funciones serverless cortas, trabajos por lotes, ejecución de código de usuario no confiable. Es mala opción para bases de datos pesadas, sistemas de archivos distribuidos o cualquier carga cuya métrica principal sea IOPS.

Comparación con Kata Containers y microVMs

La comparación obvia es con Kata Containers, que también busca aislamiento reforzado para contenedores pero con otro enfoque: arranca una máquina virtual pequeña usando Firecracker o QEMU y ejecuta el contenedor dentro. El modelo de amenaza es distinto. Kata apuesta por la barrera de hardware del hipervisor; gVisor apuesta por la reducción de superficie en espacio de usuario. Ambas son legítimas y se eligen según lo que se quiera defender.

Kata tiende a tener mejor compatibilidad con cargas de entrada-salida y red intensivas porque dentro de la máquina virtual corre un kernel Linux completo, con sus optimizaciones de años. gVisor tiende a arrancar más rápido y consumir menos memoria fija por contenedor, porque no hay hipervisor ni kernel completo que cargar. En Cloud Run, donde el arranque en frío de cada función importa, la elección de gVisor tiene sentido; en cargas donde un sandbox se mantiene vivo durante horas, Kata puede compensar por mejor rendimiento sostenido.

Firecracker por sí solo, sin el envoltorio de Kata, es un ladrillo de construcción distinto. Proveedores como Fly.io y AWS Lambda lo usan directamente. El modelo de amenaza es fuerte por la separación de hipervisor, pero operar Firecracker puro implica construir mucha más integración con el orquestador que lo que requiere runsc, que se enchufa en containerd con pocas líneas de configuración.

Operación y despliegue

Integrar gVisor en un clúster existente es relativamente sencillo. Se instala runsc, se configura containerd para reconocerlo como runtime alternativo y se usa una RuntimeClass de Kubernetes para marcar qué pods deben arrancar bajo él. Los pods marcados pasan a ejecutarse con Sentry y Gofer; el resto sigue con runc. Esto permite aplicar gVisor solo a las cargas que lo necesitan, sin imponer su coste en E/S a todo el clúster.

La observabilidad funciona razonablemente bien. runsc exporta métricas en formato Prometheus con información de uso de CPU y memoria, y los registros se integran con el stack de registros habitual. El diagnóstico cuando algo falla es más complicado que con runc, porque los mensajes pueden venir de Sentry y no del kernel real, pero la documentación del proyecto ha mejorado mucho y hay una comunidad activa resolviendo dudas.

# Instalación típica en un nodo Debian
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt update && sudo apt install -y runsc
sudo runsc install  # añade runsc a containerd
sudo systemctl restart containerd

Cuándo compensa

Mi lectura tras seguir el proyecto es que gVisor ha encontrado un sitio claro: cargas multi-tenant donde el aislamiento importa y el patrón de uso es intensivo en CPU y ligero en E/S. Plataformas que ejecutan código de terceros, funciones serverless, entornos de pruebas donde usuarios distintos comparten nodos, clústeres educativos, laboratorios de análisis de malware. En todos esos casos la reducción de superficie de ataque compensa de largo el 5 o 10 por ciento de rendimiento que se pierde.

Donde no compensa es en cargas propias de una organización que confía en su propio código. Si todos los pods de un clúster los despliega el mismo equipo con el mismo pipeline, añadir un sandbox extra rara vez justifica el coste operativo. Y en cargas con necesidades altas de E/S de disco o red, la penalización es lo bastante significativa como para que Kata o incluso la máquina virtual completa sean mejores opciones.

La elección nunca es binaria: muchos operadores usan gVisor para una fracción del clúster, Kata para otra y runc para el resto, eligiendo la barrera que mejor encaja con el nivel de confianza y el perfil de rendimiento de cada carga. Esta heterogeneidad es hoy la respuesta madura a la pregunta de cómo aislar contenedores en entornos con muchos actores, y gVisor ocupa un lugar legítimo en ella.

Entradas relacionadas