Tenencia con Seguridad a Nivel de Fila
Las plantillas de políticas por nivel de inquilino convierten la disciplina de tenant_id en aislamiento aplicado por la base de datos, una red de seguridad cuando los filtros de la aplicación fallan.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY invoices_tenant_select ON invoices
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY invoices_tenant_insert ON invoices
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
CREATE POLICY invoices_tenant_update ON invoices
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);Cuándo usar esto:
- SaaS multi-inquilino de esquema compartido con expectativas de cumplimiento (preparación para SOC2, HIPAA).
- Múltiples servicios consultan las mismas tablas con diferentes rutas de código.
- Necesitas defensa en profundidad más allá del alcance del ORM.
Ejemplo de Trabajo
BEGIN;
CREATE TABLE tenants (
tenant_id uuid PRIMARY KEY,
tier text NOT NULL CHECK (tier IN ('starter', 'business', 'enterprise'))
);
CREATE TABLE files (
tenant_id uuid NOT NULL REFERENCES tenants (tenant_id),
file_id bigint GENERATED ALWAYS AS IDENTITY,
name text NOT NULL,
PRIMARY KEY (tenant_id, file_id)
);
ALTER TABLE files ENABLE ROW LEVEL SECURITY;
ALTER TABLE files FORCE ROW LEVEL SECURITY;
-- Política estándar de aislamiento de inquilinos
CREATE POLICY files_tenant_isolation ON files
USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
-- El soporte de plataforma permite el acceso de solo lectura para tickets de nivel empresarial únicamente
CREATE ROLE support_reader;
CREATE POLICY files_support_read ON files
FOR SELECT
TO support_reader
USING (
EXISTS (
SELECT 1 FROM tenants t
WHERE t.tenant_id = files.tenant_id
AND t.tier = 'enterprise'
)
);
GRANT SELECT ON files TO support_reader;
GRANT support_reader TO support_agent;
COMMIT;Lo que esto demuestra:
FORCE ROW LEVEL SECURITYaplica políticas también a los propietarios de tablas.- Políticas separadas por comando (
SELECT,INSERT,UPDATE) cuando sea necesario. - Acceso de soporte específico del nivel a través de políticas dirigidas a roles.
Inmersión Profunda
Cómo Funciona
- RLS añade expresiones de política a cada plan de consulta como calificadores de filtro.
current_setting('app.tenant_id', true)devuelve NULL si no está establecido; la política debe fallar cerrando (sin filas).- El atributo
BYPASSRLSen el superusuario/propietario omite las políticas a menos que se establezcaFORCE. - Las políticas son por tabla: cada tabla de inquilino necesita cobertura; las lagunas son vulnerabilidades.
Plantillas de Políticas por Nivel
| Nivel | Plantilla |
|---|---|
| Starter/Business | Estricto tenant_id = app.tenant_id en todos los comandos |
| Enterprise + soporte | Política adicional de solo lectura para roles con auditoría |
| Administrador de plataforma | Rol separado, conexión separada, registro de auditoría completo |
Notas SQL
-- Verificar que RLS esté habilitado en todas las tablas de inquilinos
SELECT c.relname, c.relrowsecurity, c.relforcerowsecurity
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind = 'r'
AND c.relname NOT LIKE 'pg_%';Trampas
- RLS sin FORCE - el propietario de la tabla omite las políticas en las migraciones. Solución:
FORCE ROW LEVEL SECURITYen las tablas de datos de inquilinos. - Política faltante en tabla nueva - envío de funciones con fugas. Solución: lista de verificación de migración + consulta CI contra
pg_policies. - GUC obsoleto en conexión agrupada - inquilino incorrecto después de la reutilización. Solución:
SET app.tenant_iden cada solicitud; PgBouncerDISCARD ALL. - Herramientas BI de superusuario - Metabase se conecta como propietario, ve todos los inquilinos. Solución: rol dedicado de solo lectura sin bypass.
- Rendimiento de políticas - subconsulta compleja por fila. Solución: comparar
tenant_idcon GUC directamente; indexartenant_id.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Filtrado solo en la aplicación | Prototipos / herramientas internas | SaaS multi-inquilino en producción |
| Esquema por inquilino | Aislamiento de espacio de nombres más fuerte | Miles de esquemas |
| Vistas con security_barrier | Patrón heredado | RLS nativo es más claro en PG 18 |
| Distribución de inquilinos de Citus | Aislamiento a nivel de fragmento | PostgreSQL de nodo único estándar |
Preguntas Frecuentes
¿RLS ralentiza las consultas?
Las políticas simples de tenant_id = constante añaden una sobrecarga insignificante cuando tenant_id está indexado. Evita subconsultas por fila en las políticas.
¿Los JOIN respetan RLS?
Sí, se aplican las políticas de cada tabla. Asegúrate de que las claves de unión no puedan emparejar filas entre inquilinos (las claves compuestas ayudan).
¿Cómo se ejecutan las migraciones con RLS?
El rol de migración puede necesitar BYPASSRLS brevemente o deshabilitar RLS en una ventana de mantenimiento. Vuelve a habilitar y prueba las políticas antes del tráfico.
¿Puedo usar RLS con sentencias preparadas?
Sí. Establece GUC antes de ejecutar. Prueba con la agrupación de transacciones de PgBouncer: GUC debe establecerse dentro de la misma transacción.
¿Qué es el comportamiento de cierre por fallo?
app.tenant_id no establecido debe coincidir con cero filas, no con todas las filas. Prueba con RESET app.tenant_id en el conjunto de integración.
¿Cómo pruebo fugas entre inquilinos en CI?
Inserta dos inquilinos, establece GUC en el inquilino A, afirma que la consulta no puede ver las filas del inquilino B. Se requiere una prueba negativa por tabla.
¿Son suficientes las políticas INSERT?
Se necesita USING para SELECT/UPDATE/DELETE y WITH CHECK para INSERT/UPDATE. Los conjuntos de políticas incompletos filtran en algunos comandos.
¿RLS se aplica al superusuario?
El superusuario omite a menos que sea FORCE ROW LEVEL SECURITY. No ejecutes la aplicación como superusuario.
¿Pueden las políticas hacer referencia al usuario de la sesión?
Sí: USING (tenant_id = (SELECT tenant_id FROM users WHERE user_name = current_user)) - asegúrate de una búsqueda indexada.
¿Cómo se combinan las políticas específicas del nivel?
Múltiples políticas permisivas se combinan con OR para el mismo rol/comando. Documenta las combinaciones para evitar acceso amplio accidental.
Relacionado
- Esquema Compartido + tenant_id - diseño de columna e índice
- Roles y Permisos RLS - mecánica general de RLS
- Mejores Prácticas Multi-inquilino - casos negativos de CI
- Esquema por Inquilino - cuando RLS no es suficiente
Versiones de Stack: Esta página fue escrita para PostgreSQL 18.4 (estable 18, mantenimiento 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, y PostGIS 3.5+.