From 6d8b08d30f6ffbd9b82cdd7805a63dabecfaeb1d Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Thu, 7 May 2026 15:43:22 +0200 Subject: [PATCH] fixes and improvements --- backend/README.md | 1 + backend/src/media_library_viewer_api/clients/ssh.py | 10 +++++++--- backend/src/media_library_viewer_api/dependencies.py | 3 --- backend/src/media_library_viewer_api/routers/tasks.py | 2 -- .../services/monitoring_actions.py | 2 ++ docs/REQUIREMENTS.md | 1 + 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/README.md b/backend/README.md index b314329..7413550 100644 --- a/backend/README.md +++ b/backend/README.md @@ -126,6 +126,7 @@ docker compose up --build 2. After the API is running, open the app, go to **Settings**, and add machine entries: - **Local**: monitors the API host itself without SSH. - **SSH**: monitors another machine using a host, username, and a private key pasted directly into the machine settings, with an optional passphrase. + - The first successful SSH connection uses trust-on-first-use: the backend records that machine's host key into its managed `known_hosts` file automatically, then continues verifying it strictly on later connects. 3. Open **Monitoring** to see one section per configured machine. Each section uses its own collector state, disk path, metrics queries, and recent action history, which are populated automatically by the backend poller. diff --git a/backend/src/media_library_viewer_api/clients/ssh.py b/backend/src/media_library_viewer_api/clients/ssh.py index 6154c37..f670e4a 100644 --- a/backend/src/media_library_viewer_api/clients/ssh.py +++ b/backend/src/media_library_viewer_api/clients/ssh.py @@ -20,6 +20,8 @@ from typing import Any import paramiko +from media_library_viewer_api.services.known_hosts import ensure_known_host + logger = logging.getLogger(__name__) @@ -64,12 +66,14 @@ class RemoteSSHClient: def connect(self) -> paramiko.SSHClient: """Create or reuse the Paramiko connection. - Unknown host keys are rejected. The application can synthesize a managed - known_hosts file under its cache directory so users do not need to mount - their local SSH directory into the container. + Unknown host keys are recorded on first contact in the managed + known_hosts file when one is configured. After that, strict checking + remains in effect so host key changes are still rejected. """ if self._client: return self._client + if self.known_hosts_path: + ensure_known_host(self.host, self.port, Path(self.known_hosts_path), strict=True) client = paramiko.SSHClient() client.load_system_host_keys() if self.known_hosts_path and Path(self.known_hosts_path).is_file(): diff --git a/backend/src/media_library_viewer_api/dependencies.py b/backend/src/media_library_viewer_api/dependencies.py index bac7543..c8c21c6 100644 --- a/backend/src/media_library_viewer_api/dependencies.py +++ b/backend/src/media_library_viewer_api/dependencies.py @@ -18,7 +18,6 @@ from media_library_viewer_api.clients.jellyfin import JellyfinClient from media_library_viewer_api.clients.jellyseerr import JellyseerrClient from media_library_viewer_api.clients.local import LocalCommandClient from media_library_viewer_api.clients.ssh import RemoteSSHClient -from media_library_viewer_api.services.known_hosts import ensure_known_host from media_library_viewer_api.config import get_settings from media_library_viewer_api.services.mail_queue import MailQueue, get_mail_queue as _get_mail_queue from media_library_viewer_api.services.monitoring_poller import ( @@ -153,7 +152,6 @@ def get_ssh_client(request: Request = None): return LocalCommandClient() if machine and machine.get("host") and machine.get("username"): known_hosts_path = get_settings().ssh_known_hosts_file - ensure_known_host(str(machine.get("host")), int(machine.get("port") or 22), known_hosts_path) key_data = None key_passphrase = None ssh_key_id = str(machine.get("ssh_key_id") or "").strip() @@ -190,7 +188,6 @@ def get_ssh_client(request: Request = None): ) if not settings.ssh_key_path: raise RuntimeError("No SSH machine is configured and SSH key settings must be configured") - ensure_known_host(settings.ssh_host, settings.ssh_port, settings.ssh_known_hosts_file) return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, None, None, str(settings.ssh_known_hosts_file))) diff --git a/backend/src/media_library_viewer_api/routers/tasks.py b/backend/src/media_library_viewer_api/routers/tasks.py index 110d51a..c1b5b3a 100644 --- a/backend/src/media_library_viewer_api/routers/tasks.py +++ b/backend/src/media_library_viewer_api/routers/tasks.py @@ -14,7 +14,6 @@ from media_library_viewer_api.clients.local import LocalCommandClient from media_library_viewer_api.clients.ssh import RemoteSSHClient from media_library_viewer_api.config import get_settings from media_library_viewer_api.dependencies import get_settings_store -from media_library_viewer_api.services.known_hosts import ensure_known_host from media_library_viewer_api.services.settings_store import SettingsStore logger = logging.getLogger(__name__) @@ -67,7 +66,6 @@ def _client_for_machine(store: SettingsStore, machine: dict[str, Any]): raise HTTPException(status_code=400, detail="SSH machine is missing host or username") settings = get_settings() - ensure_known_host(host, int(machine.get("port") or 22), settings.ssh_known_hosts_file) private_key = str(machine.get("ssh_private_key") or "") passphrase = str(machine.get("ssh_private_key_passphrase") or "") diff --git a/backend/src/media_library_viewer_api/services/monitoring_actions.py b/backend/src/media_library_viewer_api/services/monitoring_actions.py index 9dda1f0..6709415 100644 --- a/backend/src/media_library_viewer_api/services/monitoring_actions.py +++ b/backend/src/media_library_viewer_api/services/monitoring_actions.py @@ -41,12 +41,14 @@ def build_machine_client(machine: dict[str, Any]): key_directory = str(machine.get("key_directory") or "").strip() key_name = str(machine.get("key_name") or "").strip() key_path = f"{key_directory}/{key_name}" if key_directory and key_name else None + settings = get_settings() return RemoteSSHClient( host=str(machine.get("host") or ""), username=str(machine.get("username") or ""), port=int(machine.get("port") or 22), key_filename=key_path, password=str(machine.get("password") or "") or None, + known_hosts_path=str(settings.ssh_known_hosts_file), ) diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index e214161..3e4cf2b 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -275,3 +275,4 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - 2026-05-06: Library stats, Jellyfin activity, and Monitoring overview now use shared section-container patterns so subcontainers stay consistent across the app. - 2026-05-07: The app versioning scheme should be hybrid: auto-detect package/build metadata when available, but allow explicit overrides for deployments that need fixed labels. - 2026-05-07: The shell should display both frontend and backend version labels so deployed builds are easy to identify without opening a separate diagnostics screen. +- 2026-05-07: SSH host verification should use trust-on-first-use for new machines by recording the first observed host key into the backend-managed known_hosts file, while still rejecting later key mismatches.