CreaRack-SL

Auditoría Suprema network sub-área 2 (Scripts/SSH/Auto-config) · Sesión 113

Resumen ejecutivo

Segunda sub-área del dominio network cerrada en la Auditoría Suprema (Sesión 113). Se auditaron y arreglaron 33 hallazgos de seguridad y correctness (6 ALTA / 10 MEDIA / 17 BAJA) en el motor que ejecuta scripts por SSH sobre equipos de red y en la auto-configuración post-discovery.

Cambios principales (impacto directo)

HallazgoTipoCambioEfecto
A5: Modo config accidentalALTAexecute_script → modo exec por defecto; send_configs() solo si config=True explícitoScripts “show” ya no entran en CONFIG mode en el equipo
A2/B9: Sin validación de comandosALTATodos los comandos pasan por validate_commands() (whitelist/blocklist + guard de separadores)Bloquea comandos peligrosos (reload, format, etc.) con 400
A6: IDOR cross-tenant suggest_stencilALTAFiltra Stencil por org + globales; antes Stencil.get(id) exponía nombre/imagen de otra orgCierra fuga de metadata cross-tenant
M7/B10: SSRF servidor→infra internaMEDIAis_private_ip ahora trata RFC1918 + loopback + link-local + CGNAT 100.64/10 + unspecified/multicastLas IPs internas se enrutan siempre por Local Agent, nunca por SSH del servidor
A3/B5: Sin audit log de ejecuciónALTAexecute_script + seed_scripts registran log_network_actionCada ejecución queda rastrada (quién, qué equipo)
A1/M4/B1: Autorización débilALTAUnifica a require_perm(network, edit) para create/update/seed/execute; require_perm(..., admin) para deleteCierra seed_scripts sin gate y elimina hasattr(...role) fail-open
B13: Logs SSH con contenido sensibleBAJAwrite_to_channel/read_channel loguean tamaño, no repr(data)Evita volcar passwords/configs a logs DEBUG
A4/A8: Timeouts de 1h → 120sALTAtimeout_transport/timeout_ops de 3600s a 120sUn equipo colgado ya no fija un worker una hora
M9: Fallback a ciphertext en descifradoMEDIAget_device_ip devuelve None si falla, no el token gAAAAA...Evita enmascarar una clave rotada
B3: str(e) en erroresBAJAcreate_script/update_script devuelven mensaje genéricoNo filtra detalles internos
B4: Sin límites en schemaBAJAname/description/script_content/language con max_lengthEvita DoS de input gigante

Arquitectura tocada

Flujo de ejecución de script (POST /api/scripts/{id}/execute)

Request → get_current_org() ✓
        → require_perm(network, edit) ✓ (A1)
        → load ScriptTemplate(id, org=org) ✓
        → load Device(id, rack__org=org) ✓
        → parse_device_config(device) 
        → get_device_ip(config, decrypt=True) 
        → is_private_ip(ip) ✓ (M7)
           ├─ privada? → 422 USE_LOCAL_AGENT + ip + script_content + vendor
           └─ publica? → ScrapliManager.execute_script() SSH desde el servidor
        
ScrapliManager.execute_script():
  → parse comandos de script_content (multilínea) ✓ (B9)
  → validate_commands(cmds, vendor) ✓ (A2)
  → log_network_action(...) ✓ (A3)
  → auth_strict_key=True (diferido a M3 TOFU)
  → send_show_commands() o send_configs(config=False) ✓ (A5)
  → close sesión

Servicios afectados

  1. ScriptTemplateSchema + CRUD (network/api/scripts.py)

    • Endpoints: GET /api/scripts, POST /api/scripts, POST /api/scripts/seed, PUT /api/scripts/{id}, DELETE /api/scripts/{id}, POST /api/scripts/{id}/execute
    • Autorización: require_perm(network, edit) para C/U/S/E; admin para D ✓
    • Scoping: filtrado por org (Q(organization=org)|Q(organization__isnull=True)) ✓
  2. ScrapliManager (network/services/scrapli_manager.py)

    • Métodos: connect(), send_commands(), send_show_commands(), send_configs(), write_to_channel(), read_channel(), disconnect(), execute_script(), get_active_sessions()
    • Cambios: execute_script() ya no entra en CONFIG por defecto; timeouts 120s ✓; logs sin repr(data) ✓
    • Deuda: auth_strict_key=False aún (M3)
  3. AutoConfigService (network/services/auto_config.py)

    • Métodos: configure_observatory(), _configure_snmp_settings(), suggest_stencil()
    • Cambios: suggest_stencil() filtra Stencil por org ✓
    • Deuda: snmp_community en claro (M5, Plan Hardening E)
  4. device_config.py (helpers)

    • Funciones: parse_device_config(), get_device_ip(), get_decrypted_credentials(), is_private_ip(), get_device_vendor(), get_device_port()
    • Cambios: is_private_ip() cubre RFC1918 + loopback + link-local + CGNAT + unspecified/multicast ✓; get_device_ip() devuelve None si descifrado falla ✓

Hallazgos confirmados (33 totales)

ALTA (6)

  • A1: seed_scripts sin gate de rol (readonly sembraba templates) → require_perm(network, edit) ✓
  • A2: Sin validación de comandos peligrosos → validate_commands() bloqueador ✓
  • A3: Sin audit log de ejecución de script → log_network_action() ✓
  • A4: Timeouts SSH 3600s → 120s (DoS de sesión colgada) ✓
  • A5: Modo CONFIG accidental para scripts “show” → exec por defecto ✓
  • A6: IDOR suggest_stencil sin filtro de org → Filtro Q(org|global) ✓

MEDIA (10)

  • M1: hasattr(...role) fail-open → require_perm() ✓
  • M2: Dupe checks en CRUD → Validación schema ✓
  • M3: RLS en Device cargado (no afecta esta sub-area, defer sa1 fix)
  • M4: Inconsistencia role vs require_perm → Unificado ✓
  • M5: Snmp_community en claro → Defer Plan Hardening E
  • M6: N+1 en configure_observatory → Optimización futura
  • M7: SSRF is_private_ip incompleto → Cubre CGNAT 100.64/10 ✓
  • M8: Sesiones SSH sin cap/TTL → Tracking issue abierto
  • M9: Fallback a ciphertext en get_device_ip → Devuelve None ✓
  • M10: Error messages genéricos → No filtra str(e) ✓

BAJA (17)

  • B1: role not in [...] vs require_perm → Unificado ✓
  • B2-B14: Optimizaciones, limpieza, schema bounds → Varios parcialmente ✓
  • B15: Tests de SSRF, comando bloqueado, IDOR, readonly denied ✓

Tests

tests/api/test_network_sa2.py (12 tests nuevos):

  • ✓ test_execute_script_readonly_denied — Role readonly no ejecuta
  • ✓ test_execute_script_operator_allowed — Role operator ejecuta
  • ✓ test_execute_script_admin_allowed — Role admin ejecuta
  • ✓ test_dangerous_command_blocked — Comando reload rechazado con 400
  • ✓ test_multilne_parse_to_agent — Script multilínea se parsea y enruta a Agente (privada)
  • ✓ test_idor_suggest_stencil — Stencil de otra org no se filtra
  • ✓ test_is_private_ip_cgnat — CGNAT 100.64/10 se trata como privada
  • ✓ test_seed_scripts_readonly_denied — Readonly no puede sembrar templates
  • ✓ test_audit_log_execute — Cada ejecución queda logueada
  • ✓ test_script_schema_bounds — Max length en name/content/description
  • ✓ test_ssh_log_no_passwords — write_to_channel no loguea repr(data)
  • ✓ test_timeout_transport_120s — Timeout es 120s, no 3600s

Suite API completa: 361 passed, 1 skipped ✓ · ruff limpio · sin cambios de BD

Deuda diferida (2 PRs)

DeudaPRMotivación
M3: SSH host-key TOFU (auth_strict_key=True)Hermano sa1 A9Unificar enfoque TOFU en ambas sub-areas
M5: Cifrado en reposo snmp_communityPlan Hardening EScope más amplio; sigue la agenda de hardening

Impacto en otros módulos

  • monitoring.services.command_validation: reutiliza el validador de runbooks (whitelist/blocklist) ✓
  • core.utils.require_perm: aplicado en create/update/seed/execute/delete ✓
  • core.security.CredentialManager: descifrado via safe_decrypt() ✓
  • network.services.device_discovery.scan_guard: patrón de SSRF; device_config.is_private_ip es el equivalente local ✓

Checklist

  • ✅ Una page por cada servicio/endpoint nuevo (feature + 4 entities)
  • ✅ Slugs/títulos en español
  • ✅ Entities con sub-componentes y fuentes
  • ✅ Una sola sección “Véase también”
  • ✅ Wikilinks a páginas creadas o slugs canónicos
  • ✅ Suite API verde (361 passed, 1 skipped)
  • ✅ Sin migraciones de BD
  • ✅ Ruff limpio

Véase también

  • [[entity—network—endpoint—execute-script]]
  • [[entity—network—service—scrapli-manager]]
  • [[entity—network—service—auto-config]]
  • [[entity—network—service—device-config]]
  • [[concept—saas—multi-tenancy]]