diff --git a/.env.example b/.env.example index 46a807b..59ff62b 100644 --- a/.env.example +++ b/.env.example @@ -24,10 +24,12 @@ SMTP_TIMEOUT=30 SSH_HOST=media-server.example.com SSH_USERNAME=username SSH_PORT=22 -# In Docker Compose, mount your private key at ./secrets/ssh/id_ed25519 -# and set this path to /root/.ssh/id_ed25519 inside the backend container. -SSH_KEY_FILENAME=/root/.ssh/id_ed25519 -SSH_KEY_NAME=id_rsa +# In Docker Compose, mount ./secrets/ssh to /root/.ssh inside the backend container. +# The app uses SSH_KEY_DIRECTORY + SSH_KEY_NAME to build the full key path. +SSH_KEY_DIRECTORY=/root/.ssh +SSH_KEY_NAME=id_ed25519 +# Optional legacy override if you want to provide the full path directly. +# SSH_KEY_FILENAME=/root/.ssh/id_ed25519 # SSH_PASSWORD=optional-password-or-key-passphrase REMOTE_MEDIA_ROOT=/mnt/media # Optional fallback prefix when REMOTE_MEDIA_ROOT mapping is not enough. diff --git a/README.md b/README.md index a424f11..d89be03 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,12 @@ LOG_LEVEL=INFO SSH_HOST=media-server.example.com SSH_USERNAME=username SSH_PORT=22 -# In Docker Compose, mount your private key at ./secrets/ssh/id_ed25519 -# and your host key file at ./secrets/ssh/known_hosts. -SSH_KEY_FILENAME=/root/.ssh/id_ed25519 +# In Docker Compose, mount ./secrets/ssh to /root/.ssh inside the backend container. +# The app uses SSH_KEY_DIRECTORY + SSH_KEY_NAME to build the full key path. +SSH_KEY_DIRECTORY=/root/.ssh +SSH_KEY_NAME=id_ed25519 +# Optional legacy override if you want to provide the full path directly. +# SSH_KEY_FILENAME=/root/.ssh/id_ed25519 SSH_PASSWORD= REMOTE_MEDIA_ROOT=/srv/media diff --git a/backend/README.md b/backend/README.md index 7146302..5911c92 100644 --- a/backend/README.md +++ b/backend/README.md @@ -71,9 +71,12 @@ OIDC_CLOCK_SKEW_SECONDS=30 SSH_HOST=media-server.example.com SSH_USERNAME=username SSH_PORT=22 -# In Docker Compose, mount your private key at ./secrets/ssh/id_ed25519 -# and your host key file at ./secrets/ssh/known_hosts. -SSH_KEY_FILENAME=/root/.ssh/id_ed25519 +# In Docker Compose, mount ./secrets/ssh to /root/.ssh inside the backend container. +# The app uses SSH_KEY_DIRECTORY + SSH_KEY_NAME to build the full key path. +SSH_KEY_DIRECTORY=/root/.ssh +SSH_KEY_NAME=id_ed25519 +# Optional legacy override if you want to provide the full path directly. +# SSH_KEY_FILENAME=/root/.ssh/id_ed25519 SSH_PASSWORD= REMOTE_MEDIA_ROOT=/srv/media diff --git a/backend/src/media_library_viewer_api/config.py b/backend/src/media_library_viewer_api/config.py index 66940c3..c996172 100644 --- a/backend/src/media_library_viewer_api/config.py +++ b/backend/src/media_library_viewer_api/config.py @@ -55,7 +55,9 @@ class Settings(BaseSettings): ssh_host: str = "" ssh_username: str = "" ssh_port: int = 22 - ssh_key_filename: str = str(Path.home() / ".ssh" / "id_rsa") + ssh_key_directory: str = str(Path.home() / ".ssh") + ssh_key_name: str = "id_rsa" + ssh_key_filename: str = "" # legacy direct override (full path) ssh_password: str = "" # Remote paths @@ -71,6 +73,12 @@ class Settings(BaseSettings): def path_prefix(self) -> str: return self.remote_path_prefix or "" + @property + def ssh_key_path(self) -> str: + if self.ssh_key_filename: + return self.ssh_key_filename + return str(Path(self.ssh_key_directory) / self.ssh_key_name) + model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"} diff --git a/backend/src/media_library_viewer_api/dependencies.py b/backend/src/media_library_viewer_api/dependencies.py index c9ab52b..8ff3f25 100644 --- a/backend/src/media_library_viewer_api/dependencies.py +++ b/backend/src/media_library_viewer_api/dependencies.py @@ -50,14 +50,14 @@ def get_ssh_client() -> RemoteSSHClient: settings.ssh_host or "", settings.ssh_username or "", settings.ssh_port, - settings.ssh_key_filename or "", + settings.ssh_key_path or "", "set" if settings.ssh_password else "missing", ) client = RemoteSSHClient( host=settings.ssh_host, username=settings.ssh_username, port=settings.ssh_port, - key_filename=settings.ssh_key_filename or None, + key_filename=settings.ssh_key_path or None, password=settings.ssh_password or None, ) try: diff --git a/backend/src/media_library_viewer_api/logging_utils.py b/backend/src/media_library_viewer_api/logging_utils.py index 42eeed9..287d76c 100644 --- a/backend/src/media_library_viewer_api/logging_utils.py +++ b/backend/src/media_library_viewer_api/logging_utils.py @@ -52,6 +52,8 @@ def describe_settings(settings: object) -> dict[str, str]: "ssh_host": getattr(settings, "ssh_host", "") or "", "ssh_username": getattr(settings, "ssh_username", "") or "", "ssh_port": str(getattr(settings, "ssh_port", 22) or 22), + "ssh_key_directory": getattr(settings, "ssh_key_directory", "") or "", + "ssh_key_name": getattr(settings, "ssh_key_name", "") or "", "ssh_key_filename": "set" if getattr(settings, "ssh_key_filename", "") else "missing", "ssh_password": "set" if getattr(settings, "ssh_password", "") else "missing", "smtp_host": _sanitize_url(getattr(settings, "smtp_host", "")), diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 32d9622..fc05b87 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -9,12 +9,13 @@ services: - .env environment: AUTH_ENABLED: "false" - SSH_KEY_FILENAME: /root/.ssh/id_rsa + SSH_KEY_DIRECTORY: /root/.ssh + SSH_KEY_NAME: ${SSH_KEY_NAME:-id_ed25519} ports: - "8000:8000" volumes: - ./backend:/app/backend - - ./secrets/ssh/:/root/.ssh/ + - ./secrets/ssh:/root/.ssh:ro restart: unless-stopped frontend: diff --git a/docker-compose.yml b/docker-compose.yml index aadc769..e0c45db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,9 +5,10 @@ services: dockerfile: backend/Dockerfile environment: AUTH_ENABLED: "true" - SSH_KEY_FILENAME: /root/.ssh/${SSH_KEY_NAME} + SSH_KEY_DIRECTORY: /root/.ssh + SSH_KEY_NAME: ${SSH_KEY_NAME} volumes: - - ./secrets/ssh:/root/.ssh:ro + - ${SSH_KEY_DIRECTORY}:/root/.ssh:ro restart: unless-stopped expose: - "8000" diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index d8e3e76..e563989 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -127,6 +127,7 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - Support OIDC login in the frontend using an OIDC client library, with backend JWT validation for protected API requests. - Provide Docker Compose deployment files at the repository root for production and local development. - Backend Docker deployment should mount a private SSH key and a known_hosts file into the container rather than baking them into the image. +- The SSH key configuration should support separate directory/name inputs so Docker Compose can mount an entire `./secrets/ssh` directory into `/root/.ssh` while the app assembles the full key path. - Show Jellyfin media counts for movies, series, and series episodes on the dashboard. - Show dashboard session activity from Jellyfin, including both currently playing sessions and logged-in idle sessions. - Activity rows should include user, media title (or `(idle)`), playback state (`playing`/`paused`/`idle`), and whether transcoding is active. @@ -169,4 +170,4 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo - 2026-05-03: Confirmed the shared session table should keep the compact overall status summary line above the rows. - 2026-05-03: Updated the dashboard monitoring cards to show 10-minute averages with high/low subtext instead of only the latest sample. - 2026-05-03: Added OIDC/JWT auth support plus root-level Docker Compose deployment files for production and dev workflows. -- 2026-05-04: Backend Docker Compose now mounts `./secrets/ssh/id_ed25519` and `./secrets/ssh/known_hosts` into `/root/.ssh` so Paramiko can use a private key and strict host-key checking without baking secrets into the image. +- 2026-05-04: Backend Docker Compose now mounts the entire `./secrets/ssh` directory into `/root/.ssh` so Paramiko can use a private key and strict host-key checking without baking secrets into the image.