Archived
Stage 1 base config/hardening toolset, applied and verified on pve1
Splits the repo into Stage 1 (base host config/hardening, applies to any node) and Stage 2 (future HA/Ceph cluster, deferred - pve1's mini-PC hardware can't support the assumed split-disk/multi-NIC layout). Adds the Stage 1 toolset: firewall deploy, named admin user creation, unattended security upgrades, subscription-nag removal (with an apt hook so the patch survives package updates), and a read-only audit script. Fixes switch-to-no-subscription-repo.sh, which only handled the legacy .list format and silently no-op'd against PVE 9's deb822 .sources files; it now removes enterprise sources outright rather than commenting them out. Shared logic (root check, idempotent file writes, backups) factored into scripts/lib/common.sh. Ran the full sequence against pve1 via scripts/bootstrap.sh + create-admin-user.sh; scripts/audit.sh confirms all checks pass.
This commit is contained in:
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# Read-only Stage 1 base-hardening audit. Checks the current state of a PVE
|
||||
# host against the checklist in docs/04-security-hardening.md and prints
|
||||
# PASS/FAIL per item. Exits non-zero if anything fails, so it can gate CI or
|
||||
# be run periodically as a compliance check. Makes no changes.
|
||||
#
|
||||
# Usage: ./audit.sh (run as root on the PVE host)
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
require_root
|
||||
|
||||
AUDIT_FAIL=0
|
||||
|
||||
# --- apt repos: no enabled enterprise source ---
|
||||
ENTERPRISE_ENABLED=0
|
||||
for f in /etc/apt/sources.list.d/*.sources /etc/apt/sources.list.d/*.list; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qi 'enterprise.proxmox.com' "$f" 2>/dev/null && ENTERPRISE_ENABLED=1
|
||||
done
|
||||
if [ "$ENTERPRISE_ENABLED" -eq 0 ]; then
|
||||
audit_pass "no enabled enterprise apt repo"
|
||||
else
|
||||
audit_fail "an enterprise apt repo is still enabled (needs a subscription to update)"
|
||||
fi
|
||||
|
||||
# --- SSH ---
|
||||
SSHD_T="$(sshd -T 2>/dev/null)"
|
||||
if echo "$SSHD_T" | grep -qiE '^permitrootlogin (prohibit-password|without-password)'; then
|
||||
audit_pass "sshd: PermitRootLogin prohibit-password (key-only)"
|
||||
else
|
||||
audit_fail "sshd: PermitRootLogin is not key-only (prohibit-password/without-password)"
|
||||
fi
|
||||
if echo "$SSHD_T" | grep -qi '^passwordauthentication no'; then
|
||||
audit_pass "sshd: PasswordAuthentication no"
|
||||
else
|
||||
audit_fail "sshd: PasswordAuthentication is not disabled"
|
||||
fi
|
||||
|
||||
# --- fail2ban ---
|
||||
if systemctl is-active --quiet fail2ban 2>/dev/null; then
|
||||
audit_pass "fail2ban is active"
|
||||
else
|
||||
audit_fail "fail2ban is not active"
|
||||
fi
|
||||
|
||||
# --- PVE firewall ---
|
||||
FW_STATUS="$(pve-firewall status 2>/dev/null || true)"
|
||||
if echo "$FW_STATUS" | grep -qi '^Status: enabled'; then
|
||||
audit_pass "pve-firewall is enabled"
|
||||
else
|
||||
audit_fail "pve-firewall is not enabled (status: ${FW_STATUS:-unknown})"
|
||||
fi
|
||||
if [ -f /etc/pve/firewall/cluster.fw ] && grep -qi '^policy_in:\s*DROP' /etc/pve/firewall/cluster.fw 2>/dev/null; then
|
||||
audit_pass "cluster.fw has default-deny inbound policy"
|
||||
else
|
||||
audit_fail "cluster.fw missing or does not default-deny inbound"
|
||||
fi
|
||||
|
||||
# --- unattended-upgrades ---
|
||||
if dpkg -s unattended-upgrades >/dev/null 2>&1 && systemctl is-enabled --quiet unattended-upgrades 2>/dev/null; then
|
||||
audit_pass "unattended-upgrades installed and enabled"
|
||||
else
|
||||
audit_fail "unattended-upgrades not installed/enabled"
|
||||
fi
|
||||
if [ -f /var/run/reboot-required ]; then
|
||||
audit_warn "a reboot is pending (/var/run/reboot-required) - schedule one"
|
||||
fi
|
||||
|
||||
# --- named admin user (not just root@pam) ---
|
||||
if pveum user list --output-format json 2>/dev/null | grep -q '"userid":"[^"]*@pve"'; then
|
||||
audit_pass "a named @pve admin user exists (root@pam is not the only account)"
|
||||
else
|
||||
audit_fail "no named @pve user found - root@pam is the only account"
|
||||
fi
|
||||
|
||||
# --- time sync ---
|
||||
if timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qx 'yes'; then
|
||||
audit_pass "clock is NTP-synchronized"
|
||||
else
|
||||
audit_fail "clock is not NTP-synchronized"
|
||||
fi
|
||||
|
||||
# --- subscription nag (cosmetic - warn only, never fails the audit) ---
|
||||
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
if [ -f "$JS_FILE" ] && ! grep -qF "data.status.toLowerCase() !== 'active'" "$JS_FILE"; then
|
||||
audit_pass "subscription nag patch applied"
|
||||
else
|
||||
audit_warn "subscription nag patch not applied (cosmetic only, see scripts/disable-subscription-nag.sh)"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$AUDIT_FAIL" -eq 0 ]; then
|
||||
echo "All Stage 1 base-hardening checks passed."
|
||||
else
|
||||
echo "One or more checks failed - see FAIL lines above."
|
||||
fi
|
||||
exit "$AUDIT_FAIL"
|
||||
Reference in New Issue
Block a user