CreaRack-SL

Auditoría Suprema · network sub-área 4 (Devices/Backups/Groups) — Seguridad de credenciales y configuraciones

Resumen

Cuarta tanda de la Auditoría Suprema del módulo network (sesión 115). Enfoque en la seguridad de credenciales, backups de configuración y grupos de dispositivos. Se cerraron 30 hallazgos confirmados (4 ALTA, 14 MEDIA, 12 BAJA) mediante unificación de gates de autorización y validaciones defensivas.

Sin cambios visibles para usuarios administradores. Es endurecimiento de permisos (readonly ya no ve secretos) + honestidad de la interfaz (restore_backup ya no finge éxito).

Hallazgos ALTA (4)

ALTA #1: Fuga de credenciales descifradas (get_device_credentials)

Problema: El endpoint /api/network/device/{device_id}/credentials devolvía usuario/contraseña SSH + enable en claro a cualquier usuario de la org, incluso con rol readonly (que tiene permiso network:view por defecto).

Impacto: Un usuario de solo lectura podía obtener las credenciales SSH vivas de cualquier dispositivo y conectarse directamente.

Fix: Gateado con require_perm(request, "network", "edit"). Paralelo al fix anterior en core (/credentials/{id}/decrypt — sa1).

Fuentes:

  • network/api/devices.py::get_device_credentials — línea 115
  • tests/api/test_network_sa4.py::TestDeviceCredentialsGate — cubre readonly 403, operator 200

ALTA #2: Server-Side Request Forgery en force_check_device

Problema: El health check (POST /api/network/device/{device_id}/check) abría un socket socket TCP desde el servidor hacia cualquier IP en la config, sin validar que no fuera una dirección interna (RFC1918, loopback, metadata). Un atacante podía forzar escaneos de servicios internos.

Impacto: Exposición de servicios privados, pivoting potencial.

Fixes:

  1. Gate require_perm(request, "network", "edit") (antes sin autenticación clara).
  2. Guard is_private_ip(ip_address) — rechaza loopback, RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), CGNAT (100.64.0.0/10).
  3. Validación de puerto: int(port) y rango 1–65535 (antes aceptaba “500” como string).
  4. Timestamps aware (timezone.now() en lugar de datetime.now()).

Fuentes:

  • network/api/devices.py::force_check_device — líneas 276–328
  • network/services/__init__.py — función is_private_ip (importada)
  • tests/api/test_network_sa4.py::TestForceCheckDevice — cubre SSRF guard, port validation, readonly 403

ALTA #3: Lectura de configuraciones con secretos en claro

Problema: Tres endpoints permitían descargar/ver configuraciones en texto plano (incluyendo communities SNMP, enable secrets, material criptográfico):

  • view_backup (GET /api/network/backup/{backup_id}/view)
  • compare_backups (POST /api/network/backup/compare)
  • download_backup (GET /api/network/backup/{backup_id}/download)

Un usuario readonly (que tiene network:view por defecto) podía descargar la configuración completa.

Matiz importante: list_backups (solo metadatos, sin config_text) SÍ se queda en view — permite al readonly ver que existen backups.

Fixes:

  • Todos los tres endpoints -> require_perm(request, "network", "edit").
  • Anti-DoS en compare_backups: cap de 2 MB por lado antes de difflib.
  • Sanitización en download_backup: filename limpio (header injection prevention) + re.sub(r"[^A-Za-z0-9._-]", "_", ...).

Fuentes:

  • network/api/backups.py::view_backup — línea 75
  • network/api/backups.py::compare_backups — línea 101
  • network/api/backups.py::download_backup — línea 354
  • tests/api/test_network_sa4.py::TestBackupReadGate — cubre readonly 403 en view/download, readonly 200 en list

ALTA #4: restore_backup finge éxito (Reglas 13/15)

Problema: El endpoint /api/network/backup/{backup_id}/restore respondía 200 {success: true, "restore initiated"} sin restaurar nada. Era un placeholder. Violaba Reglas 13 (no mentir sobre estado) y 15 (fallos explícitos).

Fix: Ahora devuelve 501 Not Implemented tras validar existencia/propiedad del backup. El mensaje es honesto: “Configuration restore is not implemented yet”.

Fuentes:

  • network/api/backups.py::restore_backup — línea 299
  • tests/api/test_network_sa4.py::TestRestoreHonest — verifica 501 admin, 403 operator

Hallazgos MEDIA (14)

  • Mutaciones de backup (trigger_backup, save_backup, delete_backup): unified a require_perm(..., "admin").
  • update_device_status / update_devices_status_batch: gateadas a require_perm(..., "edit") (antes sin gate claro).
  • list_device_groups (antes sin gate): ahora require_perm(..., "view") + optimización N+1 (annotate(Count distinct) → una query en lugar de 3 COUNTs por grupo).
  • create_device_group / assign_devices_to_groups / delete_device_group: unificados a require_perm (antes mezclaba is_admin, request.user.role directo, sin-gate). Delete sigue siendo admin.
  • Schema DeviceGroupInputSchema: name y color acotados a max_length (100, 20 respectivamente) → evita DataError 500, genera 422 limpio.
  • Manejo de errores: str(exc) retirado de handlers → mensajes genéricos (no revela detalles internos). Códigos 404/500 normalizados (declarados en decorator Ninja).
  • save_backup anti-DoS: cap 5 MB en config_text.
  • log_network_action: guard en level → fallback a “INFO” si es inválido.

Hallazgos BAJA (12)

  • Error handling en get_device_credentials: logger.exception() en lugar de str(e).
  • AttributeError 500 con Agent-JWT/anónimo en device_groups.py → ahora blocked por gate.
  • Response schemas en decoradores Ninja completas (incluye 404).
  • Timestamps: timezone.now() aware en lugar de datetime.now().
  • Validaciones de entrada: puerto no negativo, config_text > 0 y < límite.

Cambios en files

network/api/backups.py

  • Importa require_perm (nuevo de core.utils).
  • Todos los endpoints listados en ALTA #3 + mutaciones gateadas.
  • Error responses genéricas (no str(e)).

network/api/devices.py

  • Importa require_perm, is_private_ip.
  • get_device_credentials: gate a edit.
  • force_check_device: gate + SSRF guard + port validation.
  • Varios endpoints (status, network-config) ahora gateados.

network/api/device_groups.py

  • Importa require_perm.
  • Unificación de gates: list=view, create/assign=edit, delete=admin.
  • Query optimizada: annotate(Count distinct) → una sola en lugar de N+1.

network/api/common.py

  • DeviceGroupInputSchema: fields con max_length.

network/utils.py

  • log_network_action: guard en level.

tests/api/test_network_sa4.py (nuevo)

  • 19 tests que cubren todos los hallazgos ALTA + MEDIA.
  • Fixtures: _device_with_config, _backup, _client_for.

Seguridad: Resumen de cambios de permisos

Antes (caótico):

  • get_device_credentials: sin gate efectivo (vulnerable).
  • force_check_device: sin gate.
  • Backup views: sin gate (vulnerable).
  • Device groups: mezcla de is_admin, request.user.role, sin-gate.

Después (unificado):

network:view   → list_* endpoints (metadata sin secretos)
network:edit   → lectura/mutación de credenciales + configs + device status
network:admin  → mutaciones de backup + delete_device_group + force_check_device

Relación con anteriores

  • ALTA #1 espeja el fix anterior en core (/credentials/{id}/decrypt — sa1).
  • ALTA #2 implementa anti-SSRF análogo al de UPS (monitoring sa5).
  • ALTA #3 es análogo a la protección de config_text en general.
  • ALTA #4 cierra una brecha flagrante de honestidad (Regla 13).

Pendientes (deferred a PRs propias)

  • Cifrado en reposo SNMPv3 (Plan Hardening E).
  • DeviceProfileOut.snmp_community (A4 de sa1 — exposición).
  • asyncio → Huey async en trigger_backup.

Véase también

  • [[concept—security—credential-exposure]]
  • [[concept—security—ssrf-prevention]]
  • [[concept—saas—multi-tenancy]]
  • [[decision—20260609—require-perm-unified-authorization]]
  • [[entity—network—model—device]]