Incident: tareas Huey (purga, integridad, monitor de conexiones) corrían sin RLS — inertes en PROD
⚠️ Revisión del 04-09-2026 (s301): el diagnóstico de abajo era una INFERENCIA y resultó falso en PROD
Medido el 04-09 en los contenedores db-1 de PROD y de STAGE (SELECT rolconfig FROM pg_roles WHERE rolname='crearack_app'): el rol con el que conecta la aplicación lleva app.current_org_id=0 como valor por defecto de rol desde el runbook 1g de RLS (julio 2026, AUDIT.md §7-1g: “bypass declarado por defecto de rol; el middleware web lo restringe por request con SET LOCAL”). Toda conexión nueva de un worker de Huey nace con el GUC en '0' = bypass, no en NULL. Prueba del efecto: monitoring_aiinsight tiene 175 filas expired entre el 24-07 y el 01-09, escritas por la tarea Huey expire_stale_insights, que no lleva ningún envoltorio. Las tareas Huey escribían y leían con normalidad.
Lo que sigue siendo cierto: rls_bypass() es la forma explícita correcta de declarar que un código de sistema opera sobre todas las organizaciones, y protege si algún día se retira el default del rol (el endurecimiento que la Auditoría Suprema 2 pedía: GUC por organización en cada tarea + quitar el default). Es defensivo, no un arreglo de algo roto. El resto de esta página se conserva como registro de lo que se creyó el 03-09 y de por qué: la revisión en Opus leyó migraciones y middleware sin medir el rol en la base de datos. Antes de afirmar “inerte en PROD”, leer pg_roles.rolconfig. Dónde SÍ muerde el NULL: un rol sin ese default (la BD de test NOSUPERUSER de tests/test_rls.py, un rol nuevo creado a mano).
Resumen
La cola de auditoría (task #286, 14 hallazgos MEDIA) destapó que tres tareas periódicas de Huey — la purga de organizaciones borradas, la comprobación de integridad entre tenants y el aviso de saturación de conexiones a BD — llevaban corriendo en PROD/STAGE sin efecto real: el middleware que fija el contexto de Row Level Security solo actúa dentro de una petición HTTP, y un worker de Huey nunca pasa por él.
Severidad: MEDIA (no hay fuga de datos entre organizaciones; el fallo es que ciertas tareas de mantenimiento no hacían nada).
Estado: corregido en el mismo commit que lo detectó — sin incidente abierto en producción ni remediación pendiente.
Cuándo
Detectado y corregido el 2026-09-03 (sesión s300), en la revisión de la cola de auditoría (task #286). No se conoce con precisión desde cuándo estaban activas así las tres tareas — llevaban tiempo corriendo antes de esta revisión.
Síntomas visibles
Ninguno directamente visible: las tres tareas corrían, no lanzaban excepción y dejaban log de “éxito” (logger.info), pero:
purge_deleted_organizations(cron 4:00 AM) no purgaba ninguna organización con más de 90 días borrada.check_tenant_integrity(cron 4:30 AM) nunca reportaba anomalías cross-tenant, aunque las hubiera.monitor_db_connectionsno dejaba traza enSystemLogcuando el uso de conexiones a BD superaba el 80%.
Causa raíz
core.middleware.tenant_rls.TenantRLSMiddleware fija app.current_org_id con SET LOCAL en cada petición HTTP. Las tablas protegidas tienen FORCE ROW LEVEL SECURITY, así que sin ese GUC las consultas devuelven 0 filas y los INSERT se rechazan (WITH CHECK). Un worker de Huey ejecuta código fuera de cualquier request — nunca pasa por el middleware — así que el GUC llegaba NULL a las tres tareas.
Fix aplicado
Commit d14ebb58 (PR #494, v1.103.0). Nuevo core/utils/rls.py con el context manager rls_bypass(): abre una transacción y ejecuta SET LOCAL app.current_org_id = '0' — el mismo valor que el middleware usa para superusers — antes de las consultas que deben ver todas las organizaciones. Se aplica en core/tasks.py:
purge_deleted_organizations— cada organización se purga dentro derls_bypass()(función extraída_purge_one_org); además ahora deja unSystemLog(category=SYSTEM,action=organization.purge) ANTES del borrado — antes solo quedaba unlogger.infoque desaparece con el contenedor, y la purga es irreversible (CASCADE+rmtreedel directorio de backups).check_tenant_integrity— la comprobación completa entra enrls_bypass(); de paso se reescribió de iterar en Python sobre todas las filas a dos querysetsF()resueltos en BD y acotados a 200 resultados cada uno (y se eliminó una comprobación de código muerto:Devicesinrack.organization, imposible porqueRack.organizationesNOT NULL).monitor_db_connections— elSystemLog.objects.create()de aviso de saturación entra enrls_bypass().
27 tests nuevos cubren las tres tareas.
Lecciones
- El único punto que fija el contexto de RLS es el middleware de request — cualquier código que corra fuera de una petición HTTP (Huey, management commands, shells) necesita fijarlo explícitamente o queda ciego a los datos.
- El fallo era completamente silencioso: sin excepción, sin log de error, con un
logger.infoque sugería éxito. Una tarea de mantenimiento que “no falla nunca” merece sospecha, no confianza.
Preventivos futuros
Cualquier tarea periódica NUEVA (Huey) que lea o escriba tablas protegidas por RLS debe envolver ese código en rls_bypass(), siguiendo el patrón de core/tasks.py. Distinto pero emparentado con [[incident—20260710—rls-gap-tablas-nuevas]]: aquel incidente era ausencia de POLÍTICA de RLS en tablas nuevas; este es ausencia de CONTEXTO de RLS en código que nunca pasa por el middleware.
Véase también
- [[decision—20260403—multi-tenancy-rls]]
- [[feature—core—rls-with-check]]
- [[incident—20260710—rls-gap-tablas-nuevas]]
- [[concept—general—patron-asyncjob-como-se-encola-un-trabajo-asincron]]
- [[entity—core—model—systemlog]]