Row-Level Security (RLS) permite controlar qué filas puede consultar o modificar cada usuario de PostgreSQL. En esta guía crearás una tabla compartida por dos tenants, aplicarás políticas de lectura y escritura y comprobarás el aislamiento mediante pruebas positivas y negativas.

El laboratorio utiliza roles sin inicio de sesión y SET LOCAL ROLE. No necesita contraseñas ni conexiones externas.

Resultado esperado: dw_tenant_a solo podrá trabajar con las filas identificadas como dw_tenant_a; dw_tenant_b tendrá el mismo aislamiento para sus datos.

Qué resuelve Row-Level Security

Los privilegios concedidos mediante GRANT determinan si un rol puede consultar o modificar una tabla. RLS añade una segunda comprobación: determina con qué filas puede realizar esas operaciones.

Los controles se aplican en este orden:

Identidad → GRANT → política RLS → filas autorizadas

RLS reduce el impacto de una consulta que olvida incluir el filtro del tenant, pero no sustituye:

  • La autenticación.
  • Los privilegios de objetos.
  • El cifrado de las conexiones.
  • Las pruebas de autorización de la aplicación.
  • La separación entre roles propietarios y roles de ejecución.
Límite del laboratorio: el patrón de un rol de PostgreSQL por tenant facilita la demostración. Una aplicación que utiliza un único usuario de conexión necesita establecer el tenant mediante un mecanismo confiable, resistente a suplantación y limitado a cada transacción.
Los roles tenant_a y tenant_b atraviesan primero el control GRANT y después RLS; ambos usan la tabla facturas, pero cada rol alcanza únicamente las filas identificadas con su tenant.

Qué debes saber antes de activar RLS

  • Cuando RLS está habilitado y no existe una política aplicable, PostgreSQL aplica denegación predeterminada.
  • Los superusuarios y los roles con el atributo BYPASSRLS siempre omiten las políticas.
  • El propietario de una tabla normalmente omite RLS.
  • FORCE ROW LEVEL SECURITY permite someter al propietario a las políticas durante el acceso normal.
  • Solo el propietario puede habilitar o deshabilitar RLS y administrar las políticas de una tabla.
  • RLS complementa los permisos concedidos mediante GRANT; no los reemplaza.
  • RLS controla SELECT, INSERT, UPDATE y DELETE.
  • TRUNCATE y REFERENCES no están sujetos a las políticas de fila.
  • Las políticas permisivas se combinan mediante OR.
  • Las políticas restrictivas se combinan mediante AND, pero necesitan al menos una política permisiva que conceda acceso.

Diferencia entre USING y WITH CHECK

Una política puede contener dos expresiones con funciones diferentes.

USING

USING determina qué filas existentes son visibles o están disponibles para una operación.

Se utiliza en:

  • SELECT.
  • La selección de filas existentes para UPDATE.
  • La selección de filas para DELETE.

Si la expresión devuelve false o null, la fila normalmente se oculta sin producir un error.

WITH CHECK

WITH CHECK valida el nuevo contenido propuesto por:

  • INSERT.
  • El resultado de un UPDATE.

Si la expresión devuelve false o null, PostgreSQL rechaza la operación.

Un UPDATE puede atravesar ambos controles:

Fila existente → USING → fila modificada → WITH CHECK
SELECT usa USING para filtrar filas existentes; INSERT usa WITH CHECK para validar la fila propuesta; UPDATE aplica USING a la fila actual y después WITH CHECK a la fila nueva.

Necesitas:

  • Una base de datos de laboratorio en una versión compatible de PostgreSQL.
  • psql o un cliente SQL que preserve las transacciones y los mensajes de error.
  • Un rol capaz de crear roles, schemas, tablas y políticas.
  • Permiso para ejecutar SET ROLE hacia los roles de prueba.
  • Una base sin información sensible.

La sintaxis de esta guía fue revisada contra la documentación vigente de PostgreSQL 18.

Si vas a adaptar el procedimiento a una tabla real, registra previamente:

  • El propietario.
  • Los privilegios actuales.
  • Las políticas existentes.
  • El estado de RLS.
  • La versión de PostgreSQL.
  • Una copia de seguridad comprobada.
  • El procedimiento de reversión.

Paso 1: registrar la versión y la identidad administrativa

Conéctate a la base de laboratorio y ejecuta:

SELECT version();

SELECT
    current_database(),
    session_user,
    current_user;

SELECT current_setting('server_version');

Comprueba los atributos del rol conectado:

SELECT
    rolname,
    rolsuper,
    rolbypassrls
FROM pg_roles
WHERE rolname = current_user;

Para una tabla existente, registra también su propietario y el estado de RLS:

SELECT
    n.nspname AS schema_name,
    c.relname AS table_name,
    pg_get_userbyid(c.relowner) AS owner,
    c.relrowsecurity,
    c.relforcerowsecurity
FROM pg_class AS c
JOIN pg_namespace AS n
  ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
  AND c.relname = 'facturas';

Consulta las políticas existentes:

SELECT
    schemaname,
    tablename,
    policyname,
    permissive,
    roles,
    cmd,
    qual,
    with_check
FROM pg_policies
WHERE schemaname = 'public'
  AND tablename = 'facturas';

Un pg_dump --schema-only puede ayudarte a conservar la definición de una tabla para revisión, pero no sustituye una copia de datos ni una restauración ensayada.

Paso 2: crear los roles de prueba

Crea un rol de grupo para reunir los permisos de la aplicación:

CREATE ROLE dw_tenant_access NOLOGIN;

Crea dos identidades de tenant:

CREATE ROLE dw_tenant_a NOLOGIN;
CREATE ROLE dw_tenant_b NOLOGIN;

Concede la membresía del rol de acceso:

GRANT dw_tenant_access
TO dw_tenant_a, dw_tenant_b;

Permite que el usuario administrativo de esta sesión seleccione ambos roles durante la prueba:

GRANT dw_tenant_a, dw_tenant_b
TO CURRENT_USER;

Esta última concesión debe limitarse al ejecutor del laboratorio. En producción, no conviertas al propietario en miembro de los roles de aplicación sin una necesidad explícita.

Paso 3: crear la tabla compartida

Crea un schema aislado:

CREATE SCHEMA rls_demo;

Crea la tabla de facturas:

CREATE TABLE rls_demo.facturas (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id text NOT NULL,
    concepto text NOT NULL,
    total numeric(12,2) NOT NULL
        CHECK (total >= 0)
);

Agrega datos para ambos tenants:

INSERT INTO rls_demo.facturas
    (tenant_id, concepto, total)
VALUES
    ('dw_tenant_a', 'Plan A', 120.00),
    ('dw_tenant_a', 'Soporte A', 45.00),
    ('dw_tenant_b', 'Plan B', 90.00),
    ('dw_tenant_b', 'Soporte B', 35.00);

Comprueba la carga inicial:

SELECT
    tenant_id,
    count(*)
FROM rls_demo.facturas
GROUP BY tenant_id
ORDER BY tenant_id;

El resultado esperado es:

dw_tenant_a | 2
dw_tenant_b | 2

Paso 4: conceder los privilegios necesarios

Concede acceso al schema:

GRANT USAGE
ON SCHEMA rls_demo
TO dw_tenant_access;

Concede las operaciones que controlará RLS:

GRANT SELECT, INSERT, UPDATE, DELETE
ON rls_demo.facturas
TO dw_tenant_access;

Concede el permiso mínimo necesario sobre la secuencia de la columna identidad:

GRANT USAGE
ON SEQUENCE rls_demo.facturas_id_seq
TO dw_tenant_access;

En una tabla real, no asumas el nombre de la secuencia. Puedes localizarla con:

SELECT pg_get_serial_sequence(
    'rls_demo.facturas',
    'id'
);

RLS no concede permisos sobre el objeto. Si falta alguno de estos GRANT, PostgreSQL puede devolver permission denied antes de evaluar la política.

Paso 5: crear las políticas RLS

Crea las políticas y habilita RLS dentro de una transacción:

BEGIN;

Política de lectura

CREATE POLICY facturas_select_por_tenant
ON rls_demo.facturas
FOR SELECT
TO dw_tenant_access
USING (
    tenant_id = current_user::text
);

Política de inserción

CREATE POLICY facturas_insert_por_tenant
ON rls_demo.facturas
FOR INSERT
TO dw_tenant_access
WITH CHECK (
    tenant_id = current_user::text
);

Política de actualización

CREATE POLICY facturas_update_por_tenant
ON rls_demo.facturas
FOR UPDATE
TO dw_tenant_access
USING (
    tenant_id = current_user::text
)
WITH CHECK (
    tenant_id = current_user::text
);

Política de eliminación

CREATE POLICY facturas_delete_por_tenant
ON rls_demo.facturas
FOR DELETE
TO dw_tenant_access
USING (
    tenant_id = current_user::text
);

Habilita RLS:

ALTER TABLE rls_demo.facturas
ENABLE ROW LEVEL SECURITY;

Somete también al propietario durante el acceso normal:

ALTER TABLE rls_demo.facturas
FORCE ROW LEVEL SECURITY;

Confirma la transacción:

COMMIT;

FORCE ROW LEVEL SECURITY no afecta a superusuarios ni roles con BYPASSRLS. Por eso las pruebas deben ejecutarse como roles no propietarios.

Paso 6: inspeccionar la configuración aplicada

Consulta el estado de la tabla:

SELECT
    n.nspname AS schema_name,
    c.relname AS table_name,
    pg_get_userbyid(c.relowner) AS owner,
    c.relrowsecurity,
    c.relforcerowsecurity
FROM pg_class AS c
JOIN pg_namespace AS n
  ON n.oid = c.relnamespace
WHERE n.nspname = 'rls_demo'
  AND c.relname = 'facturas';

Los campos relrowsecurity y relforcerowsecurity deberían aparecer como true.

Consulta las políticas:

SELECT
    policyname,
    permissive,
    roles,
    cmd,
    qual,
    with_check
FROM pg_policies
WHERE schemaname = 'rls_demo'
  AND tablename = 'facturas'
ORDER BY policyname;

El resultado debería mostrar cuatro políticas aplicadas a dw_tenant_access.

Paso 7: probar la lectura como tenant A

Inicia una transacción y cambia la identidad efectiva:

BEGIN;

SET LOCAL ROLE dw_tenant_a;

Comprueba la identidad:

SELECT
    session_user,
    current_user;

session_user conserva el usuario que abrió la conexión. current_user debe ser dw_tenant_a.

Consulta la tabla:

SELECT
    id,
    tenant_id,
    concepto,
    total
FROM rls_demo.facturas
ORDER BY id;

dw_tenant_a debe ver únicamente sus dos filas.

Prueba una consulta explícita por el otro tenant:

SELECT *
FROM rls_demo.facturas
WHERE tenant_id = 'dw_tenant_b';

El resultado esperado es cero filas.

Finaliza la prueba:

ROLLBACK;

Paso 8: probar la lectura como tenant B

BEGIN;

SET LOCAL ROLE dw_tenant_b;

SELECT
    session_user,
    current_user;

SELECT
    id,
    tenant_id,
    concepto,
    total
FROM rls_demo.facturas
ORDER BY id;

ROLLBACK;

dw_tenant_b debe ver únicamente sus dos filas.

Paso 9: probar una inserción permitida

BEGIN;

SET LOCAL ROLE dw_tenant_a;

INSERT INTO rls_demo.facturas
    (tenant_id, concepto, total)
VALUES
    ('dw_tenant_a', 'Prueba permitida', 10.00)
RETURNING
    id,
    tenant_id,
    concepto,
    total;

ROLLBACK;

La inserción debe estar permitida porque el valor de tenant_id coincide con current_user.

La fila no persiste después de ROLLBACK. Sin embargo, la secuencia puede avanzar y dejar un hueco en los identificadores. Esto es normal: las secuencias de PostgreSQL no son transaccionales.

Paso 10: probar una inserción cruzada

BEGIN;

SET LOCAL ROLE dw_tenant_a;

INSERT INTO rls_demo.facturas
    (tenant_id, concepto, total)
VALUES
    ('dw_tenant_b', 'Prueba bloqueada', 10.00);

ROLLBACK;

PostgreSQL debe rechazar el INSERT porque la fila propuesta no supera WITH CHECK.

Después del error, la transacción permanece abortada hasta ejecutar ROLLBACK.

Paso 11: impedir que UPDATE cambie el tenant

Intenta mover una fila propia hacia el otro tenant:

BEGIN;

SET LOCAL ROLE dw_tenant_a;

UPDATE rls_demo.facturas
SET tenant_id = 'dw_tenant_b'
WHERE id = 1;

ROLLBACK;

La fila original supera USING, pero el nuevo valor no supera WITH CHECK. PostgreSQL debe rechazar la operación.

Intenta actualizar directamente filas ajenas:

BEGIN;

SET LOCAL ROLE dw_tenant_a;

UPDATE rls_demo.facturas
SET total = total + 1
WHERE tenant_id = 'dw_tenant_b';

ROLLBACK;

El resultado esperado es:

UPDATE 0

Las filas ajenas no son visibles como candidatas debido a USING.

Paso 12: probar DELETE

Comprueba que el tenant pueda eliminar una fila propia:

BEGIN;

SET LOCAL ROLE dw_tenant_a;

DELETE FROM rls_demo.facturas
WHERE id = 1
RETURNING id, tenant_id;

ROLLBACK;

La operación debe estar permitida.

Intenta eliminar filas del otro tenant:

BEGIN;

SET LOCAL ROLE dw_tenant_a;

DELETE FROM rls_demo.facturas
WHERE tenant_id = 'dw_tenant_b'
RETURNING id, tenant_id;

ROLLBACK;

El resultado esperado es:

DELETE 0

USING mantiene las filas ajenas fuera del conjunto disponible para eliminación.

Paso 13: repetir las escrituras como tenant B

Prueba una inserción válida:

BEGIN;

SET LOCAL ROLE dw_tenant_b;

INSERT INTO rls_demo.facturas
    (tenant_id, concepto, total)
VALUES
    ('dw_tenant_b', 'Prueba permitida B', 10.00)
RETURNING id, tenant_id;

ROLLBACK;

Prueba una inserción cruzada:

BEGIN;

SET LOCAL ROLE dw_tenant_b;

INSERT INTO rls_demo.facturas
    (tenant_id, concepto, total)
VALUES
    ('dw_tenant_a', 'Prueba bloqueada B', 10.00);

ROLLBACK;

La primera debe estar permitida y la segunda debe ser rechazada.

Matriz de aceptación

IdentidadOperaciónResultado esperado
dw_tenant_aSELECT de filas ADevuelve únicamente filas A
dw_tenant_aSELECT de filas BDevuelve cero filas
dw_tenant_aINSERT con tenant APermitido
dw_tenant_aINSERT con tenant BRechazado por WITH CHECK
dw_tenant_aMover A hacia tenant BRechazado por WITH CHECK
dw_tenant_aActualizar directamente BUPDATE 0 por USING
dw_tenant_aEliminar fila propiaPermitido
dw_tenant_aEliminar filas BDELETE 0 por USING
dw_tenant_bInserción propiaPermitida
dw_tenant_bInserción para ARechazada

Cómo contener una política incorrecta

Deshabilitar RLS mientras los roles conservan sus permisos puede exponer todas las filas. Ante una incidencia, revoca primero el acceso:

BEGIN;

REVOKE SELECT, INSERT, UPDATE, DELETE
ON rls_demo.facturas
FROM dw_tenant_access;

COMMIT;

Después corrige o elimina la política correspondiente:

ALTER POLICY facturas_select_por_tenant
ON rls_demo.facturas
USING (
    tenant_id = current_user::text
);

También puedes eliminar una política:

DROP POLICY facturas_select_por_tenant
ON rls_demo.facturas;

Ten presente que habilitar RLS sin una política aplicable provoca denegación predeterminada.

Restaurar el acceso después de corregir la política

Mantén el cambio, la restauración de privilegios y la comprobación dentro de una transacción:

BEGIN;

ALTER POLICY facturas_select_por_tenant
ON rls_demo.facturas
USING (
    tenant_id = current_user::text
);

GRANT SELECT, INSERT, UPDATE, DELETE
ON rls_demo.facturas
TO dw_tenant_access;

Valida como tenant A:

SET LOCAL ROLE dw_tenant_a;

SELECT
    tenant_id,
    count(*)
FROM rls_demo.facturas
GROUP BY tenant_id;

RESET ROLE;

Valida como tenant B:

SET LOCAL ROLE dw_tenant_b;

SELECT
    tenant_id,
    count(*)
FROM rls_demo.facturas
GROUP BY tenant_id;

RESET ROLE;

Si ambas consultas devuelven únicamente el tenant correspondiente, confirma:

COMMIT;

Si alguna comprobación falla:

ROLLBACK;

Configura el cliente con un comportamiento equivalente a ON_ERROR_STOP para impedir que continúe y confirme una transacción cuya validación produjo un error.

Cómo deshabilitar RLS deliberadamente

Solo si el objetivo explícito es volver al comportamiento sin políticas, y después de revisar los privilegios, ejecuta:

ALTER TABLE rls_demo.facturas
DISABLE ROW LEVEL SECURITY;

Esta orden no elimina las políticas. Las deja definidas, pero inactivas.

Si los roles conservan SELECT, UPDATE o DELETE, podrán actuar sobre todas las filas permitidas por sus privilegios de tabla.

Eliminar el laboratorio

Confirma que estás conectado a la base de pruebas. Después elimina el schema y los roles:

DROP SCHEMA rls_demo CASCADE;

DROP ROLE dw_tenant_a;
DROP ROLE dw_tenant_b;
DROP ROLE dw_tenant_access;

Esta limpieza está diseñada para el laboratorio recién creado. DROP ROLE puede fallar en un entorno reutilizado si esos roles poseen objetos o conservan privilegios sobre otros recursos.

Problemas frecuentes

La política existe, pero el usuario ve todas las filas

Comprueba la identidad efectiva:

SELECT session_user, current_user;

Revisa los atributos del rol:

SELECT
    rolname,
    rolsuper,
    rolbypassrls
FROM pg_roles
WHERE rolname = current_user;

Comprueba el propietario y el estado de RLS:

SELECT
    pg_get_userbyid(c.relowner) AS owner,
    c.relrowsecurity,
    c.relforcerowsecurity
FROM pg_class AS c
JOIN pg_namespace AS n
  ON n.oid = c.relnamespace
WHERE n.nspname = 'rls_demo'
  AND c.relname = 'facturas';

Un superusuario, un rol con BYPASSRLS o normalmente el propietario no es una identidad válida para comprobar el aislamiento.

El usuario recibe permission denied

RLS no concede acceso al objeto. Revisa:

  • USAGE sobre el schema.
  • Privilegios sobre la tabla.
  • Permisos sobre la secuencia.
  • Membresía en dw_tenant_access.
  • Permiso SET para cambiar al rol de prueba.

Después de habilitar RLS no aparece ninguna fila

Si no existe una política aplicable al rol y al comando, PostgreSQL aplica denegación predeterminada.

Consulta:

SELECT *
FROM pg_policies
WHERE schemaname = 'rls_demo'
  AND tablename = 'facturas';

Comprueba también current_user y las membresías del rol.

Una política nueva amplió el acceso

Las políticas permisivas se combinan mediante OR. Una política permisiva demasiado amplia puede conceder filas adicionales.

Las políticas restrictivas se agregan mediante AND, pero no conceden acceso por sí solas: debe existir al menos una política permisiva aplicable.

Una vista no respeta la identidad esperada

Por defecto, las relaciones subyacentes de una vista se comprueban con los permisos y las políticas del propietario de la vista.

En versiones que admiten security_invoker, puedes indicar:

CREATE VIEW vista_facturas
WITH (security_invoker = true)
AS
SELECT *
FROM rls_demo.facturas;

De esta forma se utilizan los permisos y las políticas del usuario que invoca la vista. Revisa siempre la versión, el propietario y los privilegios antes de exponer tablas con RLS mediante vistas.

Buenas prácticas para producción

  • Separa el rol propietario, el rol de migraciones y los roles de aplicación.
  • No concedas BYPASSRLS a identidades de aplicación.
  • No conectes la aplicación como superusuario.
  • Automatiza pruebas positivas y negativas para SELECT, INSERT, UPDATE y DELETE.
  • Revisa políticas, propietarios, membresías y privilegios en cada migración.
  • Indexa las columnas usadas por las políticas cuando lo justifique el patrón de consultas.
  • Evalúa los planes con datos representativos.
  • Evita subconsultas complejas en políticas sin analizar privilegios, concurrencia y costo.
  • Controla TRUNCATE y REFERENCES mediante privilegios de tabla.
  • Considera que restricciones UNIQUE, claves foráneas y mensajes de error pueden revelar la existencia de una fila.
  • Revisa cuidadosamente cualquier función SECURITY DEFINER.
  • Si utilizas un pool de conexiones, establece y limpia el contexto del tenant dentro de cada transacción.
  • Prueba la reutilización de conexiones, los errores y los rollbacks.
  • No confíes únicamente en FORCE ROW LEVEL SECURITY; prueba siempre como un rol no propietario.

Conclusión

Una configuración RLS no se valida consultando como propietario o superusuario. Debes demostrar que cada identidad no propietaria:

  • Ve únicamente sus filas.
  • No puede consultar filas ajenas.
  • Puede insertar dentro de su tenant.
  • No puede insertar para otro tenant.
  • No puede mover una fila hacia otro tenant.
  • No puede actualizar ni eliminar filas que no puede ver.

GRANT habilita la operación sobre la tabla, USING controla las filas existentes y WITH CHECK protege los valores nuevos.

El siguiente paso es integrar el modelo con la identidad real de la aplicación y ejecutar esta matriz en las migraciones y pruebas automatizadas. También debes revisar vistas, funciones SECURITY DEFINER, restricciones y pools de conexiones que puedan modificar el límite de confianza.


Cloud Serversby Donweb

Todo el poder de la nube a tus proyectos y aplicaciones.
Alojamiento ultra rápido, escalable y con alta disponibilidad.

  • Performance que te sorprenderá
  • Rápida escalabilidad y sin limitaciones
  • Arquitectura de alta disponibilidad
  • Soporte experto y ejecutivos de cuenta
  • Pagos en tu moneda y facturación local

Descubre la mejor solución de Cloud Hosting