modules: hardening (ssh/ufw/updates/tz/hostname/swap/locale/fail2ban) + sanity report
This commit is contained in:
parent
9f54745832
commit
5e0e96e8e2
188
modules/50-hardening.sh
Executable file
188
modules/50-hardening.sh
Executable file
@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env bash
|
||||
# 50-hardening — system defaults. Each sub-step y/N, all idempotent.
|
||||
|
||||
step_name="hardening"
|
||||
step_desc="ssh(ufw-safe), ufw, updates, unattended, tz, hostname, swap, locale, fail2ban"
|
||||
step_run="run_hardening"
|
||||
|
||||
run_hardening() {
|
||||
log "hardening system defaults"
|
||||
_h_ssh
|
||||
_h_ufw
|
||||
_h_updates
|
||||
_h_tz_hostname
|
||||
_h_swap
|
||||
_h_locale
|
||||
_h_fail2ban
|
||||
ok "hardening pass complete"
|
||||
}
|
||||
|
||||
# ── SSH: permit root key login, disable password auth ──────────────────────
|
||||
_h_ssh() {
|
||||
if ! command -v sshd >/dev/null 2>&1 && ! [[ -f /etc/ssh/sshd_config ]]; then
|
||||
warn "no sshd found — skipping SSH hardening"; return
|
||||
fi
|
||||
if ! yn "Harden sshd: PermitRootLogin prohibit-password, disable password auth?
|
||||
|
||||
WARNING: only say yes if you can already log in with a public key,
|
||||
else you may lock yourself out. A backup will be made and sshd -t
|
||||
is run before reload. Continue?" n; then
|
||||
echo " skip ssh hardening"; return
|
||||
fi
|
||||
|
||||
local cfg="/etc/ssh/sshd_config"
|
||||
sudo_ cp -a "$cfg" "${cfg}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
sshd_set() { # key value — replace or append a top-level directive
|
||||
local k="$1" v="$2" cfg="$3"
|
||||
if grep -qiE "^\s*#?\s*${k}\b" "$cfg"; then
|
||||
sudo_ sed -i -E "s|^\s*#?\s*${k}\b.*|${k} ${v}|" "$cfg"
|
||||
else
|
||||
echo "${k} ${v}" | sudo_ tee -a "$cfg" >/dev/null
|
||||
fi
|
||||
}
|
||||
sshd_set PermitRootLogin prohibit-password "$cfg"
|
||||
sshd_set PasswordAuthentication no "$cfg"
|
||||
sshd_set KbdInteractiveAuthentication no "$cfg"
|
||||
sshd_set PubkeyAuthentication yes "$cfg"
|
||||
|
||||
if sudo_ sshd -t 2>/dev/null; then
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-active ssh sshd 2>/dev/null | grep -q active; then
|
||||
sudo_ systemctl reload ssh 2>/dev/null || sudo_ systemctl reload sshd 2>/dev/null || sudo_ systemctl restart ssh sshd 2>/dev/null
|
||||
else
|
||||
sudo_ systemctl restart ssh 2>/dev/null || sudo_ systemctl restart sshd 2>/dev/null || \
|
||||
sudo_ service ssh restart 2>/dev/null || sudo_ service sshd restart 2>/dev/null || warn "could not reload sshd"
|
||||
fi
|
||||
ok "sshd hardened (root key-only, no passwords). Keep your current session open and test a new login before closing it."
|
||||
else
|
||||
err "sshd -t failed — restoring backup and aborting ssh hardening"
|
||||
sudo_ cp -a "${cfg}.bak.$(date +%Y%m%d-%H%M%S)" "$cfg" 2>/dev/null || true
|
||||
warn "ssh hardening aborted, config restored"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── ufw: deny incoming, allow 22 + mosh UDP + angie 80/443 ──────────────────
|
||||
_h_ufw() {
|
||||
command -v ufw >/dev/null 2>&1 || { warn "ufw not installed — skipping"; return; }
|
||||
if ! yn "Enable ufw (deny incoming, allow 22, mosh 60000:61000/udp, 80+443/tcp)?" y; then
|
||||
echo " skip ufw"; return
|
||||
fi
|
||||
sudo_ ufw --force reset >/dev/null 2>&1 || true
|
||||
sudo_ ufw default deny incoming
|
||||
sudo_ ufw default allow outgoing
|
||||
sudo_ ufw allow 22/tcp comment 'ssh'
|
||||
sudo_ ufw allow 60000:61000/udp comment 'mosh'
|
||||
sudo_ ufw allow 80/tcp comment 'angie http'
|
||||
sudo_ ufw allow 443/tcp comment 'angie https'
|
||||
sudo_ ufw --force enable
|
||||
ok "ufw enabled. status:"
|
||||
sudo_ ufw status verbose | sed 's/^/ /'
|
||||
}
|
||||
|
||||
# ── updates + unattended-upgrades (debian) ─────────────────────────────────
|
||||
_h_updates() {
|
||||
if ! yn "Run a system upgrade now?" y; then echo " skip updates"; return; fi
|
||||
case "$DISTRO" in
|
||||
arch)
|
||||
sudo_ pacman -Syu --noconfirm
|
||||
;;
|
||||
debian)
|
||||
sudo_ apt-get update -y
|
||||
sudo_ apt-get upgrade -y
|
||||
if yn "Enable unattended-upgrades (daily security auto-patches)?" y; then
|
||||
sudo_ apt-get install -y unattended-upgrades apt-listchanges
|
||||
sudo_ dpkg-reconfigure -f noninteractive unattended-upgrades 2>/dev/null || true
|
||||
ok "unattended-upgrades enabled"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── timezone + hostname ─────────────────────────────────────────────────────
|
||||
_h_tz_hostname() {
|
||||
if command -v timedatectl >/dev/null 2>&1; then
|
||||
if yn "Set timezone (default UTC)?" y; then
|
||||
local tz="${REPLY_TZ:-}"
|
||||
read -rp " timezone [UTC]: " tz
|
||||
[[ -z "$tz" ]] && tz="UTC"
|
||||
sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz"
|
||||
fi
|
||||
else
|
||||
echo " timedatectl missing — skipping tz"
|
||||
fi
|
||||
|
||||
if yn "Set a hostname now? (optional)" n; then
|
||||
read -rp " hostname: " hn
|
||||
if [[ -n "$hn" ]] && command -v hostnamectl >/dev/null 2>&1; then
|
||||
sudo_ hostnamectl set-hostname "$hn" && ok "hostname set to $hn"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ── swapfile if none and RAM is low ─────────────────────────────────────────
|
||||
_h_swap() {
|
||||
if [[ "$(swapon --show --noheadings | wc -l)" -gt 0 ]]; then
|
||||
ok "swap already present — skipping"; return
|
||||
fi
|
||||
local mem_mb; mem_mb="$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo)"
|
||||
if (( mem_mb > 2048 )); then
|
||||
echo " RAM ${mem_mb}MB > 2GB — skipping swapfile"; return
|
||||
fi
|
||||
if ! yn "Create a 2G swapfile (RAM is ${mem_mb}MB)?" y; then echo " skip swap"; return; fi
|
||||
sudo_ fallocate -l 2G /swapfile || sudo_ dd if=/dev/zero of=/swapfile bs=1M count=2048
|
||||
sudo_ chmod 600 /swapfile
|
||||
sudo_ mkswap /swapfile
|
||||
sudo_ swapon /swapfile
|
||||
if ! grep -q '^/swapfile' /etc/fstab; then
|
||||
echo '/swapfile none swap sw 0 0' | sudo_ tee -a /etc/fstab >/dev/null
|
||||
fi
|
||||
ok "2G swapfile created and enabled"
|
||||
}
|
||||
|
||||
# ── locale en_US.UTF-8 ───────────────────────────────────────────────────────
|
||||
_h_locale() {
|
||||
case "$DISTRO" in
|
||||
arch)
|
||||
if ! grep -q '^en_US.UTF-8' /etc/locale.gen 2>/dev/null; then
|
||||
echo " localegen already has en_US.UTF-8 or file absent — touching"
|
||||
fi
|
||||
if [[ -f /etc/locale.gen ]]; then
|
||||
sudo_ sed -i 's/^#en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
|
||||
sudo_ locale-gen
|
||||
fi
|
||||
;;
|
||||
debian)
|
||||
if ! locale -a 2>/dev/null | grep -qi 'en_US.utf8'; then
|
||||
echo 'en_US.UTF-8 UTF-8' | sudo_ tee -a /etc/locale.gen >/dev/null
|
||||
sudo_ locale-gen
|
||||
ok "generated en_US.UTF-8"
|
||||
else
|
||||
ok "en_US.UTF-8 already generated"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── fail2ban (best-effort) ───────────────────────────────────────────────────
|
||||
_h_fail2ban() {
|
||||
if ! yn "Install + enable fail2ban (sshd jail by default)?" y; then echo " skip fail2ban"; return; fi
|
||||
case "$DISTRO" in
|
||||
arch) sudo_ pacman -Sy --noconfirm --needed fail2ban ;;
|
||||
debian) sudo_ apt-get install -y fail2ban ;;
|
||||
esac
|
||||
# local jail for sshd (works across distros)
|
||||
sudo_ tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
|
||||
[DEFAULT]
|
||||
backend = systemd
|
||||
bantime = 1h
|
||||
findtime = 10m
|
||||
maxretry = 5
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
EOF
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl is-system-running >/dev/null 2>&1; then
|
||||
sudo_ systemctl enable --now fail2ban 2>/dev/null || warn "could not enable fail2ban"
|
||||
fi
|
||||
ok "fail2ban installed + sshd jail enabled"
|
||||
}
|
||||
38
modules/60-sanity.sh
Executable file
38
modules/60-sanity.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# 60-sanity — final read-only report + recommended manual actions.
|
||||
|
||||
step_name="sanity"
|
||||
step_desc="report system state + suggest manual follow-ups"
|
||||
step_run="run_sanity"
|
||||
|
||||
run_sanity() {
|
||||
log "sanity report"
|
||||
|
||||
echo " distribution : $DISTRO"
|
||||
echo " arch : $ARCH"
|
||||
echo " user : $USER (shell: $(getent passwd "$USER" | cut -d: -f7))"
|
||||
echo " uptime : $(uptime -p 2>/dev/null || uptime)"
|
||||
echo " memory : $(free -h | awk '/^Mem/ {print $2}') total"
|
||||
echo " swap : $(swapon --show 2>/dev/null | awk 'NR>1{print $3; found=1} END{if(!found) print "none"}')"
|
||||
echo " timezone : $(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo '?')"
|
||||
echo " hostname : $(hostname)"
|
||||
echo " locale : $(locale 2>/dev/null | awk -F= '/^LANG/ {print $2}')"
|
||||
echo " ufw : $(sudo_ ufw status 2>/dev/null | head -1 || echo 'not installed')"
|
||||
echo " fail2ban : $(sudo_ fail2ban-client status 2>/dev/null | head -2 || echo 'not running')"
|
||||
echo " docker : $(docker --version 2>/dev/null || echo 'not installed')"
|
||||
|
||||
echo
|
||||
log "recommended manual follow-ups:"
|
||||
cat <<'EOF'
|
||||
- Test a fresh SSH login (new session) before closing the current one,
|
||||
especially after ssh hardening.
|
||||
- If you use keys: confirm your public key is in ~/.ssh/authorized_keys
|
||||
for the target user.
|
||||
- Fill ~/.config/zsh/aliases-extras.zsh with your personal aliases.
|
||||
- Add starship config if you want a custom prompt: starship preset list
|
||||
(defaults ship with the bootstrap).
|
||||
- Consider 'newgrp docker' or re-login to activate the docker group.
|
||||
- Install domain + TLS for angie separately (certbot/acme.sh), not handled here.
|
||||
EOF
|
||||
ok "bootstrap sanity report done."
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user