Saltar al contenido
EMR7

Arquitectura SaaS

Cómo diseñar un SaaS multitenant sin mezclar los datos de clientes

Modelos de aislamiento, resolución del tenant en el servidor, Row Level Security en PostgreSQL y pruebas para que un cliente nunca vea los datos de otro.

Por Eduardo MolinaPublicado el 5 min de lectura

En un SaaS multitenant, muchos clientes comparten la misma aplicación y, a menudo, la misma infraestructura. Es lo que permite operar a un costo razonable y desplegar una mejora para todos a la vez. Pero también crea el riesgo más grave que puede tener una plataforma de este tipo: que una empresa vea, modifique o borre los datos de otra.

Ese fallo rara vez llega por un ataque sofisticado. Lo habitual es una consulta a la que le falta un filtro, un identificador que se puede cambiar en la URL o una caché que comparte resultados entre clientes. Por eso el aislamiento no puede depender de que cada desarrollador recuerde añadir una condición: tiene que estar en la arquitectura.

Qué significa aislar a un tenant

Un tenant es la unidad que se aísla: normalmente una empresa o una organización con sus propios usuarios. Aislar significa que toda lectura y escritura queda limitada a los datos de ese tenant, en todas las capas: base de datos, archivos, cachés, colas, búsquedas, registros y exportaciones.

La pregunta de diseño no es solo «¿dónde guardo los datos de cada cliente?», sino «¿en qué punto exacto del sistema se decide a qué tenant pertenece cada operación, y qué impide que alguien se salte esa decisión?».

Tres modelos de almacenamiento

ModeloAislamientoCosto y operaciónCuándo encaja
Base de datos por tenantMuy altoAlto: migraciones y conexiones por clienteClientes grandes, requisitos regulatorios o contractuales
Esquema por tenantAltoMedio: migraciones repetidas por esquemaDecenas o cientos de clientes con necesidad de separación
Tablas compartidas con tenant_idDepende de los controlesBajo: una sola migraciónMuchos clientes pequeños y medianos

No hay un modelo correcto para todos. Muchos productos empiezan con tablas compartidas y ofrecen una base de datos dedicada a los clientes que la necesitan. Lo importante es que el código de la aplicación no dependa del modelo elegido: si la capa de acceso a datos recibe siempre un contexto de tenant, cambiar de estrategia más adelante es mucho menos doloroso.

El tenant se resuelve en el servidor

El tenant de una petición nunca debe venir de un campo que el cliente pueda manipular libremente, como un tenant_id en el cuerpo de la petición o un parámetro de la URL sin validar.

Lo seguro es derivarlo en el servidor a partir de algo confiable:

  • La sesión autenticada del usuario y su pertenencia verificada a una organización.
  • El subdominio o dominio propio del cliente, validado contra la base de datos.
  • Un token firmado cuyo contenido el servidor puede verificar.

Una vez resuelto, el tenant se convierte en parte del contexto de la petición y se pasa explícitamente a la capa de datos. Si un usuario pertenece a varias organizaciones, la organización activa se elige y se valida en el servidor en cada petición.

Defensa en profundidad con Row Level Security

Aunque toda la aplicación filtre por tenant_id, basta un olvido para provocar una fuga. PostgreSQL ofrece una segunda barrera: las políticas de Row Level Security (RLS), que hacen que la propia base de datos filtre las filas según el contexto de la sesión.

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.tenant_id')::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

La aplicación fija el tenant al inicio de cada transacción:

BEGIN;
SET LOCAL app.tenant_id = '6f1c…';
SELECT * FROM invoices; -- solo devuelve filas de ese tenant
COMMIT;

Algunos detalles importan mucho:

  • USING filtra lo que se puede leer; WITH CHECK impide escribir filas de otro tenant.
  • Los superusuarios y los roles con BYPASSRLS ignoran las políticas, y el dueño de la tabla también, salvo que se active FORCE ROW LEVEL SECURITY. La aplicación debe conectarse con un rol sin esos privilegios.
  • SET LOCAL limita el valor a la transacción. Con un pool de conexiones, un SET sin LOCAL puede quedar «pegado» a la conexión y filtrarse a la siguiente petición.

RLS no sustituye a los controles de la aplicación; los complementa. Si uno falla, el otro sigue en pie.

Cachés, archivos y colas también son datos

Las fugas entre clientes aparecen a menudo fuera de la base de datos:

  • Cachés: toda clave debe incluir el tenant (tenant:{id}:dashboard). Una caché compartida sin ese prefijo puede servir los datos de un cliente a otro.
  • Archivos: organiza el almacenamiento por tenant y genera URLs firmadas y temporales. Nunca expongas rutas predecibles.
  • Colas y tareas en segundo plano: cada trabajo debe llevar su tenant y volver a establecer el contexto al ejecutarse. Una tarea que procesa «todo» sin filtrar es una fuga esperando ocurrir.
  • Búsqueda y analítica: los índices de búsqueda y los informes agregados necesitan el mismo filtro que la base principal.
  • Registros: evita escribir datos personales de un cliente en registros que otros pueden consultar.

Identificadores y control de acceso a objetos

El control de acceso roto encabeza el OWASP Top 10, y en un SaaS su forma más común es la referencia directa insegura a objetos: cambiar /facturas/1041 por /facturas/1042 y ver la factura de otra empresa.

Usar identificadores no secuenciales, como UUID, dificulta adivinar recursos, pero no es un control de acceso. Lo que protege de verdad es que cada consulta compruebe la pertenencia al tenant, cosa que RLS hace de forma sistemática.

Pruebas que intentan cruzar la frontera

Las pruebas habituales verifican que un usuario ve sus datos. Las importantes verifican que no ve los de otros:

  1. Crea al menos dos tenants con datos parecidos en las pruebas automatizadas.
  2. Para cada endpoint, autentícate como el tenant A e intenta leer, modificar y borrar recursos del tenant B. El resultado esperado es un 404 o un 403, nunca datos.
  3. Ejecuta las pruebas de integración con el mismo rol de base de datos que usa producción, para que RLS se aplique.
  4. Revisa las tareas en segundo plano y las exportaciones con el mismo criterio.

Checklist

  • El tenant se deriva en el servidor a partir de una fuente confiable.
  • La capa de datos recibe siempre un contexto de tenant explícito.
  • Las tablas con datos de clientes tienen RLS activado, con USING y WITH CHECK.
  • La aplicación se conecta con un rol sin BYPASSRLS y que no es dueño de las tablas, o con FORCE ROW LEVEL SECURITY.
  • El contexto se fija con SET LOCAL dentro de cada transacción.
  • Las claves de caché, las rutas de archivos y los trabajos en cola incluyen el tenant.
  • Existen pruebas automatizadas que intentan acceder a datos de otro tenant.

Fuentes y referencias

  1. Row Security Policies — Documentación de PostgreSQL
  2. Multi Tenant Security Cheat Sheet — OWASP Cheat Sheet Series
  3. A01:2021 – Broken Access Control — OWASP Top 10
  4. Tenancy models for a multitenant solution — Microsoft Learn — Azure Architecture Center

Fuentes consultadas en la fecha de publicación.

Más artículos