From 0af729829a0bc2e25dd8970e812decdaf764dd33 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:12:59 +0100 Subject: [PATCH] Update setup_harden_debian_ubuntu.sh --- setup_harden_debian_ubuntu.sh | 1222 +++++++++++++++++---------------- 1 file changed, 633 insertions(+), 589 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 8c2359d..823149a 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,9 +1,12 @@ #!/bin/bash # Debian/Ubuntu Server Setup and Hardening Script -# Version: 4.0 | 2025-06-26 +# Version: 4.1 | 2025-06-26 # Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS, 24.10 (experimental) # +# Purpose: Automates server setup, security hardening, and optional installations (Docker, Tailscale). +# Features: User creation, SSH hardening, UFW, Fail2Ban, auto-updates, monitoring (SMTP/ntfy), swap, time sync. +# Usage: Run as root with optional --quiet or --config flags. # See README.md for full documentation. set -euo pipefail # Exit on error, undefined vars, pipe failures @@ -30,6 +33,7 @@ SSHD_BACKUP_FILE="" LOCAL_KEY_ADDED=false SSH_SERVICE="" ID="" +UBUNTU_CODENAME="" SKIPPED_SETTINGS=() PROMPTED_SETTINGS=() @@ -44,45 +48,53 @@ done # --- LOGGING & PRINT FUNCTIONS --- +# Log messages to file with timestamp log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" } +# Print header with script title and version print_header() { [[ $VERBOSE == false ]] && return echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4.0 | 2025-06-26 ║${NC}" + echo -e "${CYAN}║ v4.1 | 2025-06-26 ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo } +# Print section title print_section() { [[ $VERBOSE == false ]] && return echo -e "\n${BLUE}▓▓▓ $1 ▓▓▓${NC}" | tee -a "$LOG_FILE" echo -e "${BLUE}$(printf '═%.0s' {1..65})${NC}" } +# Print success message print_success() { [[ $VERBOSE == false ]] && return echo -e "${GREEN}✓ $1${NC}" | tee -a "$LOG_FILE" } +# Print error message (always printed, even in quiet mode) print_error() { echo -e "${RED}✗ $1${NC}" | tee -a "$LOG_FILE" } +# Print warning message print_warning() { [[ $VERBOSE == false ]] && return echo -e "${YELLOW}⚠ $1${NC}" | tee -a "$LOG_FILE" } +# Print info message print_info() { [[ $VERBOSE == false ]] && return echo -e "${PURPLE}ℹ $1${NC}" | tee -a "$LOG_FILE" } +# Prompt for confirmation with default response confirm() { local prompt="$1" local default="${2:-n}" @@ -114,61 +126,73 @@ confirm() { # --- VALIDATION FUNCTIONS --- +# Validate username (lowercase, numbers, hyphens, underscores, max 32 chars) validate_username() { local username="$1" [[ "$username" =~ ^[a-z_][a-z0-9_-]*$ && ${#username} -le 32 ]] } +# Validate hostname (alphanumeric, dots, hyphens, max 253 chars) validate_hostname() { local hostname="$1" [[ "$hostname" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$ && ! "$hostname" =~ \.\. ]] } +# Validate port (1024-65535) validate_port() { local port="$1" [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1024 && "$port" -le 65535 ]] } +# Validate SSH public key format validate_ssh_key() { local key="$1" [[ -n "$key" && "$key" =~ ^(ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)\ ]] } +# Validate timezone (check if exists in /usr/share/zoneinfo) validate_timezone() { local tz="$1" [[ -e "/usr/share/zoneinfo/$tz" ]] } +# Validate swap size (e.g., 2G, 512M) validate_swap_size() { local size="$1" [[ "$size" =~ ^[0-9]+[MG]$ ]] && [[ "${size%[MG]}" -ge 1 ]] } +# Validate UFW port format (e.g., 80/tcp, 123/udp) validate_ufw_port() { local port="$1" [[ "$port" =~ ^[0-9]+(/tcp|/udp)?$ ]] } +# Validate URL format validate_url() { local url="$1" [[ "$url" =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]] } +# Validate email format validate_email() { local email="$1" [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] } +# Validate SMTP port (common ports: 25, 2525, 8025, 587, 80, 465, 8465, 443) validate_smtp_port() { local port="$1" [[ "$port" =~ ^[0-9]+$ && "$port" =~ ^(25|2525|8025|587|80|465|8465|443)$ ]] } +# Validate ntfy token format (starts with tk_) validate_ntfy_token() { local token="$1" [[ "$token" =~ ^tk_[a-zA-Z0-9_-]+$ || -z "$token" ]] } +# Convert swap size (e.g., 2G, 512M) to bytes convert_to_bytes() { local size="$1" local unit="${size: -1}" @@ -182,8 +206,260 @@ convert_to_bytes() { fi } +# --- USER PROMPT FUNCTIONS --- + +# Prompt for admin username +prompt_username() { + while true; do + read -rp "$(echo -e "${CYAN}Enter username for new admin user: ${NC}")" USERNAME + if validate_username "$USERNAME"; then + if id "$USERNAME" &>/dev/null; then + print_warning "User '$USERNAME' already exists." + if confirm "Use this existing user?"; then USER_EXISTS=true; break; fi + else + USER_EXISTS=false; break + fi + else + print_error "Invalid username. Use lowercase letters, numbers, hyphens, underscores (max 32 chars)." + fi + done + PROMPTED_SETTINGS+=("USERNAME") +} + +# Prompt for server hostname +prompt_hostname() { + while true; do + read -rp "$(echo -e "${CYAN}Enter server hostname: ${NC}")" HOSTNAME + if validate_hostname "$HOSTNAME"; then + SERVER_NAME="$HOSTNAME" + PRETTY_NAME="${PRETTY_NAME:-$HOSTNAME}" + break + else + print_error "Invalid hostname." + fi + done + PROMPTED_SETTINGS+=("HOSTNAME") +} + +# Prompt for SSH port +prompt_ssh_port() { + while true; do + read -rp "$(echo -e "${CYAN}Enter custom SSH port (1024-65535) [5595]: ${NC}")" SSH_PORT + SSH_PORT=${SSH_PORT:-5595} + if validate_port "$SSH_PORT"; then break; else print_error "Invalid port number."; fi + done + PROMPTED_SETTINGS+=("SSH_PORT") +} + +# Prompt for timezone +prompt_timezone() { + while true; do + read -rp "$(echo -e "${CYAN}Enter desired timezone (e.g., Etc/UTC, America/New_York) [Etc/UTC]: ${NC}")" TIMEZONE + TIMEZONE=${TIMEZONE:-Etc/UTC} + if validate_timezone "$TIMEZONE"; then break; else print_error "Invalid timezone."; fi + done + PROMPTED_SETTINGS+=("TIMEZONE") +} + +# Prompt for swap size +prompt_swap_size() { + while true; do + read -rp "$(echo -e "${CYAN}Enter swap file size (e.g., 2G, 512M) [2G]: ${NC}")" SWAP_SIZE + SWAP_SIZE=${SWAP_SIZE:-2G} + if validate_swap_size "$SWAP_SIZE"; then + local swap_size_bytes=$(convert_to_bytes "$SWAP_SIZE") + local available_space=$(df / | awk 'NR==2 {print $4 * 1024}') # Convert to bytes + if [[ $available_space -lt $swap_size_bytes ]]; then + print_warning "Insufficient disk space for $SWAP_SIZE swap. Available: $((available_space / 1024 / 1024))M." + if ! confirm "Try a smaller swap size?"; then + SWAP_SIZE="" + SKIPPED_SETTINGS+=("swap configuration") + break + fi + else + break + fi + else + print_error "Invalid size. Use format like '2G' or '512M'." + fi + done + [[ -n "$SWAP_SIZE" ]] && PROMPTED_SETTINGS+=("SWAP_SIZE") +} + +# Prompt for UFW ports (comma-separated) +prompt_ufw_ports() { + if confirm "Add custom UFW ports (e.g., 80/tcp,443/tcp)?"; then + while true; do + read -rp "$(echo -e "${CYAN}Enter ports (comma-separated, e.g., 80/tcp,443/tcp): ${NC}")" UFW_PORTS + if [[ -z "$UFW_PORTS" ]]; then + print_info "No custom ports entered. Skipping." + UFW_PORTS="" + break + fi + local valid=true + for port in ${UFW_PORTS//,/ }; do + if ! validate_ufw_port "$port"; then + print_error "Invalid port format: $port. Use [/tcp|/udp]." + valid=false + break + fi + done + if [[ "$valid" == true ]]; then break; fi + done + PROMPTED_SETTINGS+=("UFW_PORTS") + else + UFW_PORTS="" + fi +} + +# Prompt for automatic updates +prompt_auto_updates() { + AUTO_UPDATES="no" + confirm "Enable automatic security updates?" && AUTO_UPDATES="yes" + PROMPTED_SETTINGS+=("AUTO_UPDATES") +} + +# Prompt for Docker installation +prompt_install_docker() { + INSTALL_DOCKER="no" + confirm "Install Docker Engine?" && INSTALL_DOCKER="yes" + PROMPTED_SETTINGS+=("INSTALL_DOCKER") +} + +# Prompt for Tailscale installation +prompt_install_tailscale() { + INSTALL_TAILSCALE="no" + confirm "Install Tailscale VPN?" && INSTALL_TAILSCALE="yes" + PROMPTED_SETTINGS+=("INSTALL_TAILSCALE") +} + +# Prompt for Tailscale login server +prompt_tailscale_login_server() { + read -rp "$(echo -e "${CYAN}Enter Tailscale login server (e.g., https://hs.mydomain.com, press Enter to skip): ${NC}")" TAILSCALE_LOGIN_SERVER + if [[ -n "$TAILSCALE_LOGIN_SERVER" && ! $(validate_url "$TAILSCALE_LOGIN_SERVER") ]]; then + print_error "Invalid Tailscale login server URL." + TAILSCALE_LOGIN_SERVER="" + fi + PROMPTED_SETTINGS+=("TAILSCALE_LOGIN_SERVER") +} + +# Prompt for Tailscale auth key +prompt_tailscale_auth_key() { + read -rp "$(echo -e "${CYAN}Enter Tailscale auth key: ${NC}")" TAILSCALE_AUTH_KEY + PROMPTED_SETTINGS+=("TAILSCALE_AUTH_KEY") +} + +# Prompt for Tailscale operator +prompt_tailscale_operator() { + read -rp "$(echo -e "${CYAN}Enter Tailscale operator username [$USERNAME]: ${NC}")" TAILSCALE_OPERATOR + TAILSCALE_OPERATOR=${TAILSCALE_OPERATOR:-$USERNAME} + if [[ -n "$TAILSCALE_OPERATOR" && ! $(validate_username "$TAILSCALE_OPERATOR") ]]; then + print_error "Invalid Tailscale operator username." + TAILSCALE_OPERATOR="$USERNAME" + fi + PROMPTED_SETTINGS+=("TAILSCALE_OPERATOR") +} + +# Prompt for Tailscale DNS acceptance +prompt_tailscale_accept_dns() { + TAILSCALE_ACCEPT_DNS="yes" + confirm "Accept Tailscale DNS?" "y" || TAILSCALE_ACCEPT_DNS="no" + PROMPTED_SETTINGS+=("TAILSCALE_ACCEPT_DNS") +} + +# Prompt for Tailscale routes acceptance +prompt_tailscale_accept_routes() { + TAILSCALE_ACCEPT_ROUTES="yes" + confirm "Accept Tailscale routes?" "y" || TAILSCALE_ACCEPT_ROUTES="no" + PROMPTED_SETTINGS+=("TAILSCALE_ACCEPT_ROUTES") +} + +# Prompt for SMTP server +prompt_smtp_server() { + read -rp "$(echo -e "${CYAN}Enter SMTP server [mail.smtp2go.com]: ${NC}")" SMTP_SERVER + SMTP_SERVER=${SMTP_SERVER:-mail.smtp2go.com} + if [[ ! "$SMTP_SERVER" =~ ^[a-zA-Z0-9.-]+$ ]]; then + print_error "Invalid SMTP server." + SMTP_SERVER="" + fi + PROMPTED_SETTINGS+=("SMTP_SERVER") +} + +# Prompt for SMTP port +prompt_smtp_port() { + read -rp "$(echo -e "${CYAN}Enter SMTP port [587]: ${NC}")" SMTP_PORT + SMTP_PORT=${SMTP_PORT:-587} + if ! validate_smtp_port "$SMTP_PORT"; then + print_error "Invalid SMTP port (use 25,2525,8025,587,80,465,8465,443)." + SMTP_PORT="" + fi + PROMPTED_SETTINGS+=("SMTP_PORT") +} + +# Prompt for SMTP username +prompt_smtp_user() { + read -rp "$(echo -e "${CYAN}Enter SMTP username: ${NC}")" SMTP_USER + if [[ -z "$SMTP_USER" ]]; then + print_error "SMTP username cannot be empty." + SMTP_USER="" + fi + PROMPTED_SETTINGS+=("SMTP_USER") +} + +# Prompt for SMTP password +prompt_smtp_pass() { + read -sp "$(echo -e "${CYAN}Enter SMTP password: ${NC}")" SMTP_PASS + echo + if [[ -z "$SMTP_PASS" ]]; then + print_error "SMTP password cannot be empty." + SMTP_PASS="" + fi + PROMPTED_SETTINGS+=("SMTP_PASS") +} + +# Prompt for SMTP from email +prompt_smtp_from() { + read -rp "$(echo -e "${CYAN}Enter SMTP from email address: ${NC}")" SMTP_FROM + if [[ -z "$SMTP_FROM" || ! $(validate_email "$SMTP_FROM") ]]; then + print_error "Invalid SMTP from email." + SMTP_FROM="" + fi + PROMPTED_SETTINGS+=("SMTP_FROM") +} + +# Prompt for SMTP to email +prompt_smtp_to() { + read -rp "$(echo -e "${CYAN}Enter SMTP to email address: ${NC}")" SMTP_TO + if [[ -z "$SMTP_TO" || ! $(validate_email "$SMTP_TO") ]]; then + print_error "Invalid SMTP to email." + SMTP_TO="" + fi + PROMPTED_SETTINGS+=("SMTP_TO") +} + +# Prompt for ntfy server +prompt_ntfy_server() { + read -rp "$(echo -e "${CYAN}Enter ntfy server URL (e.g., https://ntfy.mydomain.com/ovps, press Enter to skip): ${NC}")" NTFY_SERVER + if [[ -n "$NTFY_SERVER" && ! $(validate_url "$NTFY_SERVER") ]]; then + print_error "Invalid ntfy server URL." + NTFY_SERVER="" + fi + PROMPTED_SETTINGS+=("NTFY_SERVER") +} + +# Prompt for ntfy token +prompt_ntfy_token() { + read -rp "$(echo -e "${CYAN}Enter ntfy token: ${NC}")" NTFY_TOKEN + if [[ -n "$NTFY_TOKEN" && ! $(validate_ntfy_token "$NTFY_TOKEN") ]]; then + print_error "Invalid ntfy token (must start with tk_)." + NTFY_TOKEN="" + fi + PROMPTED_SETTINGS+=("NTFY_TOKEN") +} + # --- CONFIG FILE LOADING --- +# Load and validate configuration from file load_config() { local config_file="$1" if [[ ! -f "$config_file" ]]; then @@ -319,7 +595,7 @@ load_config() { fi if [[ ${#errors[@]} -gt 0 ]]; then - for error in "${errors[@]}"; do + [[ $VERBOSE == true ]] && for error in "${errors[@]}"; do print_error "$error" done if [[ $VERBOSE == true ]]; then @@ -370,222 +646,130 @@ load_config() { return 0 } -# --- USER PROMPT FUNCTIONS --- +# --- FULL INTERACTIVE CONFIG --- -prompt_username() { - while true; do - read -rp "$(echo -e "${CYAN}Enter username for new admin user: ${NC}")" USERNAME - if validate_username "$USERNAME"; then - if id "$USERNAME" &>/dev/null; then - print_warning "User '$USERNAME' already exists." - if confirm "Use this existing user?"; then USER_EXISTS=true; break; fi - else - USER_EXISTS=false; break - fi - else - print_error "Invalid username. Use lowercase letters, numbers, hyphens, underscores (max 32 chars)." +# Collect all configuration interactively +full_interactive_config() { + prompt_username + prompt_hostname + read -rp "$(echo -e "${CYAN}Enter a 'pretty' hostname (optional): ${NC}")" PRETTY_NAME + [[ -z "$PRETTY_NAME" ]] && PRETTY_NAME="$SERVER_NAME" + prompt_ssh_port + prompt_timezone + prompt_swap_size + prompt_ufw_ports + prompt_auto_updates + prompt_install_docker + prompt_install_tailscale + if [[ "$INSTALL_TAILSCALE" == "yes" ]]; then + prompt_tailscale_login_server + if [[ -n "$TAILSCALE_LOGIN_SERVER" ]]; then + prompt_tailscale_auth_key + prompt_tailscale_operator + prompt_tailscale_accept_dns + prompt_tailscale_accept_routes fi - done - PROMPTED_SETTINGS+=("USERNAME") -} - -prompt_hostname() { - while true; do - read -rp "$(echo -e "${CYAN}Enter server hostname: ${NC}")" HOSTNAME - if validate_hostname "$HOSTNAME"; then - SERVER_NAME="$HOSTNAME" - PRETTY_NAME="${PRETTY_NAME:-$HOSTNAME}" - break - else - print_error "Invalid hostname." + fi + if confirm "Configure system monitoring with SMTP and/or ntfy?"; then + prompt_smtp_server + if [[ -n "$SMTP_SERVER" ]]; then + prompt_smtp_port + prompt_smtp_user + prompt_smtp_pass + prompt_smtp_from + prompt_smtp_to + fi + prompt_ntfy_server + if [[ -n "$NTFY_SERVER" ]]; then + prompt_ntfy_token fi - done - PROMPTED_SETTINGS+=("HOSTNAME") -} - -prompt_ssh_port() { - while true; do - read -rp "$(echo -e "${CYAN}Enter custom SSH port (1024-65535) [5595]: ${NC}")" SSH_PORT - SSH_PORT=${SSH_PORT:-5595} - if validate_port "$SSH_PORT"; then break; else print_error "Invalid port number."; fi - done - PROMPTED_SETTINGS+=("SSH_PORT") -} - -prompt_timezone() { - while true; do - read -rp "$(echo -e "${CYAN}Enter desired timezone (e.g., Etc/UTC, America/New_York) [Etc/UTC]: ${NC}")" TIMEZONE - TIMEZONE=${TIMEZONE:-Etc/UTC} - if validate_timezone "$TIMEZONE"; then break; else print_error "Invalid timezone."; fi - done - PROMPTED_SETTINGS+=("TIMEZONE") -} - -prompt_swap_size() { - while true; do - read -rp "$(echo -e "${CYAN}Enter swap file size (e.g., 2G, 512M) [2G]: ${NC}")" SWAP_SIZE - SWAP_SIZE=${SWAP_SIZE:-2G} - if validate_swap_size "$SWAP_SIZE"; then break; else print_error "Invalid size. Use format like '2G' or '512M'."; fi - done - PROMPTED_SETTINGS+=("SWAP_SIZE") -} - -prompt_ufw_ports() { - if confirm "Add custom UFW ports (e.g., 80/tcp, 443/tcp)?"; then - while true; do - read -rp "$(echo -e "${CYAN}Enter ports (comma-separated, e.g., 80/tcp,443/tcp): ${NC}")" UFW_PORTS - if [[ -z "$UFW_PORTS" ]]; then - print_info "No custom ports entered. Skipping." - UFW_PORTS="" - break - fi - local valid=true - for port in ${UFW_PORTS//,/ }; do - if ! validate_ufw_port "$port"; then - print_error "Invalid port format: $port. Use [/tcp|/udp]." - valid=false - break - fi - done - if [[ "$valid" == true ]]; then break; fi - done - PROMPTED_SETTINGS+=("UFW_PORTS") - else - UFW_PORTS="" fi } -prompt_auto_updates() { - AUTO_UPDATES="no" - confirm "Enable automatic security updates?" && AUTO_UPDATES="yes" - PROMPTED_SETTINGS+=("AUTO_UPDATES") -} - -prompt_install_docker() { - INSTALL_DOCKER="no" - confirm "Install Docker Engine?" && INSTALL_DOCKER="yes" - PROMPTED_SETTINGS+=("INSTALL_DOCKER") -} - -prompt_install_tailscale() { - INSTALL_TAILSCALE="no" - confirm "Install Tailscale VPN?" && INSTALL_TAILSCALE="yes" - PROMPTED_SETTINGS+=("INSTALL_TAILSCALE") -} - -prompt_tailscale_login_server() { - read -rp "$(echo -e "${CYAN}Enter Tailscale login server (e.g., https://hs.mydomain.com, press Enter to skip): ${NC}")" TAILSCALE_LOGIN_SERVER - if [[ -n "$TAILSCALE_LOGIN_SERVER" && ! $(validate_url "$TAILSCALE_LOGIN_SERVER") ]]; then - print_error "Invalid Tailscale login server URL." - TAILSCALE_LOGIN_SERVER="" - fi - PROMPTED_SETTINGS+=("TAILSCALE_LOGIN_SERVER") -} - -prompt_tailscale_auth_key() { - read -rp "$(echo -e "${CYAN}Enter Tailscale auth key: ${NC}")" TAILSCALE_AUTH_KEY - PROMPTED_SETTINGS+=("TAILSCALE_AUTH_KEY") -} - -prompt_tailscale_operator() { - read -rp "$(echo -e "${CYAN}Enter Tailscale operator username [$USERNAME]: ${NC}")" TAILSCALE_OPERATOR - TAILSCALE_OPERATOR=${TAILSCALE_OPERATOR:-$USERNAME} - if [[ -n "$TAILSCALE_OPERATOR" && ! $(validate_username "$TAILSCALE_OPERATOR") ]]; then - print_error "Invalid Tailscale operator username." - TAILSCALE_OPERATOR="$USERNAME" - fi - PROMPTED_SETTINGS+=("TAILSCALE_OPERATOR") -} - -prompt_tailscale_accept_dns() { - TAILSCALE_ACCEPT_DNS="yes" - confirm "Accept Tailscale DNS?" "y" || TAILSCALE_ACCEPT_DNS="no" - PROMPTED_SETTINGS+=("TAILSCALE_ACCEPT_DNS") -} - -prompt_tailscale_accept_routes() { - TAILSCALE_ACCEPT_ROUTES="yes" - confirm "Accept Tailscale routes?" "y" || TAILSCALE_ACCEPT_ROUTES="no" - PROMPTED_SETTINGS+=("TAILSCALE_ACCEPT_ROUTES") -} - -prompt_smtp_server() { - read -rp "$(echo -e "${CYAN}Enter SMTP server [mail.smtp2go.com]: ${NC}")" SMTP_SERVER - SMTP_SERVER=${SMTP_SERVER:-mail.smtp2go.com} - if [[ ! "$SMTP_SERVER" =~ ^[a-zA-Z0-9.-]+$ ]]; then - print_error "Invalid SMTP server." - SMTP_SERVER="" - fi - PROMPTED_SETTINGS+=("SMTP_SERVER") -} - -prompt_smtp_port() { - read -rp "$(echo -e "${CYAN}Enter SMTP port [587]: ${NC}")" SMTP_PORT - SMTP_PORT=${SMTP_PORT:-587} - if ! validate_smtp_port "$SMTP_PORT"; then - print_error "Invalid SMTP port (use 25,2525,8025,587,80,465,8465,443)." - SMTP_PORT="" - fi - PROMPTED_SETTINGS+=("SMTP_PORT") -} - -prompt_smtp_user() { - read -rp "$(echo -e "${CYAN}Enter SMTP username: ${NC}")" SMTP_USER - if [[ -z "$SMTP_USER" ]]; then - print_error "SMTP username cannot be empty." - SMTP_USER="" - fi - PROMPTED_SETTINGS+=("SMTP_USER") -} - -prompt_smtp_pass() { - read -sp "$(echo -e "${CYAN}Enter SMTP password: ${NC}")" SMTP_PASS - echo - if [[ -z "$SMTP_PASS" ]]; then - print_error "SMTP password cannot be empty." - SMTP_PASS="" - fi - PROMPTED_SETTINGS+=("SMTP_PASS") -} - -prompt_smtp_from() { - read -rp "$(echo -e "${CYAN}Enter SMTP from email address: ${NC}")" SMTP_FROM - if [[ -z "$SMTP_FROM" || ! $(validate_email "$SMTP_FROM") ]]; then - print_error "Invalid SMTP from email." - SMTP_FROM="" - fi - PROMPTED_SETTINGS+=("SMTP_FROM") -} - -prompt_smtp_to() { - read -rp "$(echo -e "${CYAN}Enter SMTP to email address: ${NC}")" SMTP_TO - if [[ -z "$SMTP_TO" || ! $(validate_email "$SMTP_TO") ]]; then - print_error "Invalid SMTP to email." - SMTP_TO="" - fi - PROMPTED_SETTINGS+=("SMTP_TO") -} - -prompt_ntfy_server() { - read -rp "$(echo -e "${CYAN}Enter ntfy server URL (e.g., https://ntfy.mydomain.com/ovps, press Enter to skip): ${NC}")" NTFY_SERVER - if [[ -n "$NTFY_SERVER" && ! $(validate_url "$NTFY_SERVER") ]]; then - print_error "Invalid ntfy server URL." - NTFY_SERVER="" - fi - PROMPTED_SETTINGS+=("NTFY_SERVER") -} - -prompt_ntfy_token() { - read -rp "$(echo -e "${CYAN}Enter ntfy token: ${NC}")" NTFY_TOKEN - if [[ -n "$NTFY_TOKEN" && ! $(validate_ntfy_token "$NTFY_TOKEN") ]]; then - print_error "Invalid ntfy token (must start with tk_)." - NTFY_TOKEN="" - fi - PROMPTED_SETTINGS+=("NTFY_TOKEN") -} - # --- CORE FUNCTIONS --- +# Check system compatibility and prerequisites +check_system() { + print_section "System Compatibility Check" + + # Verify root privileges + if [[ $(id -u) -ne 0 ]]; then + print_error "This script must be run as root (e.g., sudo ./setup_harden_debian_ubuntu.sh)." + exit 1 + fi + print_success "Running with root privileges." + + # Detect container environment + if [[ -f /proc/1/cgroup ]] && grep -qE '(docker|lxc|kubepod)' /proc/1/cgroup; then + IS_CONTAINER=true + print_warning "Container environment detected. Some features (like swap) will be skipped." + fi + + # Check OS compatibility + if [[ -f /etc/os-release ]]; then + source /etc/os-release + ID="$ID" + UBUNTU_CODENAME="$UBUNTU_CODENAME" + if [[ $ID == "debian" && $VERSION_ID == "12" ]] || \ + [[ $ID == "ubuntu" && $VERSION_ID =~ ^(20.04|22.04|24.04|24.10)$ ]]; then + print_success "Compatible OS detected: $PRETTY_NAME" + else + print_warning "Script not tested on $PRETTY_NAME. This is for Debian 12 or Ubuntu 20.04/22.04/24.04 LTS." + if ! confirm "Continue anyway?"; then exit 1; fi + fi + else + print_error "This does not appear to be a Debian or Ubuntu system." + exit 1 + fi + + # Check SSH daemon presence + if ! dpkg -l openssh-server | grep -q ^ii; then + print_warning "openssh-server not installed. It will be installed in the next step." + elif command -v sshd >/dev/null || command -v dropbear >/dev/null; then + if systemctl is-enabled ssh.service >/dev/null 2>&1 || systemctl is-active ssh.service >/dev/null 2>&1; then + print_info "Preliminary check: ssh.service detected." + SSH_SERVICE="ssh.service" + elif systemctl is-enabled sshd.service >/dev/null 2>&1 || systemctl is-active sshd.service >/dev/null 2>&1; then + print_info "Preliminary check: sshd.service detected." + SSH_SERVICE="sshd.service" + elif ps aux | grep -q "[s]shd\|[d]ropbear"; then + print_warning "SSH daemon running but no standard service detected. Assuming sshd." + SSH_SERVICE="sshd.service" + else + print_warning "No SSH service or daemon detected. Ensure SSH is working after package installation." + fi + else + print_error "No SSH daemon (sshd or dropbear) detected. Please install openssh-server or dropbear." + exit 1 + fi + + # Verify internet connectivity + if curl -s --head https://deb.debian.org >/dev/null || curl -s --head https://archive.ubuntu.com >/dev/null; then + print_success "Internet connectivity confirmed." + else + print_error "No internet connectivity. Please check your network." + exit 1 + fi + + # Check log directory permissions + if [[ ! -w /var/log ]]; then + print_error "Failed to write to /var/log. Cannot create log file." + exit 1 + fi + + # Fix /etc/shadow permissions + SHADOW_PERMS=$(stat -c %a /etc/shadow) + if [[ "$SHADOW_PERMS" != "640" ]]; then + print_info "Fixing /etc/shadow permissions to 640..." + chmod 640 /etc/shadow + chown root:shadow /etc/shadow + log "Fixed /etc/shadow permissions to 640." + fi + + log "System compatibility check completed." +} + +# Install required dependencies check_dependencies() { print_section "Checking Dependencies" local missing_deps=() @@ -593,6 +777,8 @@ check_dependencies() { command -v sudo >/dev/null || missing_deps+=("sudo") command -v gpg >/dev/null || missing_deps+=("gpg") command -v postmap >/dev/null || missing_deps+=("postfix") + [[ -n "${SMTP_SERVER:-}" ]] && ! command -v mail >/dev/null && missing_deps+=("mailutils") + [[ -n "${SMTP_SERVER:-}" ]] && ! command -v swaks >/dev/null && missing_deps+=("swaks") if [[ ${#missing_deps[@]} -gt 0 ]]; then print_info "Installing missing dependencies: ${missing_deps[*]}" @@ -607,72 +793,7 @@ check_dependencies() { log "Dependency check completed." } -check_system() { - print_section "System Compatibility Check" - - if [[ $(id -u) -ne 0 ]]; then - print_error "This script must be run as root (e.g., sudo ./setup_harden_debian_ubuntu.sh)." - exit 1 - fi - print_success "Running with root privileges." - - if [[ -f /proc/1/cgroup ]] && grep -qE '(docker|lxc|kubepod)' /proc/1/cgroup; then - IS_CONTAINER=true - print_warning "Container environment detected. Some features (like swap) will be skipped." - fi - - if [[ -f /etc/os-release ]]; then - source /etc/os-release - ID="$ID" - if [[ $ID == "debian" && $VERSION_ID == "12" ]] || \ - [[ $ID == "ubuntu" && $VERSION_ID =~ ^(20.04|22.04|24.04|24.10)$ ]]; then - print_success "Compatible OS detected: $PRETTY_NAME" - else - print_warning "Script not tested on $PRETTY_NAME. This is for Debian 12 or Ubuntu 20.04/22.04/24.04 LTS." - if ! confirm "Continue anyway?"; then exit 1; fi - fi - else - print_error "This does not appear to be a Debian or Ubuntu system." - exit 1 - fi - - if ! dpkg -l openssh-server | grep -q ^ii; then - print_warning "openssh-server not installed. It will be installed in the next step." - else - if systemctl is-enabled ssh.service >/dev/null 2>&1 || systemctl is-active ssh.service >/dev/null 2>&1; then - print_info "Preliminary check: ssh.service detected." - elif systemctl is-enabled sshd.service >/dev/null 2>&1 || systemctl is-active sshd.service >/dev/null 2>&1; then - print_info "Preliminary check: sshd.service detected." - elif ps aux | grep -q "[s]shd"; then - print_warning "Preliminary check: SSH daemon running but no standard service detected." - else - print_warning "No SSH service or daemon detected. Ensure SSH is working after package installation." - fi - fi - - if curl -s --head https://deb.debian.org >/dev/null || curl -s --head https://archive.ubuntu.com >/dev/null; then - print_success "Internet connectivity confirmed." - else - print_error "No internet connectivity. Please check your network." - exit 1 - fi - - if [[ ! -w /var/log ]]; then - print_error "Failed to write to /var/log. Cannot create log file." - exit 1 - fi - - SHADOW_PERMS=$(stat -c %a /etc/shadow) - if [[ "$SHADOW_PERMS" != "640" ]]; then - print_info "Fixing /etc/shadow permissions to 640..." - chmod 640 /etc/shadow - chown root:shadow /etc/shadow - log "Fixed /etc/shadow permissions to 640." - fi - - log "System compatibility check completed." -} - +# Collect configuration from file or interactively collect_config() { print_section "Configuration Setup" @@ -710,41 +831,7 @@ collect_config() { log "Configuration collected: USER=$USERNAME, HOST=$SERVER_NAME, PORT=$SSH_PORT" } -full_interactive_config() { - prompt_username - prompt_hostname - read -rp "$(echo -e "${CYAN}Enter a 'pretty' hostname (optional): ${NC}")" PRETTY_NAME - [[ -z "$PRETTY_NAME" ]] && PRETTY_NAME="$SERVER_NAME" - prompt_ssh_port - prompt_timezone - prompt_auto_updates - prompt_install_docker - prompt_install_tailscale - if [[ "$INSTALL_TAILSCALE" == "yes" ]]; then - prompt_tailscale_login_server - if [[ -n "$TAILSCALE_LOGIN_SERVER" ]]; then - prompt_tailscale_auth_key - prompt_tailscale_operator - prompt_tailscale_accept_dns - prompt_tailscale_accept_routes - fi - fi - if confirm "Configure system monitoring with SMTP and/or ntfy?"; then - prompt_smtp_server - if [[ -n "$SMTP_SERVER" ]]; then - prompt_smtp_port - prompt_smtp_user - prompt_smtp_pass - prompt_smtp_from - prompt_smtp_to - fi - prompt_ntfy_server - if [[ -n "$NTFY_SERVER" ]]; then - prompt_ntfy_token - fi - fi -} - +# Install essential packages install_packages() { print_section "Package Installation" print_info "Updating package lists and upgrading system..." @@ -765,6 +852,7 @@ install_packages() { log "Package installation completed." } +# Set up user account and SSH keys setup_user() { print_section "User Management" @@ -853,13 +941,17 @@ setup_user() { log "User management completed." } +# Configure system settings (timezone, hostname, locales) configure_system() { print_section "System Configuration" mkdir -p "$BACKUP_DIR" && chmod 700 "$BACKUP_DIR" - cp /etc/hosts "$BACKUP_DIR/hosts.backup" - cp /etc/fstab "$BACKUP_DIR/fstab.backup" - cp /etc/sysctl.conf "$BACKUP_DIR/sysctl.conf.backup" 2>/dev/null || true + # Backup critical files with restricted permissions + cp /etc/hosts "$BACKUP_DIR/hosts.backup" && chmod 600 "$BACKUP_DIR/hosts.backup" + cp /etc/fstab "$BACKUP_DIR/fstab.backup" && chmod 600 "$BACKUP_DIR/fstab.backup" + cp /etc/sysctl.conf "$BACKUP_DIR/sysctl.conf.backup" 2>/dev/null && chmod 600 "$BACKUP_DIR/sysctl.conf.backup" || true + + # Configure timezone print_info "Configuring timezone..." if [[ $(timedatectl status | grep "Time zone" | awk '{print $3}') != "$TIMEZONE" ]]; then timedatectl set-timezone "$TIMEZONE" @@ -869,12 +961,14 @@ configure_system() { print_info "Timezone already set to $TIMEZONE." fi + # Configure locales if requested if confirm "Configure system locales interactively?"; then dpkg-reconfigure locales else print_info "Skipping locale configuration." fi + # Configure hostname print_info "Configuring hostname..." if [[ $(hostnamectl --static) != "$SERVER_NAME" ]]; then hostnamectl set-hostname "$SERVER_NAME" @@ -891,42 +985,28 @@ configure_system() { log "System configuration completed." } +# Harden SSH configuration configure_ssh() { print_section "SSH Hardening" + # Verify openssh-server is installed if ! dpkg -l openssh-server | grep -q ^ii; then print_error "openssh-server package is not installed. Please ensure it is installed." exit 1 fi - if [[ $ID == "ubuntu" ]] && { systemctl is-enabled ssh.service >/dev/null 2>&1 || systemctl is-active ssh.service >/dev/null 2>&1; }; then - SSH_SERVICE="ssh.service" - elif systemctl is-enabled sshd.service >/dev/null 2>&1 || systemctl is-active sshd.service >/dev/null 2>&1; then - SSH_SERVICE="sshd.service" - elif ps aux | grep -q "[s]shd"; then - print_warning "SSH daemon running but no standard service detected. Checking for socket activation..." - if systemctl is-active ssh.socket >/dev/null 2>&1; then - print_info "Disabling ssh.socket to enable ssh.service..." - systemctl disable --now ssh.socket - fi - SSH_SERVICE="ssh.service" - if ! systemctl enable --now "$SSH_SERVICE" >/dev/null 2>&1; then - print_error "Failed to enable and start $SSH_SERVICE. Attempting manual start..." - if ! /usr/sbin/sshd; then - print_error "Failed to start SSH daemon manually. Please check openssh-server installation." - exit 1 - fi - print_success "SSH daemon started manually." - fi + # Confirm SSH service + if [[ -n "$SSH_SERVICE" ]]; then + print_info "Using SSH service: $SSH_SERVICE" else - print_error "No SSH service or daemon detected. Please verify openssh-server installation." + print_error "SSH service not set. Please check openssh-server installation." exit 1 fi - print_info "Using SSH service: $SSH_SERVICE" log "Detected SSH service: $SSH_SERVICE" systemctl status "$SSH_SERVICE" --no-pager >> "$LOG_FILE" 2>&1 - ps aux | grep "[s]shd" >> "$LOG_FILE" 2>&1 + ps aux | grep "[s]shd\|[d]ropbear" >> "$LOG_FILE" 2>&1 + # Enable and start SSH service if not active if ! systemctl is-enabled "$SSH_SERVICE" >/dev/null 2>&1; then if ! systemctl enable "$SSH_SERVICE" >/dev/null 2>&1; then print_error "Failed to enable $SSH_SERVICE. Please check service status." @@ -945,13 +1025,14 @@ configure_ssh() { fi fi + # Generate SSH key if none exists CURRENT_SSH_PORT=$(ss -tuln | grep -E ":(22|.*$SSH_SERVICE.*)" | awk '{print $5}' | cut -d':' -f2 | head -n1 || echo "22") USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6) SSH_DIR="$USER_HOME/.ssh" SSH_KEY="$SSH_DIR/id_ed25519" AUTH_KEYS="$SSH_DIR/authorized_keys" - if [[ $LOCAL_KEY_ADDED == false ]] && [[ ! -s "$AUTH_KEYS" ]]; then + if [[ $LOCAL_KEY_ADDED == false && ! -s "$AUTH_KEYS" ]]; then print_info "No local key provided and no existing keys found. Generating new SSH key..." mkdir -p "$SSH_DIR" chmod 700 "$SSH_DIR" @@ -967,18 +1048,20 @@ configure_ssh() { print_info "SSH key(s) already present or added. Skipping key generation." fi + # Test SSH key authentication print_warning "SSH Key Authentication Required for Next Steps!" echo -e "${CYAN}Test SSH access from a SEPARATE terminal now: ssh -p $CURRENT_SSH_PORT $USERNAME@$SERVER_IP${NC}" - if ! confirm "Can you successfully log in using your SSH key?"; then print_error "SSH key authentication is mandatory to proceed. Please fix and re-run." exit 1 fi + # Backup SSH configuration print_info "Backing up original SSH config..." SSHD_BACKUP_FILE="$BACKUP_DIR/sshd_config.backup_$(date +%Y%m%d_%H%M%S)" - cp /etc/ssh/sshd_config "$SSHD_BACKUP_FILE" + cp /etc/ssh/sshd_config "$SSHD_BACKUP_FILE" && chmod 600 "$SSHD_BACKUP_FILE" + # Apply hardened SSH configuration NEW_SSH_CONFIG=$(mktemp) tee "$NEW_SSH_CONFIG" > /dev/null < /dev/null <<'EOF' ****************************************************************************** @@ -1006,6 +1089,7 @@ EOF EOF fi + # Test and apply SSH configuration print_info "Testing and restarting SSH service..." if sshd -t; then if ! systemctl restart "$SSH_SERVICE"; then @@ -1030,6 +1114,7 @@ EOF exit 1 fi + # Verify root SSH login is disabled print_info "Verifying root SSH login is disabled..." if grep -q "^PermitRootLogin no" /etc/ssh/sshd_config.d/99-hardening.conf; then print_success "Root SSH login is disabled." @@ -1044,9 +1129,9 @@ EOF print_success "Confirmed: Root SSH login is disabled." fi + # Final SSH connection test print_warning "CRITICAL: Test new SSH connection in a SEPARATE terminal NOW!" print_info "Use: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" - if ! confirm "Was the new SSH connection successful?"; then print_error "Aborting. Restoring original SSH configuration." cp "$SSHD_BACKUP_FILE" /etc/ssh/sshd_config @@ -1056,6 +1141,7 @@ EOF log "SSH hardening completed." } +# Configure UFW firewall configure_firewall() { print_section "Firewall Configuration (UFW)" if ufw status | grep -q "Status: active"; then @@ -1098,15 +1184,15 @@ configure_firewall() { print_info "HTTPS rule already exists." fi fi - if confirm "Add additional custom ports (e.g., 8080/tcp, 123/udp)?"; then + if confirm "Add additional custom ports (e.g., 8080/tcp,123/udp)?"; then while true; do - read -rp "$(echo -e "${CYAN}Enter ports (space-separated, e.g., 8080/tcp 123/udp): ${NC}")" CUSTOM_PORTS + read -rp "$(echo -e "${CYAN}Enter ports (comma-separated, e.g., 8080/tcp,123/udp): ${NC}")" CUSTOM_PORTS if [[ -z "$CUSTOM_PORTS" ]]; then print_info "No custom ports entered. Skipping." break fi valid=true - for port in $CUSTOM_PORTS; do + for port in ${CUSTOM_PORTS//,/ }; do if ! validate_ufw_port "$port"; then print_error "Invalid port format: $port. Use [/tcp|/udp]." valid=false @@ -1114,7 +1200,7 @@ configure_firewall() { fi done if [[ "$valid" == true ]]; then - for port in $CUSTOM_PORTS; do + for port in ${CUSTOM_PORTS//,/ }; do if ufw status | grep -qw "$port"; then print_info "Rule for $port already exists." else @@ -1124,8 +1210,6 @@ configure_firewall() { fi done break - else - print_info "Please try again." fi done fi @@ -1167,6 +1251,7 @@ configure_firewall() { log "Firewall configuration completed." } +# Configure Fail2Ban for intrusion prevention configure_fail2ban() { print_section "Fail2Ban Configuration" if ! dpkg -l fail2ban | grep -q ^ii; then @@ -1176,7 +1261,7 @@ configure_fail2ban() { print_info "Configuring Fail2Ban..." local jail_config="/etc/fail2ban/jail.d/custom.conf" mkdir -p /etc/fail2ban/jail.d - cp /etc/fail2ban/jail.conf "$BACKUP_DIR/jail.conf.backup" 2>/dev/null || true + cp /etc/fail2ban/jail.conf "$BACKUP_DIR/jail.conf.backup" 2>/dev/null && chmod 600 "$BACKUP_DIR/jail.conf.backup" || true NEW_JAIL_CONFIG=$(mktemp) tee "$NEW_JAIL_CONFIG" > /dev/null </dev/null || true + cp "$config_file" "$BACKUP_DIR/50unattended-upgrades.backup" 2>/dev/null && chmod 600 "$BACKUP_DIR/50unattended-upgrades.backup" || true if ! grep -q "Unattended-Upgrade::Automatic-Reboot" "$config_file"; then echo 'Unattended-Upgrade::Automatic-Reboot "true";' >> "$config_file" echo 'Unattended-Upgrade::Automatic-Reboot-Time "02:00";' >> "$config_file" @@ -1246,225 +1332,172 @@ configure_auto_updates() { log "Automatic updates configuration completed." } +# Configure system monitoring with SMTP and/or ntfy configure_monitoring() { print_section "System Monitoring Configuration" - if [[ -n "${SMTP_SERVER:-}" || -n "${NTFY_SERVER:-}" ]]; then - if [[ -n "${SMTP_SERVER:-}" && -n "${SMTP_PORT:-}" && -n "${SMTP_USER:-}" && -n "${SMTP_PASS:-}" && -n "${SMTP_FROM:-}" && -n "${SMTP_TO:-}" ]]; then - print_info "Configuring Postfix for SMTP2go..." - NEW_POSTFIX_CONFIG=$(mktemp) - tee "$NEW_POSTFIX_CONFIG" > /dev/null </dev/null && chmod 600 "$BACKUP_DIR/main.cf.backup" || true + cp /etc/postfix/sasl_passwd "$BACKUP_DIR/sasl_passwd.backup" 2>/dev/null && chmod 600 "$BACKUP_DIR/sasl_passwd.backup" || true + + # Configure Postfix + postconf -e \ + "smtp_sasl_auth_enable = yes" \ + "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \ + "smtp_sasl_security_options = noanonymous" \ + "smtp_tls_security_level = encrypt" \ + "smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt" \ + "relayhost = [$SMTP_SERVER]:$SMTP_PORT" \ + "mynetworks = 127.0.0.0/8" \ + "inet_interfaces = loopback-only" + + # Set up SASL credentials + echo "[$SMTP_SERVER]:$SMTP_PORT $SMTP_USER:$SMTP_PASS" > /etc/postfix/sasl_passwd + chmod 600 /etc/postfix/sasl_passwd + postmap /etc/postfix/sasl_passwd + chmod 600 /etc/postfix/sasl_passwd.db + + # Set up sender canonical map + echo "/.*/ $SMTP_FROM" > /etc/postfix/sender_canonical + chmod 644 /etc/postfix/sender_canonical + postmap /etc/postfix/sender_canonical + postconf -e "sender_canonical_maps = hash:/etc/postfix/sender_canonical" + + # Reload Postfix + if ! systemctl reload postfix; then + print_error "Failed to reload Postfix. Check 'journalctl -u postfix'." + exit 1 + fi + print_success "Postfix configured for SMTP monitoring." + + # Test SMTP configuration + print_info "Sending test email to $SMTP_TO..." + if command -v swaks >/dev/null; then + if swaks --to "$SMTP_TO" --from "$SMTP_FROM" --server "$SMTP_SERVER" --port "$SMTP_PORT" --auth-user "$SMTP_USER" --auth-password "$SMTP_PASS" --tls --silent 2 --body "Test email from $(hostname) at $(date)" --subject "Test Alert" >> "$LOG_FILE" 2>&1; then + print_success "SMTP test email sent to $SMTP_TO." else - cat "$NEW_POSTFIX_CONFIG" >> /etc/postfix/main.cf - print_info "Creating Postfix SASL password file..." - if [[ $VERBOSE == true && ( -z "$SMTP_USER" || -z "$SMTP_PASS" ) ]]; then - [[ -z "$SMTP_USER" ]] && prompt_smtp_user - [[ -z "$SMTP_PASS" ]] && prompt_smtp_pass - fi - if [[ -z "$SMTP_USER" || -z "$SMTP_PASS" ]]; then - print_error "SMTP credentials missing. Skipping SMTP configuration." - SKIPPED_SETTINGS+=("SMTP monitoring") - SMTP_SERVER="" - rm -f "$NEW_POSTFIX_CONFIG" - return - fi - echo "[$SMTP_SERVER]:$SMTP_PORT $SMTP_USER:$SMTP_PASS" > /etc/postfix/sasl_passwd - chmod 600 /etc/postfix/sasl_passwd - postmap /etc/postfix/sasl_passwd - systemctl restart postfix - print_info "Testing SMTP configuration..." - if echo "Test email from $(hostname)" | mail -s "Test Alert" "$SMTP_TO" 2>&1 | tee -a "$LOG_FILE"; then - print_success "SMTP test email sent to $SMTP_TO." - else - print_error "Failed to send test email. Check /var/log/mail.log." - fi + print_error "Failed to send test email. Check /var/log/mail.log for details." + exit 1 fi - rm -f "$NEW_POSTFIX_CONFIG" + elif echo "Test email from $(hostname) at $(date)" | mail -s "Test Alert" "$SMTP_TO" && grep -q "sent" /var/log/mail.log; then + print_success "SMTP test email sent to $SMTP_TO." else - print_info "Incomplete SMTP configuration. Skipping SMTP." - SKIPPED_SETTINGS+=("SMTP monitoring") - SMTP_SERVER="" + print_error "Failed to send test email. Check /var/log/mail.log for details." + exit 1 fi - if [[ -n "${NTFY_SERVER:-}" && -n "${NTFY_TOKEN:-}" ]]; then - print_info "Configuring ntfy notifications..." - curl -s -o /dev/null -H "Authorization: Bearer $NTFY_TOKEN" "$NTFY_SERVER" || { - print_error "Failed to connect to ntfy server. Check NTFY_SERVER and NTFY_TOKEN." - SKIPPED_SETTINGS+=("ntfy monitoring") - NTFY_SERVER="" - NTFY_TOKEN="" - } - elif [[ -n "${NTFY_SERVER:-}" && -z "${NTFY_TOKEN:-}" && $VERBOSE == true ]]; then - prompt_ntfy_token - if [[ -n "${NTFY_TOKEN:-}" ]]; then - print_info "Configuring ntfy notifications..." - curl -s -o /dev/null -H "Authorization: Bearer $NTFY_TOKEN" "$NTFY_SERVER" || { - print_error "Failed to connect to ntfy server. Check NTFY_SERVER and NTFY_TOKEN." - SKIPPED_SETTINGS+=("ntfy monitoring") - NTFY_SERVER="" - NTFY_TOKEN="" - } - else - print_info "No ntfy token provided. Skipping ntfy." - SKIPPED_SETTINGS+=("ntfy monitoring") - NTFY_SERVER="" - fi + log "SMTP monitoring configured." + else + print_info "Skipping SMTP monitoring configuration." + SKIPPED_SETTINGS+=("SMTP monitoring") + fi + + if [[ -n "${NTFY_SERVER:-}" && -n "${NTFY_TOKEN:-}" ]]; then + print_info "Configuring ntfy monitoring..." + if curl -s -H "Authorization: Bearer $NTFY_TOKEN" -d "Test notification from $(hostname) at $(date)" "$NTFY_SERVER" >/dev/null; then + print_success "Test ntfy notification sent." else - print_info "Incomplete ntfy configuration. Skipping ntfy." - SKIPPED_SETTINGS+=("ntfy monitoring") - NTFY_SERVER="" + print_error "Failed to send test ntfy notification. Check URL and token." + exit 1 fi - print_info "Setting up disk space monitoring and rsync backup cron jobs..." - local cron_file="/etc/cron.d/setup_harden_monitoring" - NEW_CRON_CONFIG=$(mktemp) - tee "$NEW_CRON_CONFIG" > /dev/null <<'EOF' -# Disk space monitoring (alert at 90% usage) -*/15 * * * * root df -h / | awk 'NR==2 {if ($5+0 >= 90) {print "Disk usage critical on '"$(hostname)"': " $0}}' | while read -r line; do [[ -n "${SMTP_TO:-}" ]] && echo "$line" | mail -s "Disk Alert" "${SMTP_TO}"; [[ -n "${NTFY_SERVER:-}" && -n "${NTFY_TOKEN:-}" ]] && curl -s -H "Authorization: Bearer ${NTFY_TOKEN}" -d "$line" "${NTFY_SERVER}"; done -# Rsync backup to /root/backup (daily at 1am) -0 1 * * * root rsync -a --delete --exclude '/root/backup' / /root/backup && { [[ -n "${SMTP_TO:-}" ]] && echo "Rsync backup completed on $(hostname)" | mail -s "Backup Complete" "${SMTP_TO}"; [[ -n "${NTFY_SERVER:-}" && -n "${NTFY_TOKEN:-}" ]] && curl -s -H "Authorization: Bearer ${NTFY_TOKEN}" -d "Rsync backup completed on $(hostname)" "${NTFY_SERVER}"; } + log "ntfy monitoring configured." + else + print_info "Skipping ntfy monitoring configuration." + SKIPPED_SETTINGS+=("ntfy monitoring") + fi + + # Configure monitoring cron job + print_info "Configuring monitoring cron job..." + mkdir -p /root/backup && chmod 700 /root/backup + NEW_CRON_CONFIG=$(mktemp) + tee "$NEW_CRON_CONFIG" > /dev/null <<'EOF' +# Disk space monitoring (alert on >80% usage) +0 * * * * root df -h | grep '^/dev/' | awk '{if ($5+0 > 80) print "Disk usage alert on " $1 ": " $5 " used";}' | while read -r line; do + [[ -n "${SMTP_TO:-}" ]] && echo "$line" | mail -s "Disk Usage Alert: $(hostname)" "${SMTP_TO:-nobody}"; + [[ -n "${NTFY_SERVER:-}" && -n "${NTFY_TOKEN:-}" ]] && curl -s -H "Authorization: Bearer ${NTFY_TOKEN:-}" -d "$line" "${NTFY_SERVER:-}"; +done +# Backup monitoring +0 1 * * * root mkdir -p /root/backup && rsync -a --delete --exclude '/root/backup' / /root/backup && find /root/backup -mtime +7 -delete EOF - if [[ -f "$cron_file" ]] && cmp -s "$cron_file" "$NEW_CRON_CONFIG"; then - print_info "Cron jobs already configured. Skipping." - else - mv "$NEW_CRON_CONFIG" "$cron_file" - chmod 644 "$cron_file" - print_success "Disk space and backup cron jobs configured." - fi + if [[ -f /etc/cron.d/system-monitoring ]] && cmp -s /etc/cron.d/system-monitoring "$NEW_CRON_CONFIG"; then + print_info "Monitoring cron job already configured." rm -f "$NEW_CRON_CONFIG" else - print_info "Skipping system monitoring configuration (no SMTP or ntfy settings provided)." - SKIPPED_SETTINGS+=("system monitoring") + mv "$NEW_CRON_CONFIG" /etc/cron.d/system-monitoring + chmod 644 /etc/cron.d/system-monitoring + systemctl restart cron + print_success "Monitoring cron job configured." fi - log "System monitoring configuration completed." + log "Monitoring configuration completed." } +# Install Docker Engine install_docker() { + print_section "Docker Installation" if [[ "$INSTALL_DOCKER" != "yes" ]]; then print_info "Skipping Docker installation." SKIPPED_SETTINGS+=("Docker installation") return 0 fi - print_section "Docker Installation" - if command -v docker >/dev/null 2>&1; then - print_info "Docker already installed." - if ! groups "$USERNAME" | grep -qw docker; then - print_info "Adding '$USERNAME' to docker group..." - usermod -aG docker "$USERNAME" - print_success "User '$USERNAME' added to docker group." - else - print_info "User '$USERNAME' already in docker group." - fi + if command -v docker >/dev/null; then + print_info "Docker is already installed." return 0 fi print_info "Installing Docker..." - curl -fsSL https://get.docker.com -o /tmp/get-docker.sh - chmod +x /tmp/get-docker.sh - if ! grep -q "docker" /tmp/get-docker.sh; then - print_error "Downloaded Docker install script appears invalid." - rm -f /tmp/get-docker.sh - exit 1 - fi - if ! sh /tmp/get-docker.sh; then + if ! curl -fsSL https://get.docker.com | sh; then print_error "Failed to install Docker." - rm -f /tmp/get-docker.sh exit 1 fi - rm -f /tmp/get-docker.sh - systemctl enable docker - systemctl start docker - print_info "Adding '$USERNAME' to docker group..." - usermod -aG docker "$USERNAME" - if groups "$USERNAME" | grep -qw docker; then - print_success "User '$USERNAME' added to docker group." - else - print_error "Failed to add '$USERNAME' to docker group." + if ! usermod -aG docker "$USERNAME"; then + print_error "Failed to add $USERNAME to docker group." exit 1 fi - if ! docker ps >/dev/null 2>&1; then - print_error "Docker service is not running or accessible. Check 'systemctl status docker'." - exit 1 - fi - print_success "Docker installation completed." - log "Docker installation and configuration completed." + print_success "Docker installed and $USERNAME added to docker group." + log "Docker installation completed." } +# Install Tailscale VPN install_tailscale() { + print_section "Tailscale Installation" if [[ "$INSTALL_TAILSCALE" != "yes" ]]; then print_info "Skipping Tailscale installation." SKIPPED_SETTINGS+=("Tailscale installation") return 0 fi - print_section "Tailscale VPN Installation" - if command -v tailscale >/dev/null 2>&1; then - print_info "Tailscale already installed." - if [[ -n "${TAILSCALE_LOGIN_SERVER:-}" && -n "${TAILSCALE_AUTH_KEY:-}" && -n "${TAILSCALE_OPERATOR:-}" ]]; then - print_info "Configuring Tailscale with Headscale..." - local ts_cmd="tailscale up --login-server=$TAILSCALE_LOGIN_SERVER --operator=$TAILSCALE_OPERATOR" - [[ "$TAILSCALE_ACCEPT_DNS" == "yes" ]] && ts_cmd="$ts_cmd --accept-dns" - [[ "$TAILSCALE_ACCEPT_ROUTES" == "yes" ]] && ts_cmd="$ts_cmd --accept-routes" - [[ -n "${TAILSCALE_AUTH_KEY:-}" ]] && ts_cmd="$ts_cmd --authkey=$TAILSCALE_AUTH_KEY" - ts_cmd="$ts_cmd --hostname=$SERVER_NAME" - if eval "$ts_cmd" 2>&1 | tee -a "$LOG_FILE"; then - if tailscale status | grep -q "$SERVER_NAME"; then - print_success "Tailscale configured and connected." - tailscale status | tee -a "$LOG_FILE" - else - print_error "Tailscale failed to connect. Check 'tailscale status' and log file." - exit 1 - fi - else - print_error "Failed to configure Tailscale. Check log file." - exit 1 - fi - else - print_info "Tailscale configuration incomplete. Skipping configuration." - SKIPPED_SETTINGS+=("Tailscale configuration") - fi + if command -v tailscale >/dev/null; then + print_info "Tailscale is already installed." return 0 fi - print_info "Installing Tailscale..." - curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | gpg --dearmor -o /usr/share/keyrings/tailscale-archive-keyring.gpg - curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list -o /etc/apt/sources.list.d/tailscale.list - if ! apt-get update -qq || ! apt-get install -y -qq tailscale; then + print_info "Installing Tailscale using official install script..." + if ! curl -fsSL https://tailscale.com/install.sh | sh; then print_error "Failed to install Tailscale." exit 1 fi - systemctl enable tailscaled - systemctl start tailscaled + print_success "Tailscale installed." if [[ -n "${TAILSCALE_LOGIN_SERVER:-}" && -n "${TAILSCALE_AUTH_KEY:-}" && -n "${TAILSCALE_OPERATOR:-}" ]]; then - print_info "Configuring Tailscale with Headscale..." - local ts_cmd="tailscale up --login-server=$TAILSCALE_LOGIN_SERVER --operator=$TAILSCALE_OPERATOR" - [[ "$TAILSCALE_ACCEPT_DNS" == "yes" ]] && ts_cmd="$ts_cmd --accept-dns" - [[ "$TAILSCALE_ACCEPT_ROUTES" == "yes" ]] && ts_cmd="$ts_cmd --accept-routes" - [[ -n "${TAILSCALE_AUTH_KEY:-}" ]] && ts_cmd="$ts_cmd --authkey=$TAILSCALE_AUTH_KEY" - ts_cmd="$ts_cmd --hostname=$SERVER_NAME" - if eval "$ts_cmd" 2>&1 | tee -a "$LOG_FILE"; then - if tailscale status | grep -q "$SERVER_NAME"; then - print_success "Tailscale configured and connected." - tailscale status | tee -a "$LOG_FILE" - else - print_error "Tailscale failed to connect. Check 'tailscale status' and log file." - exit 1 - fi + print_info "Configuring Tailscale..." + local up_args="--authkey=$TAILSCALE_AUTH_KEY --operator=$TAILSCALE_OPERATOR" + [[ -n "$TAILSCALE_LOGIN_SERVER" ]] && up_args="$up_args --login-server=$TAILSCALE_LOGIN_SERVER" + [[ "$TAILSCALE_ACCEPT_DNS" == "yes" ]] && up_args="$up_args --accept-dns=true" || up_args="$up_args --accept-dns=false" + [[ "$TAILSCALE_ACCEPT_ROUTES" == "yes" ]] && up_args="$up_args --accept-routes=true" || up_args="$up_args --accept-routes=false" + if tailscale up $up_args; then + print_success "Tailscale configured and started." else - print_error "Failed to configure Tailscale. Check log file." + print_error "Failed to configure Tailscale. Check 'tailscale status'." exit 1 fi else - print_info "Tailscale installed but not configured (missing login server, auth key, or operator)." - SKIPPED_SETTINGS+=("Tailscale configuration") + print_warning "Tailscale installed but not configured. Run 'sudo tailscale up' manually." fi - print_success "Tailscale installation completed." - log "Tailscale installation and configuration completed." + log "Tailscale installation completed." } +# Configure swap file configure_swap() { print_section "Swap Configuration" if [[ "$IS_CONTAINER" == true ]]; then @@ -1473,116 +1506,126 @@ configure_swap() { return 0 fi if [[ -z "${SWAP_SIZE:-}" ]]; then - print_info "No swap size specified. Skipping swap configuration." + print_info "No swap size specified. Skipping." SKIPPED_SETTINGS+=("swap configuration") return 0 fi - if ! validate_swap_size "$SWAP_SIZE"; then - print_error "Invalid SWAP_SIZE format: $SWAP_SIZE. Skipping swap configuration." - SKIPPED_SETTINGS+=("swap configuration") - return 0 - fi - local swap_size_bytes=$(convert_to_bytes "$SWAP_SIZE") if [[ -f /swapfile ]]; then - current_size=$(ls -l /swapfile | awk '{print $5}') - if [[ "$current_size" -eq "$swap_size_bytes" ]]; then - print_info "Swap file already exists with correct size ($SWAP_SIZE)." - return 0 - else - print_info "Removing existing swap file..." - swapoff /swapfile 2>/dev/null || true - rm -f /swapfile - fi + print_info "Swap file already exists." + return 0 fi print_info "Creating swap file of size $SWAP_SIZE..." - if ! fallocate -l "$swap_size_bytes" /swapfile; then - print_error "Failed to create swap file. Check disk space." + local swap_size_bytes=$(convert_to_bytes "$SWAP_SIZE") + local available_space=$(df / | awk 'NR==2 {print $4 * 1024}') # Convert to bytes + if [[ $available_space -lt $swap_size_bytes ]]; then + print_error "Insufficient disk space for $SWAP_SIZE swap. Available: $((available_space / 1024 / 1024))M." + exit 1 + fi + if ! fallocate -l "$SWAP_SIZE" /swapfile; then + print_error "Failed to create swap file." exit 1 fi chmod 600 /swapfile mkswap /swapfile swapon /swapfile - if ! grep -q "^/swapfile" /etc/fstab; then + cp /etc/fstab "$BACKUP_DIR/fstab.backup_$(date +%Y%m%d_%H%M%S)" && chmod 600 "$BACKUP_DIR/fstab.backup_$(date +%Y%m%d_%H%M%S)" + if ! grep -q "/swapfile" /etc/fstab; then echo "/swapfile none swap sw 0 0" >> /etc/fstab fi - sysctl vm.swappiness=10 - echo "vm.swappiness=10" >> /etc/sysctl.conf - if free | grep -q "Swap:"; then - print_success "Swap configured: $SWAP_SIZE" - free -h | tee -a "$LOG_FILE" - else - print_error "Failed to configure swap. Check 'free -h' and log file." - exit 1 - fi + print_success "Swap file configured." log "Swap configuration completed." } +# Configure time synchronization configure_time() { print_section "Time Synchronization" - if ! dpkg -l chrony | grep -q ^ii; then - print_error "chrony package is not installed." - exit 1 + if ! systemctl is-active --quiet chrony; then + systemctl enable chrony + systemctl start chrony fi - print_info "Configuring time synchronization with chrony..." - systemctl enable chrony - systemctl start chrony - if systemctl is-active --quiet chrony; then - print_success "Time synchronization configured." - chronyc tracking | tee -a "$LOG_FILE" + if chronyc tracking >/dev/null 2>&1; then + print_success "Time synchronization configured with chrony." else - print_error "Failed to start chrony service. Check 'journalctl -u chrony'." + print_error "Failed to verify chrony status." exit 1 fi - log "Time synchronization configuration completed." + log "Time synchronization completed." } +# Clean up temporary files and package cache cleanup() { print_section "Cleanup" - print_info "Cleaning up package cache and temporary files..." - apt-get clean - rm -rf /var/lib/apt/lists/* - rm -f /tmp/*.sh - find /var/log -type f -name "*.log" -exec truncate -s 0 {} \; + print_info "Cleaning up package cache..." + apt-get autoremove -y -qq + apt-get autoclean -qq + rm -rf /tmp/* print_success "Cleanup completed." log "Cleanup completed." } +# Print final summary and next steps print_summary() { print_section "Setup Summary" - echo -e "${GREEN}Setup completed successfully!${NC}" | tee -a "$LOG_FILE" - echo -e "\n${YELLOW}Configuration Applied:${NC}" - echo -e " Username: $USERNAME" - echo -e " Hostname: $SERVER_NAME" - echo -e " SSH Port: $SSH_PORT" - echo -e " Server IP: $SERVER_IP" - echo -e " Timezone: $TIMEZONE" - [[ -n "${SWAP_SIZE:-}" && "$IS_CONTAINER" == false ]] && echo -e " Swap Size: $SWAP_SIZE" - [[ -n "${UFW_PORTS:-}" ]] && echo -e " UFW Ports: $UFW_PORTS" - [[ "$AUTO_UPDATES" == "yes" ]] && echo -e " Auto Updates: Enabled" - [[ "$INSTALL_DOCKER" == "yes" ]] && echo -e " Docker: Installed" - [[ "$INSTALL_TAILSCALE" == "yes" ]] && echo -e " Tailscale: Installed" - [[ -n "${TAILSCALE_LOGIN_SERVER:-}" ]] && echo -e " Tailscale Server: $TAILSCALE_LOGIN_SERVER" - [[ -n "${SMTP_SERVER:-}" ]] && echo -e " SMTP Server: $SMTP_SERVER" - [[ -n "${NTFY_SERVER:-}" ]] && echo -e " ntfy Server: $NTFY_SERVER" - if [[ ${#PROMPTED_SETTINGS[@]} -gt 0 ]]; then - echo -e "\n${CYAN}Prompted Settings:${NC} ${PROMPTED_SETTINGS[*]}" + echo -e "${GREEN}Setup completed successfully!${NC}" + echo -e "\n${YELLOW}System Details:${NC}" + echo -e " Hostname: $SERVER_NAME" + echo -e " Pretty Name: $PRETTY_NAME" + echo -e " SSH Port: $SSH_PORT" + echo -e " Admin User: $USERNAME" + echo -e " Server IP: $SERVER_IP" + echo -e " Timezone: $TIMEZONE" + if [[ -n "${SWAP_SIZE:-}" && "$IS_CONTAINER" == false ]]; then + echo -e " Swap Size: $SWAP_SIZE" + fi + if [[ -n "${UFW_PORTS:-}" ]]; then + echo -e " Custom Ports: ${UFW_PORTS//,/ }" + fi + if [[ "$AUTO_UPDATES" == "yes" ]]; then + echo -e " Auto Updates: Enabled" + fi + if [[ "$INSTALL_DOCKER" == "yes" ]]; then + echo -e " Docker: Installed" + fi + if [[ "$INSTALL_TAILSCALE" == "yes" ]]; then + echo -e " Tailscale: Installed" + if [[ -n "${TAILSCALE_LOGIN_SERVER:-}" ]]; then + echo -e " Tailscale LS: $TAILSCALE_LOGIN_SERVER" + fi + fi + if [[ -n "${SMTP_SERVER:-}" ]]; then + echo -e " SMTP Alerts: Configured ($SMTP_TO)" + fi + if [[ -n "${NTFY_SERVER:-}" ]]; then + echo -e " ntfy Alerts: Configured ($NTFY_SERVER)" fi if [[ ${#SKIPPED_SETTINGS[@]} -gt 0 ]]; then echo -e "\n${YELLOW}Skipped Settings:${NC} ${SKIPPED_SETTINGS[*]}" fi echo -e "\n${YELLOW}Log File:${NC} $LOG_FILE" - echo -e "${YELLOW}Backups:${NC} $BACKUP_DIR" + echo -e "\n${YELLOW}Backups:${NC} $BACKUP_DIR" echo -e "\n${YELLOW}Next Steps:${NC}" - echo -e " 1. Verify SSH: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" - echo -e " 2. Check Firewall: sudo ufw status verbose" - echo -e " 3. Check Services: systemctl status ssh fail2ban chrony docker tailscaled postfix" - echo -e " 4. Reboot (recommended): sudo reboot" - echo -e "\n${YELLOW}Note:${NC} If using a VPS (e.g., DigitalOcean, AWS), ensure the edge firewall allows port $SSH_PORT and other configured ports." - log "Setup summary completed." + echo -e " - Verify SSH access: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" + echo -e " - Check UFW status: sudo ufw status" + echo -e " - Check Fail2Ban: sudo fail2ban-client status sshd" + if [[ "$INSTALL_DOCKER" == "yes" ]]; then + echo -e " - Test Docker: sudo -u $USERNAME docker run hello-world" + fi + if [[ "$INSTALL_TAILSCALE" == "yes" ]]; then + echo -e " - Check Tailscale: sudo tailscale status" + fi + echo -e " - Review logs: less $LOG_FILE" + echo -e "\n${GREEN}Thank you for using the Debian/Ubuntu Setup and Hardening Script!${NC}" + log "Setup summary printed." } +# Main function to orchestrate setup main() { print_header + mkdir -p "$(dirname "$LOG_FILE")" + touch "$LOG_FILE" + chmod 600 "$LOG_FILE" + log "Script started." + check_system check_dependencies collect_config @@ -1600,8 +1643,9 @@ main() { configure_time cleanup print_summary + + log "Script completed successfully." } # Execute main function main -```