Auditoría de seguridad multi-tenant — 2026-08-31
Alcance: API Commerce, PostgreSQL/RLS, frontend y contrato interno con
TimeLiber Connect.
Estado: hallazgos P0/P1 corregidos, cubiertos en PostgreSQL efímero y
desplegados en la base persistente como 0005_tenant_hardening.
Resultado
La aplicación ya verificaba sesión, membresía y capability antes de acceder a
integraciones. La auditoría encontró brechas en las barreras secundarias: una
política de inserción de membresías demasiado amplia, relaciones que no
garantizaban el mismo organization_id, caché de navegador persistente entre
cuentas y una prueba RLS cuyo teardown dependía de un downgrade irreversible.
Correcciones
Identidad y RLS
- Crear una organización establece primero un contexto UUID nuevo y usa ese mismo UUID para organización y membresía propietaria.
- La política de membresía sólo permite insertar al usuario actual como
ownerdentro de ese contexto de creación. - Una membresía con
deleted_atya no concede visibilidad a la organización. - Sin contexto, las políticas continúan fallando cerradas.
Integridad relacional
integration_connectionsexpone la clave candidata(id, organization_id).- Instalaciones Shopify y receipts usan foreign keys compuestas
(connection_id, organization_id). - Intents y conexiones exigen una membresía
(organization_id, user_id)existente para el usuario asociado.
Estas constraints impiden grafting entre tenants incluso si un repository futuro olvida comprobar ambos lados de la relación.
Frontera del navegador
Al establecer o terminar una sesión se eliminan todas las queries no-auth de TanStack Query. De esta forma una segunda cuenta en el mismo navegador no puede recibir temporalmente organizaciones, conexiones o pedidos de la cuenta anterior desde memoria local.
Frontera Commerce ↔ conector
El JWT para lectura de pedidos incluye organization_id, shop y el subject
exacto shopify-orders-read. TimeLiber Connect compara el dominio firmado con
el body antes de abrir la sesión Shopify. El endpoint de linking exige el
subject shopify-installation-link.
Evidencia automatizada
tests/integration/test_tenant_rls.py levanta PostgreSQL 17 desechable, migra a
head y comprueba:
- lectura sólo del tenant activo;
- membresía eliminada sin visibilidad;
- rechazo de insert dirigido a otro tenant;
- rechazo de autoafiliación como owner a otra empresa;
- rechazo de webhook que referencia una conexión de otro tenant;
- creación válida de organización + owner mediante el repository real;
- imposibilidad de desactivar RLS;
- roles sin
SUPERUSERniBYPASSRLS.
El contenedor efímero se destruye al terminar; no se ejecuta downgrade de
0004, porque esa migración es intencionalmente irreversible.
Riesgo residual
- Los JWT internos usan secreto HMAC compartido. La evolución recomendada sigue siendo workload identity o mTLS y replay cache para operaciones mutantes.
userses identidad global, no tabla tenant. El runtime conserva grants para registro/autenticación; no debe añadirse una ruta de listado global.- Cualquier futura gestión de equipo necesita políticas y pruebas específicas; la política actual sólo cubre la creación del primer owner.
Gate
Ningún nuevo dominio comercial se acepta si carece de:
- filtro explícito
organization_iden repository; - RLS
ENABLE/FORCE; - claves relacionales que preserven el tenant en ambos lados;
- prueba de lectura, escritura y relación cross-tenant sobre PostgreSQL real;
- limpieza de caché cuando cambia la identidad autenticada.