CreaRack-SL

Auditoría Suprema · Monitoring sub-área 2 (protocolos/probes SNMP·ping·HTTP) — fixes s107

Contexto

Quinta tanda de la Auditoría Suprema (ver [[concept—onboarding—mapa-maestro-ecosistema-crearack]]) y segunda sub-área de monitoring (tras [[feature—monitoring—auditoria-sa1-s106]]). Cubre los probes / protocolos: los servicios que sondean la red desde el servidor — SNMP, ping ICMP, HTTP — más la correlación/agrupación de resultados (~1.3k LOC en 6 ficheros).

Resultado: 39 crudos → 37 dedup → 36 confirmados (4 ALTA / 17 MEDIA / 15 BAJA), 1 rechazado por verificación adversarial. 67 agentes, 3.73M tokens. Datos: auditoria-suprema/monitoring-sa2.{md,json}. PR #64→#65 (merge cd274098, CI verde, sin migraciones).

Decisión de Edu: “todo de una vez” (Regla 22, patrón [[feature—blueprints—auditoria-s105]] + sa1).

El hallazgo estrella: SSRF en los probes HTTP (ALTA)

HttpService.check / check_async / _check_ssl_cert conectaban a la URL recibida sin validar el destino. El endpoint /targets/{id}/http/check (operations.py) comprobaba is_private_ip(target.ip_address) pero después probaba config["http_url"] — un campo distinto, controlado por el usuario. Vector: target con ip_address público (pasa el guard) + http_url=http://169.254.169.254/latest/meta-data/ (metadata cloud), http://127.0.0.1:8428/ (VictoriaMetrics), o servicios internos por hostname Docker (http://cache:6379/, http://db:5432/). batch.py ni siquiera tenía el guard parcial. follow_redirects=True permitía además SSRF vía 302 a destino interno.

Modelo de amenaza confirmado: los probes server-side son para targets públicos — los dispositivos privados del cliente se monitorizan vía el Local Agent, no desde el servidor (de ahí que ping/HTTP/SNMP-poll ya saltaran is_private_ip). Por tanto un probe server-side hacia RFC1918/loopback/link-local es SSRF contra la propia infra, no monitoreo legítimo.

Fix — nuevo guard SSRF compartido monitoring/services/net_guard.py:

  • validate_destination_url(url) / validate_destination_host(host, port) → (is_safe, reason).
  • Rechaza esquema ≠ http/https, hostnames bloqueados (localhost, metadata*), y resuelve el host (getaddrinfo) rechazando si CUALQUIER IP resuelta cae en loopback (127/8, ::1), link-local/metadata (169.254/16, fe80::/10), RFC1918 (10/8, 172.16/12, 192.168/16), CGNAT/overlay NetBird (100.64/10), 0.0.0.0/8 o ULA IPv6 (fc00::/7).
  • Aplicado en la capa de servicio (check/check_async/_check_ssl_cert) → protege a todos los callers, incluido el batch.py.

El proyecto ya tenía un guard equivalente para webhooks ITSM (notification_service.validate_webhook_url); net_guard lo generaliza para los probes (unificar ambos queda como carve-out de sub-área 4).

Resto de ALTA

  1. Endpoints SNMP sin gate — snmp/test, snmp/interfaces, snmp/poll (monitoring/api/snmp.py) no aplicaban require_perm → cualquier autenticado (incl. readonly) disparaba SNMP. Añadido require_perm(request, "observatory", "edit") a los 3. (Misma raíz Broken Access Control que sa1/blueprints; helper de s105.)
  2. Escaneo SNMP de red interna — snmp/test y snmp/interfaces no tenían el guard is_private_ip que sí tiene snmp/poll → permitían sondear IPs internas arbitrarias. Añadido (mismo criterio: privados vía Agent).
  3. Inyección PromQL en grupos — group_service.query_group_metric interpolaba metric en el nombre de métrica (snmp_extras_<metric>) y tenant_id/target_ids en los selectores de la query VM sin sanear → lectura cross-tenant (misma clase que el fix de sa1 en metrics_reader, en otro fichero). Validados contra allowlist _VALID_METRIC_NAME + numéricos; entrada inválida → {"data": []} + log.

Fixes correctness / fuga de info

  • snmp/poll 200-mintiendo → devolvía {"status": "error"} (valor fuera de las STATUS_CHOICES) ante fallo SNMP. Ahora "down" (estado válido). (Heredado de sa1, verificado y cerrado aquí.)
  • DoS de memoria en HTTP probe → descargaba el body completo sin límite. Ahora lectura en streaming con cap de 2 MB + timing con time.monotonic() + datetime aware (sin utcnow() deprecado) + el campo error ya no expone str(exc) interno.
  • except Exception mudos en snmp_service.py (get_single/walk/get_interface_traffic) → ahora logger.debug; test_connection no devuelve str(exc) al cliente.
  • snmp_auth.py → el log DEBUG ya no incluye el username SNMP v3.

Tests

15 nuevos en tests/api/test_monitoring.py: gate de permisos en los 3 probes SNMP (readonly→403, admin→skip private sin red), guard SSRF (net_guard + http_service.check bloquea metadata/loopback/RFC1918/esquema y permite IP pública), inyección PromQL en query_group_metric. 253 verde en tests/api/ (1 skip preexistente). Sin migraciones. ruff limpio.

Backlog Etapa 3 (carve-outs Regla 22)

  • Probes síncronos dentro del request ASGI (SNMP walks ~30s, ping, HTTP) → async/Huey + polling (perf L, toca contrato frontend).
  • Cifrado en reposo de credenciales SNMP v3 (authKey/privKey/community en el config dict) → toca el modelo de credenciales (alinear con CredentialManager). Ya no se serializan al cliente (sa1) ni se loguean (sa2); falta el reposo.
  • IP-pinning anti-DNS-rebinding en net_guard (hoy resuelve+bloquea, como el guard de webhooks; pinnear la IP validada en el transport httpx cierra el TOCTOU).
  • Defensa-en-profundidad en ping_service (el caller ya guarda con is_private_ip).
  • validate_webhook_url (ITSM, sub-área 4) podría delegar en net_guard (evita drift de dos blocklists).
  • correlation_service (subnet por troceo de string rompe IPv6/CIDR, N+1 en match_known_issue) · group_service (ThreadPoolExecutor por request).

Relacionado

  • [[feature—monitoring—auditoria-sa1-s106]] — sub-área 1, mismo dominio.
  • [[feature—blueprints—auditoria-s105]] — dominio anterior, mismo patrón.
  • Versión coloquial: [[concept—producto—observatory-mas-seguro]].
  • Doc maestro: auditoria-suprema/AUDITORIA_SUPREMA.md (repo workspace).

Véase también

  • [[feature—monitoring—auditoria-sa1-s106]]
  • [[concept—producto—observatory-mas-seguro]]