CreaRack-SL

Mega-auditoría B-16 (27-09-2026): un Agente secundario recibía las credenciales SNMP de todos los equipos

Cuándo

27-09-2026, PR #621, commit 0d9a9f3185ac413c121b8cdacdde0abf0a3053ae (release v1.164.4). Mega-auditoría B-16 (tarea #371), preparada el 25-09-2026 y aplicada con el GO de Edu el 27-09-2026.

Síntomas visibles

GET /api/agent/targets/<org> — el camino REST por el que el Agente local (desde su versión 2.28) pide la lista de equipos a vigilar — devolvía la comunidad SNMP, las claves SNMPv3 y la URL HTTP ya descifradas a CUALQUIER Agente de la organización, también a los secundarios. El canal WebSocket ya restringía esa misma lista al Agente principal; el camino REST no tenía la comprobación equivalente.

Causa raíz

La pregunta “¿es este Agente el principal?” vivía duplicada: el WebSocket la resolvía leyendo AgentInstance.role directamente en AgentConsumer._current_role, y el endpoint REST no la hacía en absoluto. Al no depender los dos canales de una única función, bastó con que se abriera un segundo camino de acceso (el REST) para que la comprobación quedara sin aplicar ahí.

Fix aplicado

  • terminal/services.py: nueva función agent_role(agent_id), fuente única de “¿es el principal?”, leída de la base de datos en cada llamada (nunca de caché, porque un promote/demote no obliga al Agente a reconectar).
  • terminal/api/sentinel.py::get_agent_targets: si agent_role(...) != "primary", responde {"targets": [], "count": 0, "reason": "not_primary"}, sin la marca authoritative. Se descartaron un 403 y una lista con los secretos en blanco: ambas habrían hecho que el Agente guardara una comunidad vacía y sondeara con “public”; esta forma concreta de vacío la interpretan los Agentes 2.28/2.29 como “conserva tu lista”.
  • terminal/consumers.py::AgentConsumer._current_role: pasa a llamar a la misma agent_role() en vez de leer el modelo por su cuenta.
  • terminal/api/fleet.py::_send_role_change_bg: efecto colateral encontrado de paso — el aviso de promoción salía ANTES de que la transacción de la petición (el middleware de RLS envuelve toda petición en una) confirmara el cambio de rol. Ahora arranca con transaction.on_commit(...).
  • Test dedicado: tests/api/test_mega25_r8seg_agente_targets.py. Cinco tests existentes cambiaron su montaje (creaban el Agente con el rol por defecto “secondary” y ahora lo crean “primary”) sin tocar lo que comprueban.

Lecciones

  • Cuando dos canales (aquí, WebSocket y REST) exponen el mismo dato sensible, la comprobación de autorización debe vivir en una función compartida — no en dos implementaciones que pueden divergir con el tiempo.
  • Ante un cliente que pierde autorización para algo que antes tenía, ni el error explícito (403) ni el vacío “normal” son seguros por sí mismos: hay que mirar cómo interpreta el cliente cada forma de respuesta y elegir la que no le haga descartar lo que ya tenía a cambio de nada.

Preventivos futuros

El test tests/api/test_mega25_r8seg_agente_targets.py fija el comportamiento nuevo. Límite honesto: lo que un Agente secundario guardó en su base local ANTES de este fix se queda ahí — el servidor no puede borrarlo — y si ese secundario arranca su vigilancia por su cuenta, sigue usando esa lista vieja. En producción no hay hoy ningún Agente secundario (3 Agentes, los 3 principales), así que el escenario no se ha dado en real. Sin probar en PROD más allá de CI.

Véase también

  • [[incident—20260925—mega-auditoria-ronda-6-plataforma-csrf-websocket]]
  • [[incident—20260924—mega-auditoria-t02-usuarios-privilegios-y-rastro]]
  • [[entity—terminal—endpoint—receive-agent-alert]]