Archived
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.
101 lines
3.5 KiB
Bash
Executable File
101 lines
3.5 KiB
Bash
Executable File
#!/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"
|