Archived
New script setup-linux-admin-user.sh creates the Linux system user, installs an SSH authorized key, and adds the user to the sudo group. Integrated into bootstrap.sh before harden-ssh.sh so key-based access is in place before password authentication is disabled. bootstrap.sh now accepts ADMIN_USER and ADMIN_SSH_KEY env vars to run user setup and setup-admin-sudo.sh automatically at the right point. audit.sh checks that at least one non-root user has an authorized key. docs/04-security-hardening.md updated with the new steps and ordering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
4.7 KiB
Bash
Executable File
134 lines
4.7 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
|
|
|
|
# --- Linux admin user with SSH key (for non-root SSH login) ---
|
|
LINUX_ADMIN_OK=0
|
|
for auth_file in /home/*/.ssh/authorized_keys; do
|
|
[ -f "$auth_file" ] || continue
|
|
# Must have at least one non-comment, non-empty key line.
|
|
if grep -qE '^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) ' "$auth_file" 2>/dev/null; then
|
|
LINUX_ADMIN_OK=1
|
|
break
|
|
fi
|
|
done
|
|
if [ "$LINUX_ADMIN_OK" -eq 1 ]; then
|
|
audit_pass "a non-root Linux user has an SSH authorized key"
|
|
else
|
|
audit_fail "no non-root Linux user has an authorized SSH key (run setup-linux-admin-user.sh)"
|
|
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
|
|
|
|
# --- passwordless sudo for pvesh/qm/pct ---
|
|
# The nixos flake's create-proxmox-resource.sh runs pvesh/qm/pct over
|
|
# non-interactive SSH, so the admin user needs NOPASSWD for these tools.
|
|
SUDO_OK=0
|
|
for f in /etc/sudoers.d/*-proxmox; do
|
|
[ -f "$f" ] || continue
|
|
if grep -qE 'NOPASSWD:.*pvesh' "$f" && grep -qE 'NOPASSWD:.*\bqm\b' "$f" && grep -qE 'NOPASSWD:.*\bpct\b' "$f"; then
|
|
SUDO_OK=1
|
|
break
|
|
fi
|
|
done
|
|
if [ "$SUDO_OK" -eq 1 ]; then
|
|
audit_pass "admin user has NOPASSWD sudo for pvesh/qm/pct"
|
|
else
|
|
audit_fail "no sudoers file grants NOPASSWD for pvesh/qm/pct (run setup-admin-sudo.sh <username>)"
|
|
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"
|