Volver a la wiki

Servicio host-keys del Agente — SSH TOFU anti-MITM (terminal/agent/network/host_keys.py)

Ubicación

Ruta: terminal/agent/network/host_keys.py
Módulo Python: terminal.agent.network.host_keys
Estado: ✅ Activo desde s124 (2026-06-10)

Propósito

Implementa Trust-On-First-Use (TOFU) para SSH, detectando y previniendo ataques MITM (Man-in-the-Middle) en conexiones a dispositivos de red. Registra la clave pública SSH de cada dispositivo en la primera conexión y rechaza cambios en identidad en conexiones posteriores.

Contexto del problema (R4)

Antes (s123): El Agent conectaba con known_hosts=None y auth_strict_key=False — la verificación de identidad del servidor SSH estaba desactivada. Un atacante en la LAN del cliente podía:

  1. Interponer un proxy SSH falso.
  2. Interceptar credenciales o comandos.
  3. Inyectar configuración maliciosa.

Solución: TOFU — la primera conexión es de confianza (ya que el cliente debe confiar en el dispositivo para crear el target); conexiones posteriores verifican identidad.

Interfaz pública

Función register_host_key(hostname: str, key: str | bytes) -> bool

Firma:

def register_host_key(hostname: str, key: str | bytes) -> bool

Propósito: Registra la clave pública de un dispositivo en el fichero known_hosts.

Entrada:

Salida:

Comportamiento:

Función verify_host_key(hostname: str, key: str | bytes) -> bool

Firma:

def verify_host_key(hostname: str, key: str | bytes) -> bool

Propósito: Verifica que la clave de un dispositivo coincide con la registrada.

Entrada:

Salida:

Comportamiento:

Función get_known_hosts_path() -> Path

Firma:

def get_known_hosts_path() -> Path

Propósito: Retorna la ruta del fichero known_hosts.

Ubicación:

Formato de known_hosts: OpenSSH estándar (compatible con asyncssh y Scrapli):

192.168.1.100 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMz...
192.168.1.101 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5NzS...

Integración en el sistema

network/ssh.py — SSHBridge

En SSHBridge.connect():

# Primera conexión: TOFU
if not host_keys.verify_host_key(hostname, received_key):
    if not host_keys.register_host_key(hostname, received_key):
        raise HostKeyError(f"Cannot register key for {hostname}")

# Conexiones posteriores: verificación estricta
if not host_keys.verify_host_key(hostname, received_key):
    raise HostKeyError(f"Host key mismatch for {hostname}")

routes/network.py — Endpoint /network/ssh-show

Llama a SSHBridge.connect() que internamente usa host_keys.py. Si hay mismatch:

Flujo completo

  1. Agent inicia: carga known_hosts en memoria (diccionario hostname → key).
  2. SaaS crea target (ej. 192.168.1.100): Agent no sabe la clave aún.
  3. Usuario hace SSH (click en /network/ssh-show):
    • Agent intenta conectar a 192.168.1.100.
    • Recibe la clave pública del servidor SSH.
    • Verifica en known_hosts: no está.
    • TOFU: registra la clave (asume que la LAN es segura en la 1ª conexión).
    • Conexión exitosa.
  4. Día siguiente: usuario vuelve a hacer SSH:
    • Agent recibe clave de 192.168.1.100.
    • Verifica en known_hosts: sí existe, coincide.
    • Conexión exitosa.
  5. Ataque MITM simulado: atacante interpone proxy:
    • Agent recibe clave diferente de 192.168.1.100.
    • Verifica en known_hosts: existe, NO coincide.
    • Rechaza conexión → endpoint retorna 401 “Host key conflict”.

Detalles de implementación

Almacenamiento

Fichero: known_hosts (formato OpenSSH estándar).

# Estructura por línea:
hostname key_type key_base64

Ejemplo:

192.168.1.100 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMz...

Compatibilidad:

Parsing

def _parse_known_hosts_line(line: str) -> tuple[str, str] | None:
    """Extrae hostname y key_base64 de una línea."""
    parts = line.strip().split()
    if len(parts) >= 3:
        return (parts[0], " ".join(parts[1:]))
    return None

Concurrencia

Logging

Nunca logea el contenido de las claves (seguridad por defecto).

Degradación segura (fallback)

Escenario: primera conexión a un dispositivo, pero asyncssh.get_server_host_key() falla (ej. dispositivo no responde a key probe).

Comportamiento:

Criterio actual: fallback TOFU (nunca reduce conectividad, pero logea una advertencia).

Testing

Notas de operación

Limitaciones conocidas

Véase también

Subir