#!/usr/bin/env bash # # verify-hardening.sh - Idempotent audit of the VPS hardening state. # # Safe to run any time, as root or via sudo. Exit code 0 = all checks pass, # non-zero = at least one check failed. Prints a PASS/FAIL line per check. # # sudo bash deploy/verify-hardening.sh # set -uo pipefail DEPLOY_USER="${DEPLOY_USER:-deploy}" SSH_PORT="${SSH_PORT:-22}" GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m' pass() { echo -e "${GREEN}[PASS]${NC} $*"; } fail() { echo -e "${RED}[FAIL]${NC} $*"; FAILED=$((FAILED + 1)); } FAILED=0 # ── 1. Non-root deploy user exists ───────────────────────────────────────── if id "${DEPLOY_USER}" >/dev/null 2>&1; then pass "deploy user '${DEPLOY_USER}' exists" else fail "deploy user '${DEPLOY_USER}' does not exist" fi # ── 2. Root SSH login disabled ───────────────────────────────────────────── if grep -qE '^\s*PermitRootLogin\s+no' /etc/ssh/sshd_config 2>/dev/null; then pass "PermitRootLogin is 'no'" else fail "PermitRootLogin is not 'no'" fi # ── 3. Password auth disabled ────────────────────────────────────────────── if grep -qE '^\s*PasswordAuthentication\s+no' /etc/ssh/sshd_config 2>/dev/null; then pass "PasswordAuthentication is 'no'" else fail "PasswordAuthentication is not 'no'" fi # ── 4. Deploy user key exists and is locked down ─────────────────────────── KEY="${DEPLOY_USER}" KEYFILE="/home/${KEY}/.ssh/authorized_keys" if [ -f "${KEYFILE}" ] && [ -s "${KEYFILE}" ]; then pass "authorized_keys present for '${KEY}'" [ "$(stat -c '%a' "${KEYFILE}" 2>/dev/null)" = "600" ] && \ pass "authorized_keys mode is 600" || fail "authorized_keys mode is not 600" else fail "no authorized_keys for '${KEY}'" fi # ── 5. SSH config is valid ───────────────────────────────────────────────── if sshd -t 2>/dev/null; then pass "sshd_config validates" else fail "sshd_config does NOT validate" fi # ── 6. Firewall active and restrictive ───────────────────────────────────── if command -v ufw >/dev/null 2>&1; then if ufw status | grep -q "Status: active"; then pass "UFW is active" if ufw status verbose | grep -qE "Deny|deny"; then pass "UFW default deny in place" else fail "UFW default policy not 'deny incoming'" fi else fail "UFW is not active" fi else fail "ufw not installed" fi # ── 7. fail2ban active ───────────────────────────────────────────────────── if systemctl is-active --quiet fail2ban 2>/dev/null; then pass "fail2ban is active" if fail2ban-client status sshd >/dev/null 2>&1; then pass "fail2ban sshd jail enabled" else fail "fail2ban sshd jail not found" fi else fail "fail2ban not active" fi # ── 8. Unattended security upgrades ──────────────────────────────────────── if systemctl is-active --quiet unattended-upgrades 2>/dev/null; then pass "unattended-upgrades active" else fail "unattended-upgrades not active" fi # ── 9. Recent brute-force attempts (informational) ───────────────────────── echo echo "Recent SSH brute-force attempts (last 10 unique 'Invalid user'):" grep -h "Invalid user" /var/log/auth.log* 2>/dev/null \ | awk '{print $10}' | sort | uniq -c | sort -rn | head -10 || true echo if [ "${FAILED}" -eq 0 ]; then echo -e "${GREEN}[OK] All hardening checks passed.${NC}" else echo -e "${RED}[WARN] ${FAILED} check(s) failed - re-run deploy/hardening.sh or fix manually.${NC}" fi exit "${FAILED}"