Volver a la wiki

Servicio ssh_host_keys: TOFU server-side para conexiones SSH

Descripción

Servicio que implementa Trust-On-First-Use (TOFU) por organización para las conexiones SSH server-side que ScrapliManager establece hacia dispositivos de red. Cierra el hallazgo de seguridad Auditoría Suprema Etapa 3, network A9/sa1 + M3/sa2: antes, ScrapliManager conectaba con auth_strict_key=False, deshabilitando completamente la verificación de identidad del host → un atacante en el camino podía suplantar el dispositivo y capturar credenciales.

Ubicación y cobertura

Diseño

TOFU por organización

Dos tenants distintos pueden tener legitimately la misma IP RFC1918 para dispositivos diferentes. Por eso:

Nunca reduce conectividad

Refleja el mismo principio que el Agent (terminal/agent/network/host_keys.py, s124):

Funciones públicas

scrapli_strict_args(org_id, host: str, port: int = 22) -> dict

Firma: Async, retorna dict de kwargs para ScrapliManager.connect.

Lógica:

  1. Si org_id es falsy → retorna {"auth_strict_key": False} (sin tenant disponible, comportamiento antiguo)
  2. Si el host+puerto está registrado en el known_hosts de org_id:
    • Retorna {"auth_strict_key": True, "ssh_known_hosts_file": "<path>"} (stricto)
  3. Si no está registrado:
    • Intenta captar la clave con _fetch_key(host, port) (timeout 10s)
    • Si se captura: la registra con _record(...) y retorna dict stricto
    • Si falla: retorna dict no-stricto (degrada, no bloquea)

Datos de entrada (origen):

Sub-componentes privados

_store_path(org_id) -> Path

Retorna MEDIA_ROOT/ssh_known_hosts/<org_id>/known_hosts.

_is_recorded(path, host, port) -> bool

Lee el archivo y busca si ya existe una línea para el host (con el token SSH estándar: host o [host]:port si puerto != 22).

_fetch_key(host, port) -> key_object

Usa asyncssh.get_server_host_key() para captar la clave pública del servidor (timeout 10s). Retorna None si falla.

_record(path, host, port, key) -> None

Exporta la clave en formato OpenSSH y la añade al archivo (con escritura thread-safe mediante lock). Crea el directorio si no existe.

_token(host, port) -> str

Genera el primer campo del known_hosts: host (puerto 22) o [host]:port (otro puerto).

Integración

Modificaciones en ScrapliManager (network/services/scrapli_manager.py)

Propagación de org_id

Casos de test

TestEscenario
test_no_org_means_no_checkingSin org_id → no-check (comportamiento antiguo)
test_tofu_records_first_time_and_enforcesPrimera conexión: captura clave, registra, retorna stricto
test_recorded_host_is_not_refetchedHost ya registrado: no refetch, retorna stricto sin llamar a _fetch_key
test_fetch_failure_degrades_to_no_checkNo hay registro y no se puede captar → degrada a no-check
test_per_org_isolationRFC1918 IP en 2 orgs → almacenes separados
test_non_standard_port_tokenPuerto ≠ 22 → token con [host]:port

Referencias en el código

Véase también

Subir