mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update root access
This commit is contained in:
@@ -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>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-cyan-500">ProxMenux Monitor</h2>
|
||||
<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>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-orange-500">Proxmox VE</h2>
|
||||
<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={() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user