Caso de Estudio: Brecha SaaS Zyght — Aislamiento por Tenant
En abril de 2026, Zyght — una plataforma SaaS chilena de HSE (Salud, Seguridad y Medio Ambiente) — fue breacheada. 6.1TB de datos de 90+ organizaciones fueron robados, incluyendo PII de trabajadores, registros medicos, informes de incidentes, auditorias de compliance e informacion de seguridad operacional. Muchas de las empresas afectadas estan clasificadas como Operadores de Importancia Vital por la ANCI.
El Problema de Raiz
La brecha fue catastrofica no por un exploit sofisticado, sino por una arquitectura de multi-tenancy compartida. Cuando el atacante gano acceso a un punto de entrada, alcanzo los datos de los 90+ tenants a traves de una sola capa de aplicacion y base de datos.
Internet
└─ Load Balancer
└─ 1 Aplicacion (compartida)
└─ 1 Base de Datos (compartida)
├─ Datos Codelco
├─ Datos SQM
├─ Datos Falabella
├─ Datos Nestle
├─ Datos Banco de Chile
└─ ... 85 organizaciones mas
Comprometes la app = acceso a TODOS los tenants
Exfiltracion total: 6.1TB, 19 millones de archivosMentat: Sandbox-por-Tenant
En un despliegue Mentat, cada tenant corre en su propio sandbox aislado con su propia conexion a base de datos. El kernel impone el limite — no la logica de la aplicacion.
Internet
└─ Caddy (ingress)
├─ Sandbox: zyght-codelco
│ ├─ Namespace PID (aislado)
│ ├─ Namespace mount + pivot_root
│ ├─ Credenciales DB propias (secrets aislados)
│ ├─ Limites cgroups v2
│ └─ DB: codelco_hse
│
├─ Sandbox: zyght-sqm
│ ├─ Namespace PID (aislado)
│ ├─ Namespace mount + pivot_root
│ ├─ Credenciales DB propias
│ ├─ Limites cgroups v2
│ └─ DB: sqm_hse
│
└─ ... 88 sandboxes mas
Comprometes un sandbox = acceso a UN tenant
Blast radius: ~68GB en vez de 6.1TBComparacion de Impacto
| Metrica | Tenancy Compartida (Zyght) | Sandbox-por-Tenant (Mentat) |
| Datos expuestos | 6.1TB — los 90+ tenants | ~68GB — solo un tenant |
| Archivos expuestos | 19 millones | ~210K (proporcional) |
| Organizaciones afectadas | 90+ | 1 |
| Movimiento lateral | Trivial — espacio de procesos compartido | Bloqueado — aislamiento PID + mount namespace |
| Alcance de credenciales | Credenciales master de DB sirven a todos los tenants | Cada sandbox tiene solo sus propias credenciales |
| Deteccion de exfiltracion | 6.1TB por la red — dificil de no notar pero no fue detectado | Deteccion de anomalias cgroups v2 por sandbox (memoria + CPU) |
| Recuperacion | Rotar credenciales para 90+ orgs, notificar a todos, asumir compromiso total | Rotar credenciales para 1 org, incidente contenido |
Capas de Defensa
| Capa 1 | Sandbox por tenant | Cada organizacion corre en su propio namespace. Sin espacio de procesos compartido |
| Capa 2 | Credenciales aisladas | Cada sandbox recibe solo su propia cadena de conexion DB via Mentat secrets. Sin credenciales master |
| Capa 3 | pivot_root | Atacante dentro del sandbox no puede leer filesystem del host, otros sandboxes, ni estado de Mentat |
| Capa 4 | Monitoreo cgroups v2 | Transferencia anomala de datos (6.1TB) dispara alertas de ScaleWatcher. Memoria y CPU rastreados por sandbox |
| Capa 5 | Namespace de red | CLONE_NEWNET + veth restringe egress solo al endpoint de DB del tenant. Sin conexiones salientes arbitrarias |
| Capa 6 | Base inmutable | Codigo de la aplicacion en capa overlayfs read-only. Atacante no puede modificar la app para persistir backdoor |
Hull: seccomp, Landlock y egress por bridge, por tenant
Sandbox-per-tenant ya reduce el blast radius a un solo tenant. El driver Hull endurece cada tenant aun mas agregando politicas que se configuran una vez y se aplican automaticamente a cada tenant nuevo:
| Perfil seccomp por tenant | Cada tenant corre bajo el perfil node de Hull (o beam / dotnet para stacks que no son Node). Un atacante que consigue ejecucion de codigo via SQL injection no puede hacer ptrace a procesos hermanos, no puede llamar bpf para leer memoria del kernel, no puede keyctl para robar credenciales de otros sandboxes. Menos de 100 syscalls de ~400 permitidos. |
| Landlock por tenant | Allowlist de filesystem fija el workload a su propio directorio de tenant. Incluso si un atacante corre como uid 0 dentro del sandbox, open() sobre los archivos de otro tenant retorna EACCES — el LSM aplica la frontera de path por debajo del VFS, por debajo de las capabilities. |
| Egress por bridge por tenant | Con network: bridge, cada tenant recibe su propia IP en hull0 (10.88.0.X). Una regla nftables puede restringir egress para que el tenant N solo alcance su propio endpoint de base de datos — no las DBs de otros tenants, no servidores C2 arbitrarios, no el control plane de Mentat. Los caminos de exfiltracion literalmente no rutean. |
| Cero daemon compartido | Hull no tiene dockerd, no tiene containerd, no tiene arbol de procesos shim. Un CVE en la toolchain de contenedores no puede usarse para saltar entre tenants a traves de un daemon compartido corriendo como root — no hay daemon compartido. Cada hull run termina apenas forkea el workload del tenant. |
| NEWUSER --rootless | Cada tenant puede correr con --rootless, asi incluso el uid 0 dentro del contenedor se mapea a un uid del host no privilegiado distinto por tenant. Aun si un atacante compromete completamente un sandbox, la identidad del host sobre la que cae no puede tocar el directorio de datos de ningun otro tenant. |
El driver sandbox reduce el blast radius de 90 tenants a 1. Hull ademas reduce la profundidad de lo que ese tenant comprometido puede hacer — una victima de SQL injection no puede trivialmente convertirse en un container escape completo, y un container escape completo no puede trivialmente convertirse en movimiento lateral.
Configuracion de Ejemplo
# Cada tenant tiene su propia definicion de servicio
services:
- name: zyght-codelco
replicas: 1
driver: sandbox
config:
base: node22
rootfs: /opt/mentat/sandboxes/zyght-codelco/rootfs
command: /usr/local/bin/node
args: ["server.js"]
env:
- TENANT_ID=codelco
# Credenciales DB inyectadas via mt secret set
memory_mb: 512
cpu_percent: 25
max_pids: 64
port: 3001
health_check:
path: /health
interval_secs: 10
ingress:
domain: codelco.zyght.com
path: /
scale:
min: 1
max: 3
metric: cpu
scale_up_at: 70
scale_down_at: 20
cooldown_secs: 30
# Secrets configurados aparte (nunca en YAML)
# mt secret set ZYGHT_CODELCO_DB_URL "postgres://..."
# mt secret set ZYGHT_SQM_DB_URL "postgres://..."
# Cada sandbox solo recibe su propio secretLimitaciones Honestas
Mentat es un orquestador de infraestructura, no un WAF ni una herramienta de seguridad aplicativa. Provee la segunda linea de defensa:
- Primera linea (fuera del scope de Mentat): validacion de inputs, autenticacion, autorizacion, cifrado en reposo, reglas WAF. Estas previenen el compromiso inicial.
- Segunda linea (Mentat): aislamiento por tenant, reduccion de blast radius, scoping de credenciales, deteccion de anomalias via cgroups. Estas limitan el dano cuando la primera linea falla.
Si la aplicacion misma tiene una vulnerabilidad de SQL injection, Mentat no puede evitar que el atacante use la conexion legitima de la app a su DB. Pero el atacante solo alcanza los datos de un tenant en vez de los 90.
La Leccion
La brecha de Zyght no es una historia sobre un firewall faltante o un CVE sin parchear. Es una historia sobre limites de confianza arquitectonicos. Cuando 90 operadores de infraestructura critica comparten una aplicacion y una base de datos, el blast radius de cualquier vulnerabilidad es total. El aislamiento sandbox-por-tenant hace que esa arquitectura sea fisicamente imposible — el kernel impone lo que la logica de la aplicacion fallo en proteger.