"""Managed known_hosts synthesis for SSH clients. Instead of mounting a host-side ``known_hosts`` file, the backend can discover and persist host keys for configured SSH machines inside its own cache volume. This keeps strict host-key checking enabled without exposing a whole SSH configuration directory into the container. """ from __future__ import annotations import logging import socket from pathlib import Path from typing import Any import paramiko logger = logging.getLogger(__name__) def _host_alias(host: str, port: int) -> str: return host if int(port or 22) == 22 else f"[{host}]:{int(port or 22)}" def _fetch_server_key(host: str, port: int, timeout: int = 30) -> paramiko.PKey: sock = socket.create_connection((host, int(port or 22)), timeout=timeout) transport = paramiko.Transport(sock) try: transport.start_client(timeout=timeout) key = transport.get_remote_server_key() if key is None: raise RuntimeError(f"Unable to read SSH host key for {host}:{port}") return key except Exception as exc: raise RuntimeError( f"Unable to read SSH protocol banner from {host}:{port}. " "Confirm the host is running an SSH server on that port and is reachable from the backend." ) from exc finally: transport.close() sock.close() def has_known_host(host: str, port: int, known_hosts_path: Path) -> bool: """Return whether the given host/port is already present in known_hosts.""" if not host or not known_hosts_path.exists(): return False host_alias = _host_alias(host, port) host_keys = paramiko.HostKeys() host_keys.load(str(known_hosts_path)) return host_keys.lookup(host_alias) is not None def ensure_known_host(host: str, port: int, known_hosts_path: Path, *, strict: bool = True) -> bool: """Ensure a host key entry exists for the given host/port. Returns ``True`` when the file was changed. If ``strict`` is enabled and the existing key differs, a ``RuntimeError`` is raised instead of silently overwriting the entry. """ if not host: return False known_hosts_path.parent.mkdir(parents=True, exist_ok=True) host_key = _fetch_server_key(host, port) host_alias = _host_alias(host, port) host_keys = paramiko.HostKeys() if known_hosts_path.exists(): host_keys.load(str(known_hosts_path)) existing = host_keys.lookup(host_alias) key_type = host_key.get_name() if existing and key_type in existing: if existing[key_type].get_base64() == host_key.get_base64(): return False if strict: raise RuntimeError(f"SSH host key mismatch for {host_alias}") host_keys.add(host_alias, key_type, host_key) host_keys.save(str(known_hosts_path)) logger.info("Recorded SSH host key host=%s port=%s file=%s", host, port, known_hosts_path) return True def ensure_known_hosts_for_machines( machines: list[dict[str, Any]], known_hosts_path: Path, *, strict: bool = True, ) -> int: changed = 0 for machine in machines: if str(machine.get("mode") or "").lower() != "ssh": continue host = str(machine.get("host") or "").strip() port = int(machine.get("port") or 22) if not host: continue if ensure_known_host(host, port, known_hosts_path, strict=strict): changed += 1 return changed