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:
- Atacante se coloca en la red (ARP spoofing, BGP hijacking, etc.)
- Cuando CreaRack intenta conectar al switch
10.0.0.5:22, el atacante responde - Sin verificación, CreaRack cree que habla con el switch → envía credenciales SSH
- 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:
- Si
org_ides falsy → retorna{"auth_strict_key": False}(sin tenant) - Si clave ya registrada → retorna
{"auth_strict_key": True, "ssh_known_hosts_file": "<path>"} - 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)
- Intenta captar con
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(endpointrun_script): obtiene org del request → pasa aexecute_script(..., org_id=org.id)backups.py(tareafetch_config): obtiene org del device → pasa aconnect(..., org_id=device.organization_id)ssh_reader.py(lectura de puertos): anotado conorg_id=None(a propagar después)
Tests
6 tests en tests/api/test_network_ssh_tofu.py:
- Sin org: no-check (compatibilidad)
- Primera conexión: captura y registra
- Conexión repetida: usa registro, no refetch
- Captura falla: degrada
- Per-org isolation: dos tenants, dos almacenes
- 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]]