From 06a3e6b4728952cb7b2945d5cf8e01213e623f5c Mon Sep 17 00:00:00 2001 From: MacRimi Date: Mon, 9 Feb 2026 18:20:09 +0100 Subject: [PATCH] Update root access --- AppImage/components/security.tsx | 172 ++++++++++++++++++++-- AppImage/scripts/flask_security_routes.py | 16 ++ AppImage/scripts/security_manager.py | 66 +++++++++ 3 files changed, 245 insertions(+), 9 deletions(-) diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index 78259e5f..1d23df87 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -9,7 +9,7 @@ import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Key, Copy, Eye, EyeOff, Trash2, RefreshCw, Clock, ShieldCheck, Globe, FileKey, AlertTriangle, Flame, Bug, Search, Download, Power, PowerOff, Plus, Minus, Activity, Settings, Ban, - FileText, Printer, Play, BarChart3, TriangleAlert, ChevronDown, + FileText, Printer, Play, BarChart3, TriangleAlert, ChevronDown, UserCog, } from "lucide-react" import { getApiUrl, fetchApi } from "../lib/api-config" import { TwoFactorSetup } from "./two-factor-setup" @@ -165,6 +165,17 @@ export function Security() { const [f2bSavingConfig, setF2bSavingConfig] = useState(false) const [f2bApplyingJails, setF2bApplyingJails] = useState(false) + // Root Hardening state + const [rootHardeningLoading, setRootHardeningLoading] = useState(true) + const [rootHardeningData, setRootHardeningData] = useState<{ + hardening_applied: boolean + ssh_root_disabled: boolean + root_web_blocked: boolean + admin_user: string + pve_user_exists: boolean + } | null>(null) + const [showRootHardeningScript, setShowRootHardeningScript] = useState(false) + // SSL/HTTPS state const [sslEnabled, setSslEnabled] = useState(false) const [sslSource, setSslSource] = useState<"none" | "proxmox" | "custom">("none") @@ -184,6 +195,7 @@ export function Security() { loadSslStatus() loadFirewallStatus() loadSecurityTools() + loadRootHardeningStatus() }, []) const loadFirewallStatus = async () => { @@ -223,6 +235,26 @@ export function Security() { } } + const loadRootHardeningStatus = async () => { + try { + setRootHardeningLoading(true) + const data = await fetchApi("/api/security/root-hardening/status") + if (data.success) { + setRootHardeningData({ + hardening_applied: data.hardening_applied, + ssh_root_disabled: data.ssh_root_disabled, + root_web_blocked: data.root_web_blocked, + admin_user: data.admin_user, + pve_user_exists: data.pve_user_exists, + }) + } + } catch { + // Silently fail + } finally { + setRootHardeningLoading(false) + } + } + const loadFail2banDetails = async () => { try { setF2bDetailsLoading(true) @@ -1330,10 +1362,7 @@ ${(report.sections && report.sections.length > 0) ? ` {/* ── ProxMenux Monitor Security Group ── */}
-
- -

ProxMenux Monitor

-
+

ProxMenux Monitor

@@ -2117,13 +2146,128 @@ ${(report.sections && report.sections.length > 0) ? ` {/* ── Proxmox VE Security Group ── */}
-
- -

Proxmox VE

-
+

Proxmox VE

+ {/* Root Access Hardening */} + + +
+
+ + Root Access Hardening +
+ +
+ + Restrict root access to SSH and Proxmox web UI by creating a dedicated admin user + +
+ + {rootHardeningLoading ? ( +
+
+
+ ) : ( +
+ {/* Status indicators */} +
+
+ SSH Root: + {rootHardeningData?.ssh_root_disabled ? ( + Disabled + ) : ( + Enabled + )} +
+
+
+ Root Web: + {rootHardeningData?.root_web_blocked ? ( + Blocked + ) : ( + Active + )} +
+ {rootHardeningData?.admin_user && ( + <> +
+
+ Admin: + {rootHardeningData.admin_user} + {rootHardeningData.pve_user_exists && ( + + )} +
+ + )} +
+ + {/* Info/Warning based on state */} + {rootHardeningData?.hardening_applied ? ( +
+
+ +
+

Root Access Hardened

+

+ Root SSH login is disabled and root web access is blocked. + Use {rootHardeningData.admin_user} for SSH and Proxmox web UI. + For root shell, run sudo -i from the admin user. +

+
+
+
+ ) : ( +
+
+ +
+

Root Access Not Hardened

+

+ Root can access SSH and the Proxmox web UI directly. It is recommended to create a dedicated admin user and disable root remote access. + ProxMenux Monitor will continue working without interruption. +

+
+
+
+ )} + + {/* Action buttons */} +
+ {!rootHardeningData?.hardening_applied ? ( + + ) : ( + + )} +
+
+ )} + + + {/* Proxmox Firewall */} @@ -3544,6 +3688,16 @@ ${(report.sections && report.sections.length > 0) ? ` {/* Script Terminal Modals */} + { + setShowRootHardeningScript(false) + loadRootHardeningStatus() + }} + scriptPath="/usr/local/share/proxmenux/scripts/security/root_hardening.sh" + title="Root Access Hardening" + description="Manage root access hardening for SSH and Proxmox web UI" + /> { diff --git a/AppImage/scripts/flask_security_routes.py b/AppImage/scripts/flask_security_routes.py index 426145da..5b2e590a 100644 --- a/AppImage/scripts/flask_security_routes.py +++ b/AppImage/scripts/flask_security_routes.py @@ -289,3 +289,19 @@ def security_tools(): return jsonify({"success": True, "tools": tools}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 + + +# ------------------------------------------------------------------- +# Root Access Hardening +# ------------------------------------------------------------------- + +@security_bp.route('/api/security/root-hardening/status', methods=['GET']) +def root_hardening_status(): + """Get root access hardening status""" + if not security_manager: + return jsonify({"success": False, "message": "Security manager not available"}), 500 + try: + status = security_manager.get_root_hardening_status() + return jsonify({"success": True, **status}) + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 diff --git a/AppImage/scripts/security_manager.py b/AppImage/scripts/security_manager.py index b22cafbb..60f96469 100644 --- a/AppImage/scripts/security_manager.py +++ b/AppImage/scripts/security_manager.py @@ -1855,3 +1855,69 @@ def parse_lynis_report(): report["proxmox_context_applied"] = True return report + + +# ================================================================= +# Root Access Hardening +# ================================================================= + +HARDENING_FLAG = "/root/.proxmenux/root_hardening.json" +SSHD_CONFIG = "/etc/ssh/sshd_config" + +def get_root_hardening_status(): + """ + Check root hardening status: SSH root login, web access, admin user. + Returns dict with current state. + """ + result = { + "hardening_applied": False, + "ssh_root_disabled": False, + "root_web_blocked": False, + "admin_user": "", + "pve_user_exists": False, + } + + # Check SSH root login + try: + if os.path.isfile(SSHD_CONFIG): + with open(SSHD_CONFIG, 'r') as f: + for line in f: + stripped = line.strip() + if stripped and not stripped.startswith('#'): + if stripped.lower().startswith('permitrootlogin'): + val = stripped.split(None, 1)[1].strip().lower() if len(stripped.split(None, 1)) > 1 else "" + result["ssh_root_disabled"] = val == "no" + break + except Exception: + pass + + # Check hardening flag + if os.path.isfile(HARDENING_FLAG): + try: + with open(HARDENING_FLAG, 'r') as f: + data = json.loads(f.read()) + result["hardening_applied"] = True + result["admin_user"] = data.get("admin_user", "") + result["root_web_blocked"] = data.get("root_web_blocked", False) + result["ssh_root_disabled"] = data.get("ssh_root_disabled", result["ssh_root_disabled"]) + + # Verify admin user exists in PVE + if result["admin_user"]: + rc, out, _ = _run_cmd(["pveum", "user", "list", "--output-format", "json"]) + if rc == 0 and out: + try: + users = json.loads(out) + pam_user = f"{result['admin_user']}@pam" + for u in users: + if u.get("userid") == pam_user: + result["pve_user_exists"] = True + break + except json.JSONDecodeError: + # Fallback: grep approach + rc2, out2, _ = _run_cmd(["pveum", "user", "list"]) + if rc2 == 0: + result["pve_user_exists"] = f"{result['admin_user']}@pam" in out2 + except Exception: + pass + + return result