From 5dc4e593e0a6fa063109b8f58555e5e126132cff Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:49:51 +0100 Subject: [PATCH] Update setup_harden_debian_ubuntu.sh --- setup_harden_debian_ubuntu.sh | 1618 +++++++++++---------------------- 1 file changed, 526 insertions(+), 1092 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index cb50c2a..3ba6c40 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,13 +1,40 @@ #!/bin/bash -# Debian/Ubuntu Server Setup and Hardening Script -# Version: 4.2 | 2025-06-26 -# Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS, 24.10 (experimental) +# Debian 12 and Ubuntu Server Hardening Interactive Script +# Version: 3.8 | 2025-06-26 +# Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS # -# 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. +# Description: +# This script provisions and hardens a fresh Debian 12 or Ubuntu server with essential security +# configurations, user management, SSH hardening, firewall setup, and optional features +# like Docker and Tailscale. It is designed to be idempotent, safe, and suitable for +# production environments. +# +# Prerequisites: +# - Run as root on a fresh Debian 12 or Ubuntu server (e.g., sudo ./setup_harden_debian_ubuntu.sh). +# - Internet connectivity is required for package installation. +# +# Usage: +# Download: wget https://raw.githubusercontent.com/buildplan/learning/refs/heads/main/setup_harden_debian_ubuntu.sh +# Make it executable: chmod +x setup_harden_debian_ubuntu.sh +# Run it: sudo ./setup_harden_debian_ubuntu.sh [--quiet] +# +# Options: +# --quiet: Suppress non-critical output for automation. +# +# Notes: +# - The script creates a log file in /var/log/setup_harden_debian_ubuntu_*.log. +# - Critical configurations are backed up before modification. Backup files are at /root/setup_harden_backup_*. +# - A new admin user is created with a mandatory password or SSH key for authentication. +# - Root SSH login is disabled; all access is via the new user with sudo privileges. +# - The user will be prompted to select a timezone, swap size, and custom firewall ports. +# - A reboot is recommended at the end to apply all changes. +# - Test the script in a VM before production use. +# +# Troubleshooting: +# - Check the log file for errors if the script fails. +# - If SSH access is lost, use the server console to restore /etc/ssh/sshd_config.backup_*. +# - Ensure sufficient disk space (>2GB) for swap file creation. set -euo pipefail # Exit on error, undefined vars, pipe failures @@ -26,75 +53,64 @@ NC='\033[0m' # No Color SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="/var/log/setup_harden_debian_ubuntu_$(date +%Y%m%d_%H%M%S).log" VERBOSE=true -CONFIG_FILE="" BACKUP_DIR="/root/setup_harden_backup_$(date +%Y%m%d_%H%M%S)" IS_CONTAINER=false SSHD_BACKUP_FILE="" LOCAL_KEY_ADDED=false SSH_SERVICE="" -ID="" -UBUNTU_CODENAME="" -SKIPPED_SETTINGS=() -PROMPTED_SETTINGS=() +ID="" # This will be populated from /etc/os-release # --- PARSE ARGUMENTS --- while [[ $# -gt 0 ]]; do case $1 in --quiet) VERBOSE=false; shift ;; - --config) CONFIG_FILE="$2"; shift 2 ;; *) shift ;; esac 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.2 | 2025-06-26 ║${NC}" + echo -e "${CYAN}║ v3.8 | 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 +# --- USER INTERACTION --- + confirm() { local prompt="$1" local default="${2:-n}" @@ -126,73 +142,42 @@ 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" + # Matches port (e.g., 8080) or port/protocol (e.g., 8080/tcp, 123/udp) [[ "$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}" @@ -206,595 +191,14 @@ 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 - print_error "Config file $config_file not found." - return 1 - fi - print_info "Loaded configuration from $config_file" - log "Loaded configuration from $config_file" - source "$config_file" - - # Initialize defaults - USERNAME="${USERNAME:-}" - HOSTNAME="${HOSTNAME:-$(hostname)}" # Fallback to current hostname - SERVER_NAME="$HOSTNAME" # Set SERVER_NAME immediately - PRETTY_NAME="${PRETTY_NAME:-$HOSTNAME}" # Set PRETTY_NAME with fallback - SSH_PORT="${SSH_PORT:-5595}" - TIMEZONE="${TIMEZONE:-Etc/UTC}" - SWAP_SIZE="${SWAP_SIZE:-2G}" - UFW_PORTS="${UFW_PORTS:-}" - AUTO_UPDATES="${AUTO_UPDATES:-no}" - INSTALL_DOCKER="${INSTALL_DOCKER:-no}" - INSTALL_TAILSCALE="${INSTALL_TAILSCALE:-no}" - TAILSCALE_LOGIN_SERVER="${TAILSCALE_LOGIN_SERVER:-}" - TAILSCALE_AUTH_KEY="${TAILSCALE_AUTH_KEY:-}" - TAILSCALE_OPERATOR="${TAILSCALE_OPERATOR:-${USERNAME:-}}" - TAILSCALE_ACCEPT_DNS="${TAILSCALE_ACCEPT_DNS:-yes}" - TAILSCALE_ACCEPT_ROUTES="${TAILSCALE_ACCEPT_ROUTES:-yes}" - SMTP_SERVER="${SMTP_SERVER:-}" - SMTP_PORT="${SMTP_PORT:-}" - SMTP_USER="${SMTP_USER:-}" - SMTP_PASS="${SMTP_PASS:-}" - SMTP_FROM="${SMTP_FROM:-}" - SMTP_TO="${SMTP_TO:-}" - NTFY_SERVER="${NTFY_SERVER:-}" - NTFY_TOKEN="${NTFY_TOKEN:-}" - - # Validate required fields - local errors=() - if [[ -z "$USERNAME" ]]; then - errors+=("Missing USERNAME") - elif ! validate_username "$USERNAME"; then - errors+=("Invalid USERNAME") - fi - if [[ -z "$HOSTNAME" ]]; then - errors+=("Missing HOSTNAME") - elif ! validate_hostname "$HOSTNAME"; then - errors+=("Invalid HOSTNAME") - SERVER_NAME="$(hostname)" # Fallback to current hostname if invalid - HOSTNAME="$SERVER_NAME" - fi - if [[ "$HOSTNAME" != *.* ]]; then - print_warning "Hostname '$HOSTNAME' is not an FQDN. Consider using an FQDN (e.g., $HOSTNAME.mydomain.com) for better compatibility." - fi - if [[ -z "$SSH_PORT" ]]; then - errors+=("Missing SSH_PORT") - elif ! validate_port "$SSH_PORT"; then - errors+=("Invalid SSH_PORT") - fi - if [[ -z "$TIMEZONE" ]]; then - errors+=("Missing TIMEZONE") - elif ! validate_timezone "$TIMEZONE"; then - errors+=("Invalid TIMEZONE") - fi - if [[ -n "$SWAP_SIZE" && ! "$SWAP_SIZE" =~ ^[0-9]+[MG]$ ]]; then - errors+=("Invalid SWAP_SIZE") - fi - if [[ -n "$UFW_PORTS" ]]; then - for port in ${UFW_PORTS//,/ }; do - if ! validate_ufw_port "$port"; then - errors+=("Invalid UFW_PORTS format: $port") - fi - done - fi - if [[ -n "$AUTO_UPDATES" && ! "$AUTO_UPDATES" =~ ^(yes|no)$ ]]; then - errors+=("Invalid AUTO_UPDATES (must be yes/no)") - fi - if [[ -n "$INSTALL_DOCKER" && ! "$INSTALL_DOCKER" =~ ^(yes|no)$ ]]; then - errors+=("Invalid INSTALL_DOCKER (must be yes/no)") - fi - if [[ -n "$INSTALL_TAILSCALE" && ! "$INSTALL_TAILSCALE" =~ ^(yes|no)$ ]]; then - errors+=("Invalid INSTALL_TAILSCALE (must be yes/no)") - fi - if [[ "$INSTALL_TAILSCALE" == "yes" && -n "$TAILSCALE_LOGIN_SERVER" ]]; then - if ! validate_url "$TAILSCALE_LOGIN_SERVER"; then - errors+=("Invalid TAILSCALE_LOGIN_SERVER") - fi - if [[ -z "$TAILSCALE_AUTH_KEY" ]]; then - errors+=("Missing TAILSCALE_AUTH_KEY") - fi - if [[ -z "$TAILSCALE_OPERATOR" ]]; then - errors+=("Missing TAILSCALE_OPERATOR") - elif ! validate_username "$TAILSCALE_OPERATOR"; then - errors+=("Invalid TAILSCALE_OPERATOR") - fi - if [[ -n "$TAILSCALE_ACCEPT_DNS" && ! "$TAILSCALE_ACCEPT_DNS" =~ ^(yes|no)$ ]]; then - errors+=("Invalid TAILSCALE_ACCEPT_DNS") - fi - if [[ -n "$TAILSCALE_ACCEPT_ROUTES" && ! "$TAILSCALE_ACCEPT_ROUTES" =~ ^(yes|no)$ ]]; then - errors+=("Invalid TAILSCALE_ACCEPT_ROUTES") - fi - fi - if [[ -n "$SMTP_SERVER" ]]; then - if [[ ! "$SMTP_SERVER" =~ ^[a-zA-Z0-9.-]+$ ]]; then - errors+=("Invalid SMTP_SERVER") - fi - if [[ -z "$SMTP_PORT" ]]; then - errors+=("Missing SMTP_PORT") - elif ! validate_smtp_port "$SMTP_PORT"; then - errors+=("Invalid SMTP_PORT") - fi - if [[ -z "$SMTP_USER" ]]; then - errors+=("Missing SMTP_USER") - fi - if [[ -z "$SMTP_PASS" ]]; then - errors+=("Missing SMTP_PASS") - fi - if [[ -z "$SMTP_FROM" ]]; then - errors+=("Missing SMTP_FROM") - elif ! validate_email "$SMTP_FROM"; then - errors+=("Invalid SMTP_FROM") - fi - if [[ -z "$SMTP_TO" ]]; then - errors+=("Missing SMTP_TO") - elif ! validate_email "$SMTP_TO"; then - errors+=("Invalid SMTP_TO") - fi - fi - if [[ -n "$NTFY_SERVER" ]]; then - if ! validate_url "$NTFY_SERVER"; then - errors+=("Invalid NTFY_SERVER") - fi - if [[ -z "$NTFY_TOKEN" && "$VERBOSE" == false ]]; then - errors+=("Missing NTFY_TOKEN (skipping ntfy in quiet mode)") - SKIPPED_SETTINGS+=("ntfy") - NTFY_SERVER="" - NTFY_TOKEN="" - elif [[ -n "$NTFY_TOKEN" && ! "$NTFY_TOKEN" =~ ^tk_[a-zA-Z0-9_-]+$ ]]; then - errors+=("Invalid NTFY_TOKEN") - fi - fi - - if [[ ${#errors[@]} -gt 0 ]]; then - [[ $VERBOSE == true ]] && for error in "${errors[@]}"; do - print_error "$error" - done - if [[ $VERBOSE == true ]]; then - print_info "Prompting for missing/invalid required settings..." - [[ -z "$USERNAME" || ! $(validate_username "$USERNAME") ]] && prompt_username - [[ -z "$HOSTNAME" || ! $(validate_hostname "$HOSTNAME") ]] && prompt_hostname - [[ -z "$SSH_PORT" || ! $(validate_port "$SSH_PORT") ]] && prompt_ssh_port - [[ -z "$TIMEZONE" || ! $(validate_timezone "$TIMEZONE") ]] && prompt_timezone - [[ -n "$SWAP_SIZE" && ! $(validate_swap_size "$SWAP_SIZE") ]] && prompt_swap_size - if [[ -n "$UFW_PORTS" ]]; then - local valid_ports=true - for port in ${UFW_PORTS//,/ }; do - if ! validate_ufw_port "$port"; then - valid_ports=false - break - fi - done - [[ "$valid_ports" == false ]] && prompt_ufw_ports - fi - [[ -n "$AUTO_UPDATES" && ! "$AUTO_UPDATES" =~ ^(yes|no)$ ]] && prompt_auto_updates - [[ -n "$INSTALL_DOCKER" && ! "$INSTALL_DOCKER" =~ ^(yes|no)$ ]] && prompt_install_docker - [[ -n "$INSTALL_TAILSCALE" && ! "$INSTALL_TAILSCALE" =~ ^(yes|no)$ ]] && prompt_install_tailscale - if [[ "$INSTALL_TAILSCALE" == "yes" && -n "$TAILSCALE_LOGIN_SERVER" ]]; then - [[ ! $(validate_url "$TAILSCALE_LOGIN_SERVER") ]] && prompt_tailscale_login_server - [[ -z "$TAILSCALE_AUTH_KEY" ]] && prompt_tailscale_auth_key - [[ -z "$TAILSCALE_OPERATOR" || ! $(validate_username "$TAILSCALE_OPERATOR") ]] && prompt_tailscale_operator - [[ -n "$TAILSCALE_ACCEPT_DNS" && ! "$TAILSCALE_ACCEPT_DNS" =~ ^(yes|no)$ ]] && prompt_tailscale_accept_dns - [[ -n "$TAILSCALE_ACCEPT_ROUTES" && ! "$TAILSCALE_ACCEPT_ROUTES" =~ ^(yes|no)$ ]] && prompt_tailscale_accept_routes - fi - if [[ -n "$SMTP_SERVER" ]]; then - [[ ! "$SMTP_SERVER" =~ ^[a-zA-Z0-9.-]+$ ]] && prompt_smtp_server - [[ -z "$SMTP_PORT" || ! $(validate_smtp_port "$SMTP_PORT") ]] && prompt_smtp_port - [[ -z "$SMTP_USER" ]] && prompt_smtp_user - [[ -z "$SMTP_PASS" ]] && prompt_smtp_pass - [[ -z "$SMTP_FROM" || ! $(validate_email "$SMTP_FROM") ]] && prompt_smtp_from - [[ -z "$SMTP_TO" || ! $(validate_email "$SMTP_TO") ]] && prompt_smtp_to - fi - if [[ -n "$NTFY_SERVER" ]]; then - [[ ! $(validate_url "$NTFY_SERVER") ]] && prompt_ntfy_server - [[ -z "$NTFY_TOKEN" || ! $(validate_ntfy_token "$NTFY_TOKEN") ]] && prompt_ntfy_token - fi - # Ensure SERVER_NAME and PRETTY_NAME are set after prompting - SERVER_NAME="$HOSTNAME" - PRETTY_NAME="${PRETTY_NAME:-$HOSTNAME}" - return 0 - else - print_error "Invalid or missing configuration in quiet mode. Using default hostname: $HOSTNAME" - SERVER_NAME="$HOSTNAME" - PRETTY_NAME="${PRETTY_NAME:-$HOSTNAME}" - return 0 - fi - fi - # Ensure SERVER_NAME and PRETTY_NAME are set if validation passes - SERVER_NAME="$HOSTNAME" - PRETTY_NAME="${PRETTY_NAME:-$HOSTNAME}" - return 0 -} - -# --- FULL INTERACTIVE CONFIG --- - -# 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="$HOSTNAME" - SERVER_NAME="$HOSTNAME" # Ensure SERVER_NAME is set - 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 - 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 -} - # --- 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=() command -v curl >/dev/null || missing_deps+=("curl") 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[*]}" @@ -809,25 +213,103 @@ check_dependencies() { log "Dependency check completed." } -# Collect configuration from file or interactively -collect_config() { - print_section "Configuration Setup" +check_system() { + print_section "System Compatibility Check" - if [[ -n "$CONFIG_FILE" || -f "/etc/setup_harden.conf" ]]; then - CONFIG_FILE=${CONFIG_FILE:-/etc/setup_harden.conf} - if ! load_config "$CONFIG_FILE"; then - if [[ $VERBOSE == false ]]; then - print_error "Configuration file invalid in quiet mode. Exiting." - exit 1 - else - print_info "Falling back to interactive mode due to config issues." - full_interactive_config - fi - fi - else - full_interactive_config + 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 + if [[ $ID == "debian" && $VERSION_ID == "12" ]] || \ + [[ $ID == "ubuntu" && $VERSION_ID =~ ^(20.04|22.04|24.04)$ ]]; 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 + + # Preliminary SSH service check + 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 + + # Check /etc/shadow permissions + if [[ ! -w /etc/shadow ]]; then + print_error "/etc/shadow is not writable. Check permissions (should be 640, root:shadow)." + 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_config() { + print_section "Configuration Setup" + 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 + while true; do + read -rp "$(echo -e "${CYAN}Enter server hostname: ${NC}")" SERVER_NAME + if validate_hostname "$SERVER_NAME"; then break; else print_error "Invalid hostname."; fi + done + read -rp "$(echo -e "${CYAN}Enter a 'pretty' hostname (optional): ${NC}")" PRETTY_NAME + [[ -z "$PRETTY_NAME" ]] && PRETTY_NAME="$SERVER_NAME" + while true; do + read -rp "$(echo -e "${CYAN}Enter custom SSH port (1024-65535) [2222]: ${NC}")" SSH_PORT + SSH_PORT=${SSH_PORT:-2222} + if validate_port "$SSH_PORT"; then break; else print_error "Invalid port number."; fi + done SERVER_IP=$(curl -s https://ifconfig.me 2>/dev/null || echo "unknown") print_info "Detected server IP: $SERVER_IP" echo -e "\n${YELLOW}Configuration Summary:${NC}" @@ -835,19 +317,10 @@ collect_config() { echo -e " Hostname: $SERVER_NAME" echo -e " SSH Port: $SSH_PORT" echo -e " Server IP: $SERVER_IP" - if [[ ${#PROMPTED_SETTINGS[@]} -gt 0 ]]; then - echo -e " Prompted: ${PROMPTED_SETTINGS[*]}" - fi - if [[ ${#SKIPPED_SETTINGS[@]} -gt 0 ]]; then - echo -e " Skipped: ${SKIPPED_SETTINGS[*]}" - fi - if [[ "$VERBOSE" == true ]]; then - if ! confirm "\nContinue with this configuration?" "y"; then print_info "Exiting."; exit 0; fi - fi + if ! confirm "\nContinue with this configuration?" "y"; then print_info "Exiting."; exit 0; fi log "Configuration collected: USER=$USERNAME, HOST=$SERVER_NAME, PORT=$SSH_PORT" } -# Install essential packages install_packages() { print_section "Package Installation" print_info "Updating package lists and upgrading system..." @@ -860,7 +333,7 @@ install_packages() { ufw fail2ban unattended-upgrades chrony \ rsync wget vim htop iotop nethogs ncdu tree \ rsyslog cron jq gawk coreutils perl skopeo git \ - openssh-client openssh-server postfix libsasl2-modules curl; then + openssh-client openssh-server; then print_error "Failed to install one or more essential packages." exit 1 fi @@ -868,7 +341,6 @@ install_packages() { log "Package installation completed." } -# Set up user account and SSH keys setup_user() { print_section "User Management" @@ -957,34 +429,37 @@ 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" - print_success "Timezone set to $TIMEZONE." - log "Timezone set to $TIMEZONE." - else - print_info "Timezone already set to $TIMEZONE." - fi + 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 + if [[ $(timedatectl status | grep "Time zone" | awk '{print $3}') != "$TIMEZONE" ]]; then + timedatectl set-timezone "$TIMEZONE" + print_success "Timezone set to $TIMEZONE." + log "Timezone set to $TIMEZONE." + else + print_info "Timezone already set to $TIMEZONE." + fi + break + else + print_error "Invalid timezone. View list with 'timedatectl list-timezones'." + fi + done - # 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" @@ -1001,28 +476,55 @@ configure_system() { log "System configuration completed." } -# Harden SSH configuration configure_ssh() { print_section "SSH Hardening" - # Verify openssh-server is installed + # Ensure 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 - # Confirm SSH service - if [[ -n "$SSH_SERVICE" ]]; then - print_info "Using SSH service: $SSH_SERVICE" + # Detect SSH service name and handle socket activation + 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." + echo -e "${CYAN}Run these commands to diagnose:${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - systemctl status ssh${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - systemctl status sshd${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - ps aux | grep sshd${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - dpkg -l openssh-server${NC}" | tee -a "$LOG_FILE" + exit 1 + fi + print_success "SSH daemon started manually." + fi else - print_error "SSH service not set. Please check openssh-server installation." + print_error "No SSH service or daemon detected. Please verify openssh-server installation and SSH daemon status." + echo -e "${CYAN}Run these commands to diagnose:${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - systemctl status ssh${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - systemctl status sshd${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - ps aux | grep sshd${NC}" | tee -a "$LOG_FILE" + echo -e "${CYAN} - dpkg -l openssh-server${NC}" | tee -a "$LOG_FILE" 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\|[d]ropbear" >> "$LOG_FILE" 2>&1 + ps aux | grep "[s]shd" >> "$LOG_FILE" 2>&1 - # Enable and start SSH service if not active + # Ensure SSH service is enabled and running 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." @@ -1041,14 +543,13 @@ 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" @@ -1064,20 +565,19 @@ 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" && chmod 600 "$SSHD_BACKUP_FILE" + cp /etc/ssh/sshd_config "$SSHD_BACKUP_FILE" - # Apply hardened SSH configuration + # Check if SSH config needs updating NEW_SSH_CONFIG=$(mktemp) tee "$NEW_SSH_CONFIG" > /dev/null </dev/null; then print_error "Root SSH login is still possible! Please check SSH configuration." exit 1 @@ -1145,9 +645,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 @@ -1157,7 +657,6 @@ EOF log "SSH hardening completed." } -# Configure UFW firewall configure_firewall() { print_section "Firewall Configuration (UFW)" if ufw status | grep -q "Status: active"; then @@ -1173,78 +672,52 @@ configure_firewall() { else print_info "SSH rule for port $SSH_PORT already exists." fi - if [[ -n "${UFW_PORTS:-}" ]]; then - for port in ${UFW_PORTS//,/ }; do - if ufw status | grep -qw "$port"; then - print_info "Rule for $port already exists." - else - ufw allow "$port" comment "Custom port $port" - print_success "Added rule for $port." - log "Added UFW rule for $port." - fi - done - elif [[ $VERBOSE == true ]]; then - if confirm "Allow HTTP traffic (port 80)?"; then - if ! ufw status | grep -qw "80/tcp"; then - ufw allow http comment 'HTTP' - print_success "HTTP traffic allowed." - else - print_info "HTTP rule already exists." - fi + if confirm "Allow HTTP traffic (port 80)?"; then + if ! ufw status | grep -qw "80/tcp"; then + ufw allow http comment 'HTTP' + print_success "HTTP traffic allowed." + else + print_info "HTTP rule already exists." fi - if confirm "Allow HTTPS traffic (port 443)?"; then - if ! ufw status | grep -qw "443/tcp"; then - ufw allow https comment 'HTTPS' - print_success "HTTPS traffic allowed." - else - print_info "HTTPS rule already exists." - fi + fi + if confirm "Allow HTTPS traffic (port 443)?"; then + if ! ufw status | grep -qw "443/tcp"; then + ufw allow https comment 'HTTPS' + print_success "HTTPS traffic allowed." + else + print_info "HTTPS rule already exists." fi - if confirm "Add additional custom ports (e.g., 8080/tcp,123/udp)?"; then - while true; do - 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 - if ! validate_ufw_port "$port"; then - print_error "Invalid port format: $port. Use [/tcp|/udp]." - valid=false - break - fi - done - if [[ "$valid" == true ]]; then - for port in ${CUSTOM_PORTS//,/ }; do - if ufw status | grep -qw "$port"; then - print_info "Rule for $port already exists." - else - ufw allow "$port" comment "Custom port $port" - print_success "Added rule for $port." - log "Added UFW rule for $port." - fi - done + fi + 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 + if [[ -z "$CUSTOM_PORTS" ]]; then + print_info "No custom ports entered. Skipping." + break + fi + valid=true + for port in $CUSTOM_PORTS; do + if ! validate_ufw_port "$port"; then + print_error "Invalid port format: $port. Use [/tcp|/udp]." + valid=false break fi done - fi - fi - if [[ "$INSTALL_TAILSCALE" == "yes" ]]; then - if ! ufw status | grep -qw "41641/udp"; then - ufw allow 41641/udp comment 'Tailscale' - print_success "Tailscale port 41641/udp allowed." - else - print_info "Tailscale port 41641/udp rule already exists." - fi - fi - if [[ -n "${SMTP_PORT:-}" ]]; then - if ! ufw status | grep -qw "$SMTP_PORT/tcp"; then - ufw allow "$SMTP_PORT"/tcp comment 'SMTP' - print_success "SMTP port $SMTP_PORT/tcp allowed." - else - print_info "SMTP port $SMTP_PORT/tcp rule already exists." - fi + if [[ "$valid" == true ]]; then + for port in $CUSTOM_PORTS; do + if ufw status | grep -qw "$port"; then + print_info "Rule for $port already exists." + else + ufw allow "$port" comment "Custom port $port" + print_success "Added rule for $port." + log "Added UFW rule for $port." + fi + done + break + else + print_info "Please try again." + fi + done fi print_info "Enabling firewall..." if ! ufw --force enable; then @@ -1267,396 +740,360 @@ 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 - print_error "fail2ban package is not installed." - exit 1 - fi - 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 && chmod 600 "$BACKUP_DIR/jail.conf.backup" || true - NEW_JAIL_CONFIG=$(mktemp) - tee "$NEW_JAIL_CONFIG" > /dev/null <> "$NEW_JAIL_CONFIG" + # Collect all SSH ports (main SSH_PORT and any custom ports that are SSH-related) + SSH_PORTS="$SSH_PORT" + if [[ -n "${CUSTOM_PORTS:-}" ]]; then + for port in $CUSTOM_PORTS; do + port_num="${port%%/*}" + if [[ "$port_num" != "$SSH_PORT" && "$port" =~ ^[0-9]+(/tcp)?$ ]]; then + SSH_PORTS="$SSH_PORTS,$port_num" fi done fi - if [[ -f "$jail_config" ]] && cmp -s "$jail_config" "$NEW_JAIL_CONFIG"; then + NEW_FAIL2BAN_CONFIG=$(mktemp) + tee "$NEW_FAIL2BAN_CONFIG" > /dev/null </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" - fi - if ! grep -q "Unattended-Upgrade::Mail" "$config_file"; then - if [[ -n "${SMTP_TO:-}" ]]; then - echo "Unattended-Upgrade::Mail \"$SMTP_TO\";" >> "$config_file" - echo 'Unattended-Upgrade::MailReport "on-change";' >> "$config_file" + print_section "Automatic Security Updates" + if confirm "Enable automatic security updates via unattended-upgrades?"; then + if ! dpkg -l unattended-upgrades | grep -q ^ii; then + print_error "unattended-upgrades package is not installed." + exit 1 fi - fi - systemctl restart unattended-upgrades - if systemctl is-active --quiet unattended-upgrades; then - print_success "Automatic updates configured." + print_info "Configuring unattended upgrades..." + echo "unattended-upgrades unattended-upgrades/enable_auto_updates boolean true" | debconf-set-selections + DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive unattended-upgrades + print_success "Automatic security updates enabled." else - print_error "Failed to start unattended-upgrades service." - exit 1 + print_info "Skipping automatic security updates." fi log "Automatic updates configuration completed." } -# Configure system monitoring with SMTP and/or ntfy -configure_monitoring() { - print_section "System Monitoring Configuration" - - # Ensure backup directory exists with restricted permissions - mkdir -p "$BACKUP_DIR" && chmod 700 "$BACKUP_DIR" - - 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 SMTP monitoring..." - - # Backup Postfix configuration - cp /etc/postfix/main.cf "$BACKUP_DIR/main.cf.backup" 2>/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 - cp /etc/aliases "$BACKUP_DIR/aliases.backup" 2>/dev/null && chmod 600 "$BACKUP_DIR/aliases.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" - - # Configure /etc/aliases to suppress root alias warning - if ! grep -q "^root:" /etc/aliases; then - echo "root: $SMTP_TO" >> /etc/aliases - newaliases - print_success "Configured /etc/aliases with root alias to $SMTP_TO." - else - print_info "/etc/aliases already configured." - fi - - # 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 - print_error "Failed to send test email. Check /var/log/mail.log for details." - exit 1 - fi - elif echo "Test email from $(hostname) at $(date)" | mail -s "Test Alert" "$SMTP_TO"; then - sleep 2 - if tail -n 50 /var/log/mail.log | grep -qE "status=(sent|delivered|completed)"; then - print_success "SMTP test email sent to $SMTP_TO." - else - print_error "Failed to send test email. Check /var/log/mail.log for details." - exit 1 - fi - 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_error "Failed to send test ntfy notification. Check URL and token." - exit 1 - fi - 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 /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 - 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 "Monitoring configuration completed." -} - -# Install Docker Engine install_docker() { - print_section "Docker Installation" - if [[ "$INSTALL_DOCKER" != "yes" ]]; then + if ! confirm "Install Docker Engine (Optional)?"; then print_info "Skipping Docker installation." - SKIPPED_SETTINGS+=("Docker installation") return 0 fi - if command -v docker >/dev/null; then - print_info "Docker is already installed." + print_section "Docker Installation" + if command -v docker >/dev/null 2>&1; then + print_info "Docker already installed." return 0 fi - print_info "Installing Docker..." - if ! curl -fsSL https://get.docker.com | sh; then - print_error "Failed to install Docker." + print_info "Removing old container runtimes..." + apt-get remove -y -qq docker docker-engine docker.io containerd runc 2>/dev/null || true + print_info "Adding Docker's official GPG key and repository..." + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${ID} $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list + print_info "Installing Docker packages..." + if ! apt-get update -qq || ! apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then + print_error "Failed to install Docker packages." exit 1 fi - if ! usermod -aG docker "$USERNAME"; then - print_error "Failed to add $USERNAME to docker group." + print_info "Adding '$USERNAME' to docker group..." + getent group docker >/dev/null || groupadd docker + if ! groups "$USERNAME" | grep -qw docker; then + usermod -aG docker "$USERNAME" + print_success "User '$USERNAME' added to docker group." + else + print_info "User '$USERNAME' is already in docker group." + fi + print_info "Configuring Docker daemon..." + NEW_DOCKER_CONFIG=$(mktemp) + tee "$NEW_DOCKER_CONFIG" >¼ /dev/null <&1 | tee -a "$LOG_FILE" | grep -q "Hello from Docker"; then + print_success "Docker sanity check passed." + else + print_error "Docker hello-world test failed. Please verify installation." exit 1 fi - print_success "Docker installed and $USERNAME added to docker group." + print_warning "NOTE: '$USERNAME' must log out and back in to use Docker without sudo." log "Docker installation completed." } -# Install Tailscale VPN install_tailscale() { - print_section "Tailscale Installation" - if [[ "$INSTALL_TAILSCALE" != "yes" ]]; then + if ! confirm "Install Tailscale VPN (Optional)?"; then print_info "Skipping Tailscale installation." - SKIPPED_SETTINGS+=("Tailscale installation") return 0 fi - if command -v tailscale >/dev/null; then - print_info "Tailscale is already installed." + print_section "Tailscale VPN Installation" + if command -v tailscale >/dev/null 2>&1; then + print_info "Tailscale already installed." return 0 fi - print_info "Installing Tailscale using official install script..." - if ! curl -fsSL https://tailscale.com/install.sh | sh; then - print_error "Failed to install Tailscale." + print_info "Installing Tailscale..." + curl -fsSL https://tailscale.com/install.sh -o /tmp/tailscale_install.sh + chmod +x /tmp/tailscale_install.sh + if ! grep -q "tailscale" /tmp/tailscale_install.sh; then + print_error "Downloaded Tailscale install script appears invalid." + rm -f /tmp/tailscale_install.sh exit 1 fi - print_success "Tailscale installed." - if [[ -n "${TAILSCALE_LOGIN_SERVER:-}" && -n "${TAILSCALE_AUTH_KEY:-}" && -n "${TAILSCALE_OPERATOR:-}" ]]; then - 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 && tailscale status >/dev/null 2>&1; then - print_success "Tailscale configured and started." - else - print_error "Failed to configure Tailscale. Check 'tailscale status'." - exit 1 - fi - else - print_warning "Tailscale installed but not configured. Run 'sudo tailscale up' manually." + if ! /tmp/tailscale_install.sh; then + print_error "Failed to install Tailscale." + rm -f /tmp/tailscale_install.sh + exit 1 fi + rm -f /tmp/tailscale_install.sh + print_warning "ACTION REQUIRED: Run 'sudo tailscale up' after script finishes." + print_success "Tailscale installation complete." log "Tailscale installation completed." } -# Configure swap file configure_swap() { + if [[ $IS_CONTAINER == true ]]; then + print_info "Swap configuration skipped in container." + return 0 + fi print_section "Swap Configuration" - if [[ "$IS_CONTAINER" == true ]]; then - print_info "Skipping swap configuration in container environment." - SKIPPED_SETTINGS+=("swap configuration") - return 0 + local existing_swap + existing_swap=$(swapon --show --noheadings | awk '{print $1}' || true) + if [[ -n "$existing_swap" ]]; then + local current_size + current_size=$(ls -lh "$existing_swap" | awk '{print $5}') + print_info "Existing swap file found: $existing_swap ($current_size)" + if confirm "Modify existing swap file size?"; then + while true; do + read -rp "$(echo -e "${CYAN}Enter new swap size (e.g., 2G, 512M) [current: $current_size]: ${NC}")" SWAP_SIZE + SWAP_SIZE=${SWAP_SIZE:-$current_size} + if validate_swap_size "$SWAP_SIZE"; then + break + else + print_error "Invalid size. Use format like '2G' or '512M'." + fi + done + REQUIRED_SPACE=$(convert_to_bytes "$SWAP_SIZE") + AVAILABLE_SPACE=$(df -k / | tail -n 1 | awk '{print $4}') + if (( AVAILABLE_SPACE < REQUIRED_SPACE / 1024 )); then + print_error "Insufficient disk space for $SWAP_SIZE swap file. Available: $((AVAILABLE_SPACE / 1024))MB" + exit 1 + fi + print_info "Disabling existing swap file..." + swapoff "$existing_swap" || { print_error "Failed to disable swap file."; exit 1; } + print_info "Resizing swap file to $SWAP_SIZE..." + if ! fallocate -l "$SWAP_SIZE" "$existing_swap" || ! chmod 600 "$existing_swap" || ! mkswap "$existing_swap" || ! swapon "$existing_swap"; then + print_error "Failed to resize or enable swap file." + exit 1 + fi + print_success "Swap file resized to $SWAP_SIZE." + else + print_info "Keeping existing swap file." + return 0 + fi + else + if ! confirm "Configure a swap file (recommended for < 4GB RAM)?"; then + print_info "Skipping swap configuration." + return 0 + fi + 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 + REQUIRED_SPACE=$(convert_to_bytes "$SWAP_SIZE") + AVAILABLE_SPACE=$(df -k / | tail -n 1 | awk '{print $4}') + if (( AVAILABLE_SPACE < REQUIRED_SPACE / 1024 )); then + print_error "Insufficient disk space for $SWAP_SIZE swap file. Available: $((AVAILABLE_SPACE / 1024))MB" + exit 1 + fi + print_info "Creating $SWAP_SIZE swap file..." + if ! fallocate -l "$SWAP_SIZE" /swapfile || ! chmod 600 /swapfile || ! mkswap /swapfile || ! swapon /swapfile; then + print_error "Failed to create or enable swap file." + exit 1 + fi + if ! grep -q '^/swapfile ' /etc/fstab; then + echo '/swapfile none swap sw 0 0' >> /etc/fstab + fi + print_success "Swap file created: $SWAP_SIZE" fi - if [[ -z "${SWAP_SIZE:-}" ]]; then - print_info "No swap size specified. Skipping." - SKIPPED_SETTINGS+=("swap configuration") - return 0 + print_info "Optimizing swap settings..." + NEW_SWAP_CONFIG=$(mktemp) + tee "$NEW_SWAP_CONFIG" > /dev/null </dev/null fi - if [[ -f /swapfile ]]; then - print_info "Swap file already exists." - return 0 - fi - print_info "Creating swap file of size $SWAP_SIZE..." - 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 - 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 - print_success "Swap file configured." + print_success "Swap configured successfully." + swapon --show | tee -a "$LOG_FILE" + free -h | tee -a "$LOG_FILE" log "Swap configuration completed." } -# Configure time synchronization -configure_time() { +configure_time_sync() { print_section "Time Synchronization" - if ! systemctl is-active --quiet chrony; then - systemctl enable chrony - systemctl start chrony - fi - if chronyc tracking >/dev/null 2>&1; then - print_success "Time synchronization configured with chrony." + print_info "Ensuring chrony is active..." + systemctl enable --now chrony + sleep 2 + if systemctl is-active --quiet chrony; then + print_success "Chrony is active for time synchronization." + chronyc tracking | tee -a "$LOG_FILE" else - print_error "Failed to verify chrony status." + print_error "Chrony service failed to start." exit 1 fi log "Time synchronization completed." } -# Clean up temporary files and package cache -cleanup() { - print_section "Cleanup" - 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." +final_cleanup() { + print_section "Final System Cleanup" + print_info "Running final system update and cleanup..." + if ! apt-get update -qq; then + print_error "Failed to update package lists." + exit 1 + fi + if ! apt-get upgrade -y -qq || ! apt-get --purge autoremove -y -qq || ! apt-get autoclean -y -qq; then + print_error "Final system cleanup failed." + exit 1 + fi + systemctl daemon-reload + print_success "Final system update and cleanup complete." + log "Final system cleanup completed." } -# Print final summary and next steps -print_summary() { - print_section "Setup Summary" - 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" +generate_summary() { + print_section "Setup Complete!" + print_info "Checking critical services..." + for service in "$SSH_SERVICE" fail2ban chrony; do + if systemctl is-active --quiet "$service"; then + print_success "Service $service is active." + else + print_error "Service $service is NOT active." + fi + done + if ufw status | grep -q "Status: active"; then + print_success "Service ufw is active." + else + print_error "Service ufw is NOT active." 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" + if command -v docker >/dev/null 2>&1; then + if systemctl is-active --quiet docker; then + print_success "Service docker is active." + else + print_error "Service docker is NOT active." fi fi - if [[ -n "${SMTP_SERVER:-}" ]]; then - echo -e " SMTP Alerts: Configured ($SMTP_TO)" + echo -e "\n${GREEN}Server setup and hardening script has finished successfully.${NC}" + echo + echo -e "${YELLOW}Configuration Summary:${NC}" + echo -e " Admin User: $USERNAME" + echo -e " Hostname: $SERVER_NAME" + echo -e " SSH Port: $SSH_PORT" + echo -e " Server IP: $SERVER_IP" + echo + echo -e "${PURPLE}Log File: ${LOG_FILE}${NC}" + echo -e "${PURPLE}Backups: ${BACKUP_DIR}${NC}" + echo + echo -e "${CYAN}Post-Reboot Verification Steps:${NC}" + echo -e " - SSH access: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" + echo -e " - Firewall rules: sudo ufw status verbose" + echo -e " - Time sync: chronyc tracking" + echo -e " - Fail2Ban status: sudo fail2ban-client status sshd" + echo -e " - Swap status: swapon --show && free -h" + if command -v docker >/dev/null 2>&1; then + echo -e " - Docker status: docker ps" fi - if [[ -n "${NTFY_SERVER:-}" ]]; then - echo -e " ntfy Alerts: Configured ($NTFY_SERVER)" + if command -v tailscale >/dev/null 2>&1; then + echo -e " - Tailscale status: sudo tailscale status" fi - if [[ ${#SKIPPED_SETTINGS[@]} -gt 0 ]]; then - echo -e "\n${YELLOW}Skipped Settings:${NC} ${SKIPPED_SETTINGS[*]}" + print_warning "\nA reboot is required to apply all changes cleanly." + if [[ $VERBOSE == true ]]; then + if confirm "Reboot now?" "y"; then + print_info "Rebooting now..." + sleep 3 + reboot + else + print_warning "Please reboot manually with 'sudo reboot'." + fi + else + print_warning "Quiet mode enabled. Please reboot manually with 'sudo reboot'." fi - echo -e "\n${YELLOW}Log File:${NC} $LOG_FILE" - echo -e "\n${YELLOW}Backups:${NC} $BACKUP_DIR" - echo -e "\n${YELLOW}Next Steps:${NC}" - 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." + log "Script finished successfully." } -# Main function to orchestrate setup -main() { - print_header - mkdir -p "$(dirname "$LOG_FILE")" - touch "$LOG_FILE" - chmod 600 "$LOG_FILE" - log "Script started." +handle_error() { + local exit_code=$? + local line_no="$1" + print_error "An error occurred on line $line_no (exit code: $exit_code)." + print_info "Log file: $LOG_FILE" + print_info "Backups: $BACKUP_DIR" + exit $exit_code +} - check_system +main() { + trap 'handle_error $LINENO' ERR + touch "$LOG_FILE" && chmod 600 "$LOG_FILE" + log "Starting Debian/Ubuntu hardening script." + + print_header check_dependencies + check_system collect_config install_packages setup_user @@ -1665,16 +1102,13 @@ main() { configure_firewall configure_fail2ban configure_auto_updates - configure_monitoring + configure_time_sync install_docker install_tailscale configure_swap - configure_time - cleanup - print_summary - - log "Script completed successfully." + final_cleanup + generate_summary } -# Execute main function +# Run main function main "$@"