Update root access

This commit is contained in:
MacRimi
2026-02-09 18:20:09 +01:00
parent 9108882921
commit 06a3e6b472
3 changed files with 245 additions and 9 deletions

View File

@@ -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<any>("/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 ── */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Shield className="h-4 w-4 text-cyan-500" />
<h2 className="text-sm font-semibold uppercase tracking-wider text-cyan-500">ProxMenux Monitor</h2>
</div>
<div className="flex-1 h-px bg-cyan-500/20" />
</div>
@@ -2117,13 +2146,128 @@ ${(report.sections && report.sections.length > 0) ? `
{/* ── Proxmox VE Security Group ── */}
<div className="flex items-center gap-3 mt-4">
<div className="flex items-center gap-2">
<Flame className="h-4 w-4 text-orange-500" />
<h2 className="text-sm font-semibold uppercase tracking-wider text-orange-500">Proxmox VE</h2>
</div>
<div className="flex-1 h-px bg-orange-500/20" />
</div>
{/* Root Access Hardening */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCog className="h-5 w-5 text-orange-500" />
<CardTitle>Root Access Hardening</CardTitle>
</div>
<Button
variant="ghost"
size="sm"
onClick={loadRootHardeningStatus}
disabled={rootHardeningLoading}
>
<RefreshCw className={`h-4 w-4 ${rootHardeningLoading ? "animate-spin" : ""}`} />
<span className="ml-1 text-xs">Refresh</span>
</Button>
</div>
<CardDescription>
Restrict root access to SSH and Proxmox web UI by creating a dedicated admin user
</CardDescription>
</CardHeader>
<CardContent>
{rootHardeningLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin h-8 w-8 border-4 border-orange-500 border-t-transparent rounded-full" />
</div>
) : (
<div className="space-y-4">
{/* Status indicators */}
<div className="flex items-center gap-4 flex-wrap px-3 py-2.5 bg-muted/30 rounded-lg border border-border">
<div className="flex items-center gap-1.5 text-sm">
<span className="text-muted-foreground">SSH Root:</span>
{rootHardeningData?.ssh_root_disabled ? (
<span className="font-bold text-green-500">Disabled</span>
) : (
<span className="font-bold text-yellow-500">Enabled</span>
)}
</div>
<div className="w-px h-4 bg-border" />
<div className="flex items-center gap-1.5 text-sm">
<span className="text-muted-foreground">Root Web:</span>
{rootHardeningData?.root_web_blocked ? (
<span className="font-bold text-green-500">Blocked</span>
) : (
<span className="font-bold text-yellow-500">Active</span>
)}
</div>
{rootHardeningData?.admin_user && (
<>
<div className="w-px h-4 bg-border" />
<div className="flex items-center gap-1.5 text-sm">
<span className="text-muted-foreground">Admin:</span>
<span className="font-bold">{rootHardeningData.admin_user}</span>
{rootHardeningData.pve_user_exists && (
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
)}
</div>
</>
)}
</div>
{/* Info/Warning based on state */}
{rootHardeningData?.hardening_applied ? (
<div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<ShieldCheck className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-green-500">Root Access Hardened</p>
<p className="text-xs text-muted-foreground mt-1">
Root SSH login is disabled and root web access is blocked.
Use <span className="font-mono font-semibold">{rootHardeningData.admin_user}</span> for SSH and Proxmox web UI.
For root shell, run <span className="font-mono font-semibold">sudo -i</span> from the admin user.
</p>
</div>
</div>
</div>
) : (
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-yellow-500">Root Access Not Hardened</p>
<p className="text-xs text-muted-foreground mt-1">
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.
</p>
</div>
</div>
</div>
)}
{/* Action buttons */}
<div className="flex flex-wrap gap-2">
{!rootHardeningData?.hardening_applied ? (
<Button
onClick={() => setShowRootHardeningScript(true)}
className="bg-orange-600 hover:bg-orange-700 text-white"
>
<Shield className="h-4 w-4 mr-2" />
Harden Root Access
</Button>
) : (
<Button
variant="outline"
onClick={() => setShowRootHardeningScript(true)}
className="border-yellow-500/30 text-yellow-500 hover:bg-yellow-500/10"
>
<RefreshCw className="h-4 w-4 mr-2" />
Revert Hardening
</Button>
)}
</div>
</div>
)}
</CardContent>
</Card>
{/* Proxmox Firewall */}
<Card>
<CardHeader>
@@ -3544,6 +3688,16 @@ ${(report.sections && report.sections.length > 0) ? `
</Card>
{/* Script Terminal Modals */}
<ScriptTerminalModal
open={showRootHardeningScript}
onClose={() => {
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"
/>
<ScriptTerminalModal
open={showFail2banInstaller}
onClose={() => {

View File

@@ -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

View File

@@ -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