106 lines
4.2 KiB
Bash
Executable file
106 lines
4.2 KiB
Bash
Executable file
#!/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}"
|