From 4dfd5127858311f3792e198274bb741fdb8c8cc2 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 13:26:07 +0200 Subject: [PATCH] Wire up a real main<->VM SSH path for the standby's remote-info feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standby's "SSH to main server for containers/stats/system info" fallback (RUNNING_ON_MAIN_SERVER=False path) was wired in code across backups.py/users.py/commands.py/app.py but never actually worked: outbound SSH from the VM is blocked by its own firewall (deliberate hardening — ufw DENY OUT on 22/tcp and 2222/tcp, left untouched), and separately the VM's contabo-key was never added to the main server's authorized_keys in the first place. Fixed via a persistent reverse tunnel instead of opening the VM's firewall: main-to-vm-tunnel.service (systemd, auto-restart) runs on the main server and keeps `ssh -R 2224:localhost:22` open to the VM, so the VM can reach the main server's SSH via localhost:2224 without ever needing outbound access itself. Added the standby's public key to the main server's authorized_keys (chattr +i-locked — unlocked, appended, re-locked immediately). config.py (gitignored, not in this commit — edited live on both servers directly) gets new MAIN_SERVER_SSH_HOST/PORT ("localhost"/"2224"), kept deliberately separate from the existing MAIN_SERVER_IP (which stays the real IP and is still used for on-page display in several templates — overloading it for the tunnel target would have silently changed what's shown in the UI). All four call sites switched from MAIN_SERVER_IP/MAIN_SERVER_PORT to the new pair. Also fixed a pre-existing bug in commands.py's run_command(): its SSH fallback had no -i key at all, so it could never have authenticated even with the tunnel in place. Verified end-to-end on the live standby: run_command(), backups.py's and users.py's _ssh_main() all confirmed to actually execute on the real main server (hostname/whoami/backup count all matched), not erroring or silently no-op'ing. --- platform/app.py | 11 ++++++----- platform/modules/backups.py | 11 ++++++----- platform/modules/commands.py | 6 ++++-- platform/modules/users.py | 7 ++++--- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/platform/app.py b/platform/app.py index b695ce1..c1cc5c5 100644 --- a/platform/app.py +++ b/platform/app.py @@ -11,7 +11,8 @@ from datetime import datetime, timezone from config import ( MAIN_SERVER_IP, RUNNING_ON_MAIN_SERVER, VM_HOST, VM_PORT, VM_KEY, VM_USER, - MAIN_SERVER_KEY, MAIN_SERVER_PORT, MAIN_SERVER_USER, + MAIN_SERVER_KEY, MAIN_SERVER_USER, + MAIN_SERVER_SSH_HOST, MAIN_SERVER_SSH_PORT, ) from modules.auth import login_required from modules.backups import ( @@ -695,9 +696,9 @@ def api_r2_upload(): local_path = f"/tmp/{backup_file}" if not os.path.exists(local_path): pull_cmd = ( - f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_PORT} " + f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_SSH_PORT} " f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 " - f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}:/root/backups/{backup_file} " + f"{MAIN_SERVER_USER}@{MAIN_SERVER_SSH_HOST}:/root/backups/{backup_file} " f"{local_path}" ) res = subprocess.run(pull_cmd, shell=True, capture_output=True, text=True) @@ -799,9 +800,9 @@ def restore_start(): backup_path = f"/tmp/{backup_file}" if not os.path.exists(backup_path): pull_cmd = ( - f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_PORT} " + f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_SSH_PORT} " f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 " - f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}:/root/backups/{backup_file} " + f"{MAIN_SERVER_USER}@{MAIN_SERVER_SSH_HOST}:/root/backups/{backup_file} " f"{backup_path}" ) res = subprocess.run(pull_cmd, shell=True, capture_output=True, text=True) diff --git a/platform/modules/backups.py b/platform/modules/backups.py index 966ac0f..9252f0d 100644 --- a/platform/modules/backups.py +++ b/platform/modules/backups.py @@ -11,7 +11,8 @@ import socket import psutil from config import ( RUNNING_ON_MAIN_SERVER, - MAIN_SERVER_IP, MAIN_SERVER_USER, MAIN_SERVER_KEY, MAIN_SERVER_PORT, + MAIN_SERVER_USER, MAIN_SERVER_KEY, + MAIN_SERVER_SSH_HOST, MAIN_SERVER_SSH_PORT, VM_HOST, VM_PORT, VM_KEY, VM_USER, ) @@ -43,10 +44,10 @@ def _ssh_main(remote_cmd, timeout=30): else: escaped = remote_cmd.replace("'", "'\\''") ssh = ( - f"ssh -i {MAIN_SERVER_KEY} -p {MAIN_SERVER_PORT} " + f"ssh -i {MAIN_SERVER_KEY} -p {MAIN_SERVER_SSH_PORT} " f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " f"-o BatchMode=yes " - f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}" + f"{MAIN_SERVER_USER}@{MAIN_SERVER_SSH_HOST}" ) return _run(f"{ssh} '{escaped}'", timeout=timeout) @@ -275,9 +276,9 @@ def _resolve_archive_path(backup_file, source, add): tmp_path = f"/tmp/audit_{backup_file}" if not os.path.exists(tmp_path): pull_cmd = ( - f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_PORT} " + f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_SSH_PORT} " f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 " - f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}:/root/backups/{backup_file} " + f"{MAIN_SERVER_USER}@{MAIN_SERVER_SSH_HOST}:/root/backups/{backup_file} " f"{tmp_path}" ) _out, err = _run(pull_cmd, timeout=120) diff --git a/platform/modules/commands.py b/platform/modules/commands.py index 74f0283..37676e0 100644 --- a/platform/modules/commands.py +++ b/platform/modules/commands.py @@ -1,5 +1,5 @@ import subprocess -from config import RUNNING_ON_MAIN_SERVER, MAIN_SERVER_IP +from config import RUNNING_ON_MAIN_SERVER, MAIN_SERVER_KEY, MAIN_SERVER_SSH_HOST, MAIN_SERVER_SSH_PORT def run_command(command): """Run command on main server (directly or via SSH)""" @@ -14,10 +14,12 @@ def run_command(command): else: ssh_cmd = [ "ssh", + "-i", MAIN_SERVER_KEY, + "-p", MAIN_SERVER_SSH_PORT, "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10", "-o", "BatchMode=yes", - f"root@{MAIN_SERVER_IP}", + f"root@{MAIN_SERVER_SSH_HOST}", command ] try: diff --git a/platform/modules/users.py b/platform/modules/users.py index f43b56d..2ce38f9 100644 --- a/platform/modules/users.py +++ b/platform/modules/users.py @@ -7,7 +7,8 @@ import json from config import ( RUNNING_ON_MAIN_SERVER, - MAIN_SERVER_IP, MAIN_SERVER_USER, MAIN_SERVER_KEY, MAIN_SERVER_PORT, + MAIN_SERVER_USER, MAIN_SERVER_KEY, + MAIN_SERVER_SSH_HOST, MAIN_SERVER_SSH_PORT, ) @@ -33,10 +34,10 @@ def _ssh_main(remote_cmd, timeout=30): # Escape single quotes in remote_cmd for safe shell wrapping escaped = remote_cmd.replace("'", "'\\''") ssh = ( - f"ssh -i {MAIN_SERVER_KEY} -p {MAIN_SERVER_PORT} " + f"ssh -i {MAIN_SERVER_KEY} -p {MAIN_SERVER_SSH_PORT} " f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " f"-o BatchMode=yes " - f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}" + f"{MAIN_SERVER_USER}@{MAIN_SERVER_SSH_HOST}" ) return _run(f"{ssh} '{escaped}'", timeout=timeout)