CreaRack-SL

Feature: SSH host-key TOFU server-side (Auditoría Suprema Etapa 3)

Resumen ejecutivo

El servidor de CreaRack ahora verifica la identidad de los equipos de red cuando se conecta por SSH, igual que el Agente local ya hacía. Cierra un agujero de seguridad real: un atacante en la red podía suplantar un switch/router y capturar las credenciales SSH. Implementación: TOFU (Trust-On-First-Use) por organización, sin modelo Django, con degradación segura si no se puede captar la clave.

Impacto: ninguno visible para el usuario final — conexiones funcionan igual, pero ahora protegidas.

Antecedentes y hallazgo (Auditoría Suprema, Etapa 3)

Hallazgo: Network A9/sa1 + M3/sa2

ScrapliManager (driver SSH) conectaba con auth_strict_key=False, deshabilitando completamente la verificación de identidad del host. Esto permitía un ataque de hombre en el medio (MITM) en el camino SaaS → dispositivo:

  1. Atacante se coloca en la red (ARP spoofing, BGP hijacking, etc.)
  2. Cuando CreaRack intenta conectar al switch 10.0.0.5:22, el atacante responde
  3. Sin verificación, CreaRack cree que habla con el switch → envía credenciales SSH
  4. Atacante captura credenciales y puede:
    • Aceder a los dispositivos con esas credenciales
    • Inyectar comandos maliciosos en backups/scripts
    • Escalar a toda la red del datacenter

Severidad: MEDIA-ALTA en un contexto SaaS multi-tenant donde el servidor se conecta a equipos de clientes.

Solución

Diseño: TOFU por organización

Trust-On-First-Use (TOFU): la primera conexión a un host es de confianza; registras su “huella digital” (clave pública) y, en adelante, rechazas cualquier otra huella.

Por organización: dos tenants distintos pueden tener la misma IP RFC1918 (p.ej. ambos usan 192.168.1.1 para switches diferentes en redes privadas distintas). Por eso el almacén debe estar aislado por org_id:

MEDIA_ROOT/
└── ssh_known_hosts/
    ├── 1/  (tenant A)
    │   └── known_hosts
    ├── 2/  (tenant B)
    │   └── known_hosts
    └── ...

Nunca reduce conectividad: si es la primera conexión y no se puede captar la clave (fallo de red, timeout, servidor que no responde), degrada a no-check — eso es mejor que bloquear todas las nuevas conexiones. MITM protection solo aplica cuando ya hay una clave registrada.

Implementación

Nuevo servicio: network/services/ssh_host_keys.py

Función pública:

async def scrapli_strict_args(org_id, host: str, port: int = 22) -> dict:
    """Retorna kwargs para ScrapliManager.connect() que habilitan TOFU verification."""

Flujo:

  1. Si org_id es falsy → retorna {"auth_strict_key": False} (sin tenant)
  2. Si clave ya registrada → retorna {"auth_strict_key": True, "ssh_known_hosts_file": "<path>"}
  3. Si no registrada:
    • Intenta captar con asyncssh.get_server_host_key() (timeout 10s)
    • Si éxito → registra y retorna stricto
    • Si fallo → retorna no-stricto (degrada)

Modificaciones en ScrapliManager

async def connect(..., org_id=None):
    # ...
    strict_args = await scrapli_strict_args(org_id, host, port)
    conn = driver_class(
        # ...
        **strict_args,  # auth_strict_key ± ssh_known_hosts_file
        # ...
    )

Propagación de org_id

  • scripts.py (endpoint run_script): obtiene org del request → pasa a execute_script(..., org_id=org.id)
  • backups.py (tarea fetch_config): obtiene org del device → pasa a connect(..., org_id=device.organization_id)
  • ssh_reader.py (lectura de puertos): anotado con org_id=None (a propagar después)

Tests

6 tests en tests/api/test_network_ssh_tofu.py:

  1. Sin org: no-check (compatibilidad)
  2. Primera conexión: captura y registra
  3. Conexión repetida: usa registro, no refetch
  4. Captura falla: degrada
  5. Per-org isolation: dos tenants, dos almacenes
  6. Puerto no-estándar: token correcto con [host]:port

Documento de decisión (recomendado)

Haber elegido TOFU con degradación (vs bloqueo riguroso) es una decisión de seguridad/usabilidad que merece documentarse en decision--20260611--ssh-tofu-auditoria-suprema-a9-m3.

Cambios visibles en el proyecto

CHANGELOG.md

## [Unreleased] - 2026-06-11 (Sesión 127 · Auditoría Suprema Etapa 3)

### Security
- **Verificación de host-key (TOFU) en las conexiones SSH server-side**
  Cierra MITM en camino SaaS→dispositivo (network A9/M3).
  TOFU por organización, sin modelo/migración, degrada a no-check si falla captura.

RELEASE_NOTES.md

## s127 (2026-06-11)
**Ámbito**: producto (backend · seguridad)

### Lo que cambia
El servidor ahora comprueba la identidad de los equipos de red por SSH.
Memoriza la "huella digital" en la primera conexión y rechaza cambios.
Nunca reduce conectividad: si es la primera vez y no consigue la huella, se conecta igual.

### Detalle técnico
`network/services/ssh_host_keys.py`: TOFU por org, almacén under MEDIA_ROOT,
sin DB model. Propagado en `scripts.py` (org del request) y `backups.py` (org del device).

Véase también

  • [[entity—network—service—ssh-host-keys]]
  • [[entity—network—service—scrapli-manager]]
  • [[decision—20260611—ssh-tofu-auditoria-suprema-a9-m3]]
  • [[concept—network—ssh-host-verification]]
  • [[concept—security—mitm-prevention]]