#!/usr/bin/env bash # Best-effort VM hardening — probes current state, only applies what's missing. # Skips cleanly when infra has already configured a step. # - UFW (allow SSH only) # - unattended-upgrades (security channel) # - fail2ban # - sshd: disable root login + password auth (always validates first) set -euo pipefail # shellcheck source=./lib.sh source "$(cd "$(dirname "$0")" && pwd)/lib.sh" require_debian require_not_root log "Hardening pass — each section probes and skips if already done." # ─── 1. UFW ──────────────────────────────────────────────────────────── if ! command -v ufw >/dev/null; then apt_install ufw fi if sudo ufw status | head -1 | grep -q 'Status: active'; then skip "UFW already active. Current rules:" sudo ufw status numbered | sed 's/^/ /' else if confirm "Enable UFW with 'deny incoming, allow SSH'?"; then sudo ufw default deny incoming >/dev/null sudo ufw default allow outgoing >/dev/null sudo ufw allow OpenSSH >/dev/null sudo ufw --force enable ok "UFW enabled (SSH-only ingress)." else skip "UFW left disabled." fi fi # ─── 2. unattended-upgrades ──────────────────────────────────────────── apt_install unattended-upgrades AUTO_UP="/etc/apt/apt.conf.d/20auto-upgrades" if [[ -f "$AUTO_UP" ]] \ && sudo grep -q 'APT::Periodic::Update-Package-Lists "1"' "$AUTO_UP" \ && sudo grep -q 'APT::Periodic::Unattended-Upgrade "1"' "$AUTO_UP"; then skip "unattended-upgrades already enabled." else cat <<'EOF' | sudo tee "$AUTO_UP" >/dev/null APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; EOF ok "unattended-upgrades enabled." fi # ─── 3. fail2ban ─────────────────────────────────────────────────────── apt_install fail2ban if systemctl is-active fail2ban.service >/dev/null 2>&1; then skip "fail2ban already running." else sudo systemctl enable --now fail2ban.service ok "fail2ban started + enabled at boot." fi # ─── 4. sshd hardening ──────────────────────────────────────────────── # Drop-in file rather than editing /etc/ssh/sshd_config directly. # We always validate with `sshd -t` before reloading the daemon. SSHD_DROPIN="/etc/ssh/sshd_config.d/99-apf-portal-hardening.conf" if sudo test -f "$SSHD_DROPIN"; then skip "sshd hardening drop-in already at $SSHD_DROPIN." else warn "About to disable password auth + root login on SSH." warn "Make sure your SSH key already works from your workstation." if confirm "Continue?"; then cat <<'EOF' | sudo tee "$SSHD_DROPIN" >/dev/null # Managed by docs/setup/scripts/70-hardening.sh. # To override or extend, drop another file in /etc/ssh/sshd_config.d/. PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no UsePAM yes AllowAgentForwarding yes EOF if sudo sshd -t 2>/dev/null; then sudo systemctl reload ssh.service 2>/dev/null || sudo systemctl reload sshd.service ok "sshd reloaded with hardened config." else err "sshd config validation failed — removing $SSHD_DROPIN." sudo rm -f "$SSHD_DROPIN" exit 1 fi else skip "sshd hardening declined." fi fi ok "Hardening pass complete."