fix(ssh): verify the host key netOrk expects, report the one seen (NetOrk/netork#1167)
CI / test (3.10) (push) Successful in 54s
CI / test (3.11) (push) Successful in 40s
CI / test (3.12) (push) Successful in 51s
CI / test (3.10) (pull_request) Successful in 42s
CI / test (3.11) (pull_request) Successful in 35s
CI / test (3.12) (pull_request) Successful in 49s

The driver accepted any SSH host key. It now verifies through
napalm-device-types' HostKeyCheck (>= 4.19.0): with a known key in
optional_args["ssh_host_keys"] any other key is refused before login, and
every key seen is reported to optional_args["ssh_host_key_observer"].
Without either argument it connects as before (first use).
This commit is contained in:
Christian Manivong committed 2026-10-18 21:23:46 +02:00
1 parent d134ccbb89
commit 6bb3035857
6 files changed
+179 -56

No files matched your search

+34 -23
View File
@@ -48,6 +48,7 @@ from napalm_device_types import (
SystemdServicesMixin,
)
from napalm.base.exceptions import ConnectionException
from napalm_device_types.ssh_host_keys import HostKeyCheck, SshHostKeyMismatch
try:
from proxmoxer import ProxmoxAPI
@@ -318,39 +319,49 @@ class ProxmoxDriver(
# Fallback: direct paramiko SSH
try:
import paramiko
if self._ssh_client is None:
ssh_user = self._ssh_username or self.username
ssh_pass = self._ssh_password or self.password
ssh_pkey = None
if self._ssh_key and not ssh_pass:
from io import StringIO as _StringIO
ssh_pkey = paramiko.RSAKey.from_private_key(_StringIO(self._ssh_key))
self._ssh_client = paramiko.SSHClient()
self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
connect_kwargs: _JsonDict = {
"hostname": self.hostname,
"port": 22,
"username": ssh_user,
"timeout": self.timeout,
}
if ssh_pkey:
connect_kwargs["pkey"] = ssh_pkey
else:
connect_kwargs["password"] = ssh_pass
self._ssh_client.connect(**connect_kwargs)
_, stdout, stderr = self._ssh_client.exec_command(command, timeout=self.timeout)
_, stdout, stderr = self._node_ssh().exec_command(command, timeout=self.timeout)
err = stderr.read().decode().strip()
out = stdout.read().decode().strip()
return out or err
except ImportError:
logger.warning("paramiko not installed — cannot exec SSH commands")
except SshHostKeyMismatch as exc:
# Refused before any login, and already reported to the caller's store.
logger.warning("%s", exc)
except Exception as exc:
logger.debug("SSH exec command failed: %s", exc)
return ""
def _node_ssh(self) -> Any: # paramiko.SSHClient; paramiko is imported lazily
"""The SSH session to the node on port 22, opened once.
Its host key is the one the caller expects for port 22, or on first use
whichever the node presents; either way it is reported (netork#1167).
"""
import paramiko
if self._ssh_client is None:
ssh_pass = self._ssh_password or self.password
connect_kwargs: _JsonDict = {
"username": self._ssh_username or self.username,
"timeout": self.timeout,
}
if self._ssh_key and not ssh_pass:
from io import StringIO as _StringIO
connect_kwargs["pkey"] = paramiko.RSAKey.from_private_key(_StringIO(self._ssh_key))
else:
connect_kwargs["password"] = ssh_pass
client = paramiko.SSHClient()
try:
HostKeyCheck(self.optional_args, self.hostname, 22).connect(client, **connect_kwargs)
except Exception:
client.close()
raise
self._ssh_client = client
return self._ssh_client
# ------------------------------------------------------------------ #
# Node info helpers
# ------------------------------------------------------------------ #