Fix SSH permissions: store known_hosts in persistent /data and disable pseudo-terminal warning

This commit is contained in:
jschantli committed 2026-09-04 20:30:02 -07:00
1 parent 6d03f5de43
commit f74a3e09f5
2 files changed
+48 -68

No files matched your search

+44 -65
View File
@@ -4,7 +4,7 @@ import shutil
import time
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple
from backend.config import load_settings, DEFAULT_KEY_FILE, DEFAULT_PUB_KEY_FILE
from backend.config import load_settings, DEFAULT_KEY_FILE, DEFAULT_PUB_KEY_FILE, DATA_DIR
def get_public_key() -> str:
if DEFAULT_PUB_KEY_FILE.exists():
@@ -40,9 +40,12 @@ def build_ssh_prefix(settings: Optional[Dict[str, Any]] = None) -> List[str]:
raw_key_path = settings.get("vps_ssh_key_path", str(DEFAULT_KEY_FILE))
key_path = resolve_key_path(raw_key_path)
password = settings.get("vps_ssh_password", "")
known_hosts = str(DATA_DIR / "known_hosts")
ssh_opts = [
"-T",
"-o", "StrictHostKeyChecking=accept-new",
"-o", f"UserKnownHostsFile={known_hosts}",
"-o", "ConnectTimeout=8",
"-o", "ServerAliveInterval=15",
"-p", port
@@ -69,10 +72,12 @@ def build_scp_prefix(settings: Optional[Dict[str, Any]] = None) -> List[str]:
raw_key_path = settings.get("vps_ssh_key_path", str(DEFAULT_KEY_FILE))
key_path = resolve_key_path(raw_key_path)
password = settings.get("vps_ssh_password", "")
known_hosts = str(DATA_DIR / "known_hosts")
scp_opts = [
"-P", port,
"-o", "StrictHostKeyChecking=accept-new",
"-o", f"UserKnownHostsFile={known_hosts}",
"-o", "ConnectTimeout=10"
]
@@ -99,79 +104,53 @@ async def run_remote_quick(cmd: str, settings: Optional[Dict[str, Any]] = None)
stdout, stderr = await proc.communicate()
return proc.returncode, stdout.decode('utf-8', errors='replace'), stderr.decode('utf-8', errors='replace')
except Exception as e:
return -1, "", str(e)
return 1, "", str(e)
async def test_vps_connection(custom_settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
s = custom_settings or load_settings()
host = s.get("vps_host", "").strip()
async def test_vps_connection(settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
if not settings:
settings = load_settings()
host = settings.get("vps_host", "").strip()
if not host:
return {
"success": False,
"connected": False,
"error": "No VPS Host / IP configured. Please specify the VPS address in Settings."
}
return {"success": False, "error": "No VPS host configured. Please provide IP or hostname."}
# Run comprehensive diagnostic script on remote VPS
diag_script = """
echo "===PING_OK==="
uname -n
cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"'
uname -r
uptime -p 2>/dev/null || uptime
which docker >/dev/null 2>&1 && echo "DOCKER_FOUND" || echo "DOCKER_NOT_FOUND"
docker ps >/dev/null 2>&1 && echo "DOCKER_RUNNING" || echo "DOCKER_STOPPED"
"""
ret, stdout, stderr = await run_remote_quick(diag_script, s)
# 1. Test basic SSH connectivity & gather OS summary
cmd_ping = 'echo "PANGOLIN_PONG" && uname -srm && cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d \'"\''
code, stdout, stderr = await run_remote_quick(cmd_ping, settings)
if ret != 0 or "===PING_OK===" not in stdout:
err = stderr.strip() or "Connection failed. Please check Host, Port, and SSH credentials."
if "Permission denied" in err:
err = "Permission denied (SSH authentication failed). Please ensure your SSH public key or password is correct on the VPS."
elif "Connection timed out" in err or "No route to host" in err:
err = f"Could not reach {host}:{s.get('vps_port', 22)}. Verify firewall, IP address, and SSH port."
return {
"success": False,
"connected": False,
"error": err
}
if code != 0:
err_msg = stderr.strip() or "Connection timed out or failed."
if "Permission denied" in err_msg:
err_msg += " (SSH Authentication failed. Check key in ~/.ssh/authorized_keys or password)"
return {"success": False, "error": err_msg}
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
hostname = lines[1] if len(lines) > 1 else host
os_name = lines[2] if len(lines) > 2 else "Linux"
kernel = lines[3] if len(lines) > 3 else ""
uptime_str = lines[4] if len(lines) > 4 else ""
docker_found = "DOCKER_FOUND" in stdout
docker_running = "DOCKER_RUNNING" in stdout
lines = stdout.strip().split("\n")
os_name = lines[2] if len(lines) > 2 else (lines[1] if len(lines) > 1 else "Linux")
kernel = lines[1] if len(lines) > 1 else ""
# Check Pangolin path on VPS
p_dir = s.get("vps_pangolin_dir", "/opt/pangolin")
p_script = f"""
[ -d "{p_dir}" ] && echo "DIR_OK" || echo "DIR_MISSING"
[ -f "{p_dir}/docker-compose.yml" ] || [ -f "{p_dir}/compose.yaml" ] && echo "COMPOSE_OK" || echo "COMPOSE_MISSING"
find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null | head -n 1
"""
_, p_out, _ = await run_remote_quick(p_script, s)
p_lines = [l.strip() for l in p_out.splitlines() if l.strip()]
# 2. Check Docker availability on VPS
code_docker, stdout_docker, _ = await run_remote_quick('docker --version', settings)
docker_installed = (code_docker == 0)
docker_version = stdout_docker.strip() if docker_installed else "Not detected"
dir_ok = "DIR_OK" in p_lines
compose_ok = "COMPOSE_OK" in p_lines
db_file = None
for l in p_lines:
if l.endswith((".db", ".sqlite")):
db_file = l
break
# 3. Check Pangolin Directory
pangolin_dir = settings.get("vps_pangolin_dir", "/opt/pangolin")
chk_dir_cmd = f'[ -d "{pangolin_dir}" ] && echo "DIR_EXISTS" || echo "DIR_MISSING"'
_, stdout_dir, _ = await run_remote_quick(chk_dir_cmd, settings)
pangolin_dir_found = "DIR_EXISTS" in stdout_dir
# 4. Check SQLite database status in Pangolin directory
chk_db_cmd = f'find "{pangolin_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null'
_, stdout_db, _ = await run_remote_quick(chk_db_cmd, settings)
dbs_found = [p.strip() for p in stdout_db.strip().split("\n") if p.strip()]
return {
"success": True,
"connected": True,
"hostname": hostname,
"os": os_name,
"os_name": os_name,
"kernel": kernel,
"uptime": uptime_str,
"docker_installed": docker_found,
"docker_running": docker_running,
"pangolin_dir_exists": dir_ok,
"compose_file_exists": compose_ok,
"database_file": db_file,
"message": f"Successfully connected to {hostname} ({host}) via SSH!"
"docker_installed": docker_installed,
"docker_version": docker_version,
"pangolin_dir_found": pangolin_dir_found,
"sqlite_databases": dbs_found,
"message": "SSH Connection successful! Remote host is ready for backups and updates."
}
+4 -3
View File
@@ -5,7 +5,8 @@ set -e
DATA_PATH="${DATA_DIR:-/data}"
BACKUP_PATH="${BACKUP_DIR:-/backups}"
mkdir -p "$DATA_PATH" "$BACKUP_PATH"
mkdir -p "$DATA_PATH" "$BACKUP_PATH" "$DATA_PATH/.ssh"
export HOME="$DATA_PATH"
# Set secure permissions on SSH private key if it exists
if [ -f "$DATA_PATH/id_ed25519" ]; then
@@ -22,9 +23,9 @@ if [ "$PUID" -ne 0 ] && [ "$PGID" -ne 0 ]; then
groupadd -g "$PGID" pangolin 2>/dev/null || true
fi
# Create or update user
# Create or update user with persistent home in DATA_PATH
if ! getent passwd pangolin >/dev/null 2>&1; then
useradd -u "$PUID" -g "$PGID" -d /app -s /bin/bash pangolin 2>/dev/null || true
useradd -u "$PUID" -g "$PGID" -d "$DATA_PATH" -s /bin/bash pangolin 2>/dev/null || true
fi
# Ensure ownership of persistent data and backup volumes