Saltar al contenido
Volver a Insights
Arquitectura9 min de lectura

Aislamiento multi-tenant: por qué la seguridad a nivel de fila no es suficiente

Cuatro capas de aislamiento de inquilinos desde el gateway API hasta la base de datos. Defensa en profundidad para infraestructura financiera.

Aislamiento Multi-Tenant en Sistemas Financieros: Row-Level Security No Es Suficiente


Un desarrollador olvida una cláusula WHERE. El ORM genera una consulta sin scope. Un endpoint admin evita el filtro de tenant por "conveniencia operativa." Un bug, una consulta, y el Tenant A ve el historial de transacciones del Tenant B.

En un producto SaaS, esto es una fuga de datos. En un sistema financiero, es una violación regulatoria. PSD2 requiere protección de datos de pago. GDPR impone multas por persona afectada. DORA requiere que los sistemas TIC prevengan acceso no autorizado a datos financieros. Una sola fuga de datos cross-tenant activa los tres.

Row-Level Security (RLS) se supone que previene esto. La base de datos impone fronteras de tenant independientemente de lo que haga el código de aplicación. Pero RLS es la última línea de defensa, no la única. Responde "¿puede esta sesión de base de datos acceder a esta fila?", pero alguien debe establecer el contexto de sesión correctamente. Alguien debe asegurar que el ID de tenant que fluye por el sistema es auténtico, no suministrado por el llamador.

El aislamiento multi-tenant en sistemas financieros requiere tres capas. Cada capa captura fallos que las otras pasan por alto.

Capa 1: El Gateway

El API gateway se ubica entre la internet pública y la aplicación. Su trabajo: autenticar al llamador, determinar a qué tenant pertenece, e inyectar esa identidad como un header confiable.

La propiedad crítica: la aplicación nunca lee la identidad del tenant del body del request del llamador, query parameters, ni JWT claims que el llamador controla. El gateway valida el token OAuth2 (o API key), resuelve el tenant asociado, y establece un header HTTP, X-Tenant-ID, en el request interno. La aplicación lee el header. El llamador no puede falsificarlo.

Esto previene una categoría de ataque que el filtrado a nivel de aplicación no puede: un llamador comprometido o malicioso que envía un token de autenticación válido pero manipula el contexto de tenant. Si la aplicación lee tenant_id del body del request, un llamador con credenciales válidas puede pretender ser cualquier tenant. Si la aplicación lee X-Tenant-ID de un header inyectado por el gateway, la afirmación se verifica antes de que el request llegue al código de aplicación.

Implementación: middleware ForwardAuth de Traefik, plugins de Kong, o un gateway personalizado que llama a un servicio de auth. El servicio de auth valida el token, resuelve el tenant, y retorna el ID de tenant como header de respuesta. El gateway lo inyecta. La aplicación confía en él.

Capa 2: La Aplicación

Cada método de servicio recibe contexto de tenant del header del gateway. El contexto es inmutable por la duración del request. El servicio no puede construir una consulta para un tenant diferente, no porque esté prohibido por convención, sino porque la API no acepta ID de tenant como parámetro. Lo lee del header inyectado.

Esto elimina el problema de la cláusula WHERE. La aplicación no añade WHERE tenant_id = ? a cada consulta manualmente. El contexto de tenant se establece en la conexión de base de datos al inicio del request, y RLS (Capa 3) lo impone transparentemente.

Pero la capa de aplicación añade algo que la base de datos no puede: validación con scope de request. Antes de cualquier operación de escritura, el servicio verifica que los recursos siendo modificados pertenecen al tenant actual. ¿Una transferencia de Cuenta A a Cuenta B? Ambas cuentas deben pertenecer al tenant solicitante. ¿Una actualización de cliente? El cliente debe pertenecer al tenant solicitante. Estas verificaciones ocurren en código de aplicación, antes de tocar la base de datos.

¿Por qué no confiar solo en RLS? Porque RLS opera a nivel de fila. Puede prevenir leer filas de otro tenant. No puede prevenir operaciones semánticamente inválidas dentro de una sola consulta, por ejemplo, construir una transferencia donde la cuenta de débito y la cuenta de crédito pertenecen a tenants diferentes. Esa validación requiere lógica de aplicación.

Capa 3: La Base de Datos

Políticas de Row-Level Security de PostgreSQL en cada tabla que contiene datos de tenant. La política:

CREATE POLICY tenant_isolation ON finance_transfer
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

La aplicación establece app.current_tenant_id al inicio de cada conexión de base de datos (del header inyectado por el gateway). Cada consulta se filtra automáticamente. Olvidar una cláusula WHERE es irrelevante, la base de datos no retornará filas de otros tenants independientemente de la consulta.

El detalle clave: el rol de base de datos usado por la aplicación no puede evadir RLS. Solo el rol de migración (usado para cambios de esquema, nunca para consultas de aplicación) tiene permiso BYPASSRLS. Si el rol de aplicación pudiera evadir RLS, un solo bug de SET ROLE o inyección SQL derrotaría todo el modelo de aislamiento.

-- Rol de aplicación: RLS impuesto
CREATE ROLE finance_api NOINHERIT NOBYPASSRLS;

-- Rol de migración: RLS evadido (solo cambios de esquema, nunca usado por aplicación)
CREATE ROLE finance_admin BYPASSRLS;

Capa 4: El Ledger

Los sistemas financieros tienen una preocupación que las plataformas SaaS genéricas no tienen: el ledger debe imponer aislamiento de tenant independientemente de la base de datos relacional.

El motor de ledger opera sobre records de tamaño fijo con un campo user_data_128 que lleva información de tenant codificada. Una transferencia entre dos cuentas solo es válida si ambas cuentas comparten la misma codificación de tenant. El ledger rechaza transferencias cross-tenant a nivel de protocolo, antes de que la transferencia llegue al almacenamiento.

Esto no es redundante. Es defensa en profundidad. Considere el modo de fallo: un bug en la capa de aplicación construye una transferencia donde la cuenta de débito pertenece al Tenant A y la cuenta de crédito al Tenant B. La verificación de la capa de aplicación (Capa 2) debería capturarlo. Pero si no lo hace, una validación faltante, una race condition, un camino de código añadido por un nuevo desarrollador que no conocía la verificación, el motor de ledger rechaza la transferencia. El registro financiero nunca se corrompe.

AplicaciónBug: transfer(debit=TenantA:cuenta1, credit=TenantB:cuenta2)
Capa 2Verificación de aplicación: debería capturarPasado por alto (bug)
Capa 3Base de datos RLS: ¿ambas cuentas visibles para TenantA?Bloqueado
Capa 4Motor de ledger: cuentas tienen diferente codificación de tenantRechazado

Dos redes de seguridad independientes detrás de la aplicación. Cualquiera de las dos es suficiente para prevenir la corrupción. Ambas juntas la hacen estructuralmente imposible.

Propagación en Workflows

Los workflows multi-paso abarcan múltiples servicios. La creación de cuenta llama al servicio financiero, luego al servicio IBAN, luego al servicio KYC. Cada llamada debe llevar el contexto de tenant correcto.

El motor de ejecución durable propaga el ID de tenant en cada invocación de workflow. Cuando un workflow llama al servicio financiero, inyecta X-Tenant-ID en el header del request HTTP. El servicio financiero lo valida contra el valor inyectado por el gateway. Si un paso del workflow llama a un proveedor externo (KYC, screening AML), el contexto de tenant se usa para seleccionar la configuración correcta del proveedor, sin exponer el ID de tenant al sistema externo.

Si el motor de workflows no propaga contexto de tenant, las llamadas cross-service pueden ejecutarse en el scope de tenant equivocado. Un bug común, no hipotético, en sistemas multi-tenant que añaden orquestación de workflows como una ocurrencia tardía.

Cinco Preguntas para Evaluar Su Aislamiento

Respuestas binarias. Sin crédito parcial.

1. ¿Puede un llamador establecer su propia identidad de tenant? Si la aplicación lee tenant_id del body del request o de un JWT claim controlado por el llamador: sí. Su capa de gateway falta o está incompleta.

2. ¿Puede el código de aplicación construir una consulta para un tenant diferente? Si algún camino de código acepta tenant_id como parámetro de función en vez de leerlo del contexto del request: sí. Su capa de aplicación tiene una brecha.

3. ¿Tiene su rol de base de datos permisos para evadir RLS? Si la aplicación se conecta con un rol que tiene BYPASSRLS, SUPERUSER, o es dueño de las tablas: sí. Su capa de base de datos está rota. RLS se impone pero no tiene sentido.

4. ¿Puede una transferencia mover fondos entre cuentas de diferentes tenants? Si el ledger no verifica independientemente la propiedad de tenant de ambas cuentas en una transferencia: sí. Su capa de ledger falta.

5. ¿Su motor de workflows propaga contexto de tenant a través de fronteras de servicio? Si las llamadas cross-service dentro de un workflow no llevan contexto de tenant: no. Sus procesos multi-paso pueden fugar scope.

Cada "sí" a las preguntas 1-4 y "no" a la pregunta 5 es una brecha. En un sistema financiero, cada brecha es un hallazgo regulatorio potencial.


Leer más: El Ledger | Seguridad y Compliance


Fuentes:

  • DORA, Reglamento (UE) 2022/2554, Art. 9 (Protección y prevención de acceso no autorizado)
  • PSD2, Directiva 2015/2366, Art. 94 (Protección de datos personales)
  • GDPR, Reglamento 2016/679, Art. 32 (Seguridad del procesamiento)
  • Documentación de PostgreSQL: Row Security Policies (https://www.postgresql.org/docs/current/ddl-rowsecurity.html)