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.

arquitectura tipica tenant compartido
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 archivos

Mentat: 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.

arquitectura sandbox-por-tenant
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.1TB

Comparacion de Impacto

MetricaTenancy Compartida (Zyght)Sandbox-por-Tenant (Mentat)
Datos expuestos6.1TB — los 90+ tenants~68GB — solo un tenant
Archivos expuestos19 millones~210K (proporcional)
Organizaciones afectadas90+1
Movimiento lateralTrivial — espacio de procesos compartidoBloqueado — aislamiento PID + mount namespace
Alcance de credencialesCredenciales master de DB sirven a todos los tenantsCada sandbox tiene solo sus propias credenciales
Deteccion de exfiltracion6.1TB por la red — dificil de no notar pero no fue detectadoDeteccion de anomalias cgroups v2 por sandbox (memoria + CPU)
RecuperacionRotar credenciales para 90+ orgs, notificar a todos, asumir compromiso totalRotar credenciales para 1 org, incidente contenido

Capas de Defensa

Capa 1Sandbox por tenantCada organizacion corre en su propio namespace. Sin espacio de procesos compartido
Capa 2Credenciales aisladasCada sandbox recibe solo su propia cadena de conexion DB via Mentat secrets. Sin credenciales master
Capa 3pivot_rootAtacante dentro del sandbox no puede leer filesystem del host, otros sandboxes, ni estado de Mentat
Capa 4Monitoreo cgroups v2Transferencia anomala de datos (6.1TB) dispara alertas de ScaleWatcher. Memoria y CPU rastreados por sandbox
Capa 5Namespace de redCLONE_NEWNET + veth restringe egress solo al endpoint de DB del tenant. Sin conexiones salientes arbitrarias
Capa 6Base inmutableCodigo 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 tenantCada 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 tenantAllowlist 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 tenantCon 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 compartidoHull 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 --rootlessCada 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

zyght-tenant.yaml
# 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 secret

Limitaciones 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.