#!/usr/bin/env bash # harden-base.sh — idempotent Debian/Ubuntu baseline hardening. # Runs ON THE TARGET VPS (not the local workstation). Generic Debian/ # Ubuntu hardening pattern: SSH+ufw+unattended-upgrades+fail2ban. # # USAGE # curl -fsSL /harden-base.sh | sudo bash -s -- [options] # OR # scp harden-base.sh keiadmin@host:/tmp/ # ssh keiadmin@host "sudo bash /tmp/harden-base.sh" # # OPTIONS # --admin-user default: keiadmin # --ssh-port default: 22 (opens in ufw + enforces in sshd drop-in) # --allow-port repeatable. e.g. --allow-port 443/tcp --allow-port 80/tcp # --no-caddy skip Caddy install (default: skip — install via its own block) # --no-reboot default (never reboots; surfaces needrestart hints only) # --skip repeatable; known steps: apt, ssh, ufw, fail2ban, auditd, unattended # # IDEMPOTENCY # Every step is `test → configure → reload`. Re-run = diff-and-apply. # Detects the sshd_config.d/99-kei.conf + audit rules files; overwrites with # known-good content; never destroys /etc/ssh/sshd_config itself. # # ENV # No secrets read. SECRETS SINGLE SOURCE (RULE 0.8) → harden-base does not # touch tokens/keys. # # EXIT # 0 ok # 1 usage / platform not supported # 2 hardening step failed (stderr) set -euo pipefail log() { printf '[%s] [harden-base] %s\n' "$(date '+%H:%M:%S')" "$*" >&2; } die() { log "ERROR: $*"; exit "${2:-2}"; } # ------------------------------------------------------------------ args ADMIN_USER="keiadmin" SSH_PORT="22" ALLOW_PORTS=() SKIPS=() NO_CADDY=1 # default on; Caddy install is its own block while [ $# -gt 0 ]; do case "$1" in --admin-user) ADMIN_USER="$2"; shift 2 ;; --ssh-port) SSH_PORT="$2"; shift 2 ;; --allow-port) ALLOW_PORTS+=("$2"); shift 2 ;; --no-caddy) NO_CADDY=1; shift ;; --no-reboot) shift ;; # accepted for clarity; we never auto-reboot --skip) SKIPS+=("$2"); shift 2 ;; -h|--help) cat <&2 harden-base.sh — Debian/Ubuntu baseline hardening. OPTIONS --admin-user default: keiadmin --ssh-port default: 22 --allow-port repeatable (e.g. 443/tcp, 80/tcp) --no-caddy (default) skip Caddy install --skip apt|ssh|ufw|fail2ban|auditd|unattended EOF exit 0 ;; *) die "unknown flag '$1'" 1 ;; esac done # ------------------------------------------------------------------ guards [ "$(id -u)" -eq 0 ] || die "must run as root (sudo)." 1 . /etc/os-release 2>/dev/null || die "cannot read /etc/os-release" 1 case "${ID:-}" in debian|ubuntu) : ;; *) die "only Debian/Ubuntu supported (got ID=${ID:-unknown})" 1 ;; esac skipped() { local step="$1" for s in "${SKIPS[@]}"; do [ "$s" = "$step" ] && return 0; done return 1 } # ------------------------------------------------------------------ step: apt step_apt() { skipped apt && { log "skip apt"; return; } log "apt update + base packages…" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq \ ufw fail2ban unattended-upgrades needrestart auditd audispd-plugins \ curl wget jq ca-certificates openssh-server } # ------------------------------------------------------------------ step: admin user step_admin_user() { if id "$ADMIN_USER" >/dev/null 2>&1; then log "user '$ADMIN_USER' exists" else log "creating '$ADMIN_USER' (sudo, bash, NOPASSWD)" useradd -m -s /bin/bash -G sudo "$ADMIN_USER" install -d -m 0700 -o "$ADMIN_USER" -g "$ADMIN_USER" "/home/$ADMIN_USER/.ssh" fi install -d -m 0755 /etc/sudoers.d cat >/etc/sudoers.d/90-keiadmin </dev/null } # ------------------------------------------------------------------ step: ssh step_ssh() { skipped ssh && { log "skip ssh"; return; } log "ssh: writing /etc/ssh/sshd_config.d/99-kei.conf (port=$SSH_PORT, user=$ADMIN_USER)…" install -d -m 0755 /etc/ssh/sshd_config.d cat >/etc/ssh/sshd_config.d/99-kei.conf </dev/null || systemctl reload sshd } # ------------------------------------------------------------------ step: ufw step_ufw() { skipped ufw && { log "skip ufw"; return; } log "ufw: default-deny-in + allow-out + ssh rate-limit…" ufw --force reset >/dev/null ufw default deny incoming ufw default allow outgoing ufw default deny routed ufw limit "$SSH_PORT/tcp" comment "ssh (rate-limited)" for p in "${ALLOW_PORTS[@]}"; do log " allow $p" ufw allow "$p" done ufw logging medium ufw --force enable } # ------------------------------------------------------------------ step: fail2ban step_fail2ban() { skipped fail2ban && { log "skip fail2ban"; return; } log "fail2ban: writing /etc/fail2ban/jail.local (sshd jail)…" cat >/etc/fail2ban/jail.local </etc/audit/rules.d/99-kei.rules <<'EOF' # GENERATED by harden-base.sh — idempotent baseline. -w /etc/ssh/sshd_config -p wa -k sshd_config -w /etc/ssh/sshd_config.d/ -p wa -k sshd_config -w /root/.ssh/ -p wa -k ssh_keys_root -w /etc/sudoers -p wa -k sudoers -w /etc/sudoers.d/ -p wa -k sudoers -a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=unset -k sudo_root -w /etc/passwd -p wa -k identity -w /etc/group -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/gshadow -p wa -k identity -a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k module -a always,exit -F arch=b64 -S adjtimex -S settimeofday -S clock_settime -k time -w /etc/localtime -p wa -k time -e 2 EOF augenrules --load >/dev/null systemctl enable --now auditd } # ------------------------------------------------------------------ step: unattended-upgrades + needrestart step_unattended() { skipped unattended && { log "skip unattended"; return; } log "unattended-upgrades + needrestart (list-only)…" cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::AutocleanInterval "7"; EOF cat >/etc/apt/apt.conf.d/50unattended-upgrades.kei <<'EOF' Unattended-Upgrade::Origins-Pattern { "origin=Debian,codename=${distro_codename}-security"; "origin=Debian,codename=${distro_codename}-updates"; "origin=Ubuntu,archive=${distro_codename}-security"; }; Unattended-Upgrade::Automatic-Reboot "false"; Unattended-Upgrade::MailReport "on-change"; EOF # needrestart: list services, suppress TTY prompts (non-TTY cron safe). if [ -f /etc/needrestart/needrestart.conf ]; then sed -i "s/^#\?\$nrconf{restart}.*/\$nrconf{restart} = 'l';/" /etc/needrestart/needrestart.conf sed -i "s/^#\?\$nrconf{kernelhints}.*/\$nrconf{kernelhints} = -1;/" /etc/needrestart/needrestart.conf fi } # ------------------------------------------------------------------ main log "start: admin=$ADMIN_USER ssh=$SSH_PORT extra-ports=${ALLOW_PORTS[*]:-none}" step_apt step_admin_user step_ssh step_ufw step_fail2ban step_auditd step_unattended log "done. Next: verify via _primitives/_rust/ssh-check + firewall-diff."