Flask-based VPS management panel with SSH remote command execution. Includes E2E encrypted SSH tunnel (AES-256-GCM + Go agent), setup wizard, security hardening tools, DNS management, firewall configs, monitoring, backup, and .sec patch update system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
127 lines
3.8 KiB
Python
127 lines
3.8 KiB
Python
"""SSH client with optional E2E encryption.
|
|
|
|
When E2E is enabled, commands are encrypted with the tunnel key before
|
|
being sent over SSH, and responses are decrypted on return. The Go agent
|
|
on the VPS handles decryption/execution/re-encryption.
|
|
|
|
When E2E is disabled, commands run as plain text over SSH (still encrypted
|
|
by SSH transport, just no additional application-layer encryption).
|
|
"""
|
|
|
|
import paramiko
|
|
import config
|
|
import json
|
|
|
|
_client = None
|
|
|
|
|
|
def get_client():
|
|
global _client
|
|
if _client and _client.get_transport() and _client.get_transport().is_active():
|
|
return _client
|
|
cfg = config.load()
|
|
_client = paramiko.SSHClient()
|
|
_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
|
key_path = cfg.get("ssh_key_path", "")
|
|
if not key_path:
|
|
raise RuntimeError("SSH key path not configured")
|
|
|
|
# Try Ed25519 first, fall back to RSA
|
|
try:
|
|
key = paramiko.Ed25519Key.from_private_key_file(key_path)
|
|
except Exception:
|
|
try:
|
|
key = paramiko.RSAKey.from_private_key_file(key_path)
|
|
except Exception:
|
|
key = paramiko.ECDSAKey.from_private_key_file(key_path)
|
|
|
|
_client.connect(
|
|
hostname=cfg["vps_host"],
|
|
port=cfg["vps_port"],
|
|
username=cfg["vps_user"],
|
|
pkey=key,
|
|
timeout=15,
|
|
)
|
|
return _client
|
|
|
|
|
|
def run(cmd, timeout=30):
|
|
"""Execute a command on the VPS. Uses E2E encryption when enabled."""
|
|
import e2e
|
|
|
|
if e2e.is_e2e_enabled():
|
|
return _run_e2e(cmd, timeout)
|
|
else:
|
|
return _run_plain(cmd, timeout)
|
|
|
|
|
|
def _run_plain(cmd, timeout=30):
|
|
"""Execute command via plain SSH (no E2E)."""
|
|
client = get_client()
|
|
_, stdout, stderr = client.exec_command(cmd, timeout=timeout)
|
|
out = stdout.read().decode("utf-8", errors="replace")
|
|
err = stderr.read().decode("utf-8", errors="replace")
|
|
code = stdout.channel.recv_exit_status()
|
|
return {"stdout": out, "stderr": err, "exit_code": code}
|
|
|
|
|
|
def _run_e2e(cmd, timeout=30):
|
|
"""Execute command via E2E encrypted tunnel.
|
|
|
|
1. Encrypt command with tunnel key
|
|
2. SSH: echo <encrypted> | setec-agent
|
|
3. Agent decrypts, executes, encrypts response
|
|
4. Decrypt response locally
|
|
"""
|
|
import e2e
|
|
|
|
tunnel_key = e2e.get_tunnel_key()
|
|
|
|
# Build the wrapped command
|
|
agent_cmd = e2e.wrap_command_for_agent(tunnel_key, cmd)
|
|
|
|
# Execute via SSH
|
|
client = get_client()
|
|
_, stdout, stderr = client.exec_command(agent_cmd, timeout=timeout)
|
|
out = stdout.read().decode("utf-8", errors="replace").strip()
|
|
err = stderr.read().decode("utf-8", errors="replace").strip()
|
|
code = stdout.channel.recv_exit_status()
|
|
|
|
# If agent returned an error on stderr, it's an agent-level failure
|
|
if code != 0 and not out:
|
|
return {
|
|
"stdout": "",
|
|
"stderr": f"E2E agent error: {err}" if err else "E2E agent returned non-zero with no output",
|
|
"exit_code": code,
|
|
}
|
|
|
|
# Decrypt the response
|
|
try:
|
|
response = e2e.decrypt_response(tunnel_key, out)
|
|
return {
|
|
"stdout": response.get("stdout", ""),
|
|
"stderr": response.get("stderr", ""),
|
|
"exit_code": response.get("exit_code", 0),
|
|
}
|
|
except Exception as ex:
|
|
# If decryption fails, the output might be from a non-E2E command
|
|
# or the agent isn't installed. Return raw output with warning.
|
|
return {
|
|
"stdout": "",
|
|
"stderr": f"E2E decryption failed: {str(ex)}\nRaw output: {out[:200]}",
|
|
"exit_code": -1,
|
|
}
|
|
|
|
|
|
def run_plain_always(cmd, timeout=30):
|
|
"""Always run without E2E — used for agent deployment and setup commands."""
|
|
return _run_plain(cmd, timeout)
|
|
|
|
|
|
def close():
|
|
global _client
|
|
if _client:
|
|
_client.close()
|
|
_client = None
|