#!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script # Version: 0.62 | 2025-08-04 # Changelog: # - v0.62: Added modular execution with interactive menu and --tasks flag # - v0.61: Display Lynis suggestions in summary, hide tailscale auth key, cleanup temp files # - v0.60: CI for shellcheck # - v0.59: Added sysctl security settings and self-update check # - v0.58: Improved fail2ban to parse ufw logs # - v0.57: Fixed silent failure in test_backup() # - v0.56: Made tailscale config optional # - v0.55: Improved setup_user() with ssh-keygen options # - v0.54: Enhanced rollback_ssh_changes() for Ubuntu # - v0.53: Fixed test_backup() for non-root sudo users # - v0.52: Added SSH rollback for Ubuntu 24.10 # - v0.51: Corrected repo links # - v0.50: Versioning format change # - v4.3: Added SHA256 integrity verification # - v4.2: Added Lynis and debsecan, backup testing # - v4.1: Added Tailscale configuration # - v4.0: Added automated backup configuration # # Description: # Provisions and hardens a fresh Debian 12 or Ubuntu server with security configurations, # user management, SSH hardening, firewall, and optional features like Docker, Tailscale, # and backups. Supports modular execution via --tasks or interactive menu. # # Usage: # sudo -E ./du_setup.sh [--quiet] [--tasks=/dev/null 2>&1 && tput setaf 1 >/dev/null 2>&1; then RED=$(tput setaf 1) GREEN=$(tput setaf 2) YELLOW="$(tput bold)$(tput setaf 3)" BLUE=$(tput setaf 4) PURPLE=$(tput setaf 5) CYAN=$(tput setaf 6) BOLD=$(tput bold) NC=$(tput sgr0) else RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' BOLD='' fi # --- Logging & Print Functions --- log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" } print_header() { [[ $VERBOSE == false ]] && return echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" echo -e "${CYAN}║ v0.21 | 2025-08-43 ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo } 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() { [[ $VERBOSE == false ]] && return echo -e "${GREEN}✓ $1${NC}" | tee -a "$LOG_FILE" } print_error() { echo -e "${RED}✗ $1${NC}" | tee -a "$LOG_FILE" } print_warning() { [[ $VERBOSE == false ]] && return echo -e "${YELLOW}⚠ $1${NC}" | tee -a "$LOG_FILE" } print_info() { [[ $VERBOSE == false ]] && return echo -e "${PURPLE}ℹ $1${NC}" | tee -a "$LOG_FILE" } # --- User Interaction --- confirm() { local prompt="$1" default="${2:-n}" response [[ $VERBOSE == false ]] && return 0 prompt="$prompt [${default^^}/$( [[ $default == "y" ]] && echo "n" || echo "y" )]: " while true; do read -rp "$(echo -e "${CYAN}$prompt${NC}")" response response=${response,,} response=${response:-$default} case $response in y|yes) return 0 ;; n|no) return 1 ;; *) echo -e "${RED}Please answer yes or no.${NC}" ;; esac done } # --- Validation Functions --- validate_username() { [[ "$1" =~ ^[a-z_][a-z0-9_-]*$ && ${#1} -le 32 ]] } validate_hostname() { [[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$ && ! "$1" =~ \.\. ]] } validate_port() { [[ "$1" =~ ^[0-9]+$ && "$1" -ge 1024 && "$1" -le 65535 ]] } validate_backup_port() { [[ "$1" =~ ^[0-9]+$ && "$1" -ge 1 && "$1" -le 65535 ]] } validate_ssh_key() { [[ -n "$1" && "$1" =~ ^(ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)\ ]] } validate_timezone() { [[ -e "/usr/share/zoneinfo/$1" ]] } validate_swap_size() { [[ "${1^^}" =~ ^[0-9]+[MG]$ && "${1%[MG]}" -ge 1 ]] } validate_ufw_port() { [[ "$1" =~ ^[0-9]+(/tcp|/udp)?$ ]] } validate_cron_schedule() { local schedule="$1" temp_cron temp_cron=$(mktemp) echo "$schedule /bin/true" > "$temp_cron" if crontab -u root "$temp_cron" 2>/dev/null; then rm -f "$temp_cron" return 0 else rm -f "$temp_cron" return 1 fi } convert_to_bytes() { local size_upper="${1^^}" unit="${size_upper: -1}" value="${size_upper%[MG]}" if [[ "$unit" == "G" ]]; then echo $((value * 1024 * 1024 * 1024)) elif [[ "$unit" == "M" ]]; then echo $((value * 1024 * 1024)) else echo 0 fi } # --- Task Selection Function --- select_tasks_interactive() { print_section "Task Selection" echo -e "${CYAN}Select tasks to run (use numbers, comma-separated, e.g., 1,3,5):${NC}" echo -e "${YELLOW}Available tasks:${NC}" echo -e " ${BOLD}1)${NC} Update Check - Check for script updates" echo -e " ${BOLD}2)${NC} Dependency Check - Ensure required tools are installed" echo -e " ${BOLD}3)${NC} System Check - Verify OS compatibility and root privileges" echo -e " ${BOLD}4)${NC} Configuration Collection - Set up username, hostname, SSH port" echo -e " ${BOLD}5)${NC} Package Installation - Install essential packages" echo -e " ${BOLD}6)${NC} User Setup - Create admin user and configure SSH keys" echo -e " ${BOLD}7)${NC} System Configuration - Set timezone, hostname, and locales" echo -e " ${BOLD}8)${NC} SSH Hardening - Configure SSH with key-based auth and custom port" echo -e " ${BOLD}9)${NC} Firewall Setup - Configure UFW with custom rules" echo -e " ${BOLD}10)${NC} Fail2Ban Setup - Configure Fail2Ban for intrusion prevention" echo -e " ${BOLD}11)${NC} Auto Updates - Enable unattended-upgrades" echo -e " ${BOLD}12)${NC} Time Synchronization - Configure chrony for NTP" echo -e " ${BOLD}13)${NC} Kernel Hardening - Apply sysctl security settings" echo -e " ${BOLD}14)${NC} Docker Installation - Install Docker (optional)" echo -e " ${BOLD}15)${NC} Tailscale Installation - Set up Tailscale VPN (optional)" echo -e " ${BOLD}16)${NC} Backup Configuration - Set up rsync-based backups" echo -e " ${BOLD}17)${NC} Swap Configuration - Configure swap file" echo -e " ${BOLD}18)${NC} Security Audit - Run Lynis and debsecan (Debian only)" echo -e " ${BOLD}19)${NC} Final Cleanup - Update system and clean up" echo -e " ${BOLD}20)${NC} Generate Summary - Create final report" echo -e "${CYAN}Enter numbers (e.g., 1,3,5) or 'all' for all tasks:${NC}" read -rp " " task_choices if [[ "$task_choices" == "all" ]]; then SELECTED_TASKS=("update" "deps" "system" "config" "packages" "user" "system_config" "ssh" "firewall" "fail2ban" "auto_updates" "time_sync" "kernel" "docker" "tailscale" "backup" "swap" "audit" "cleanup" "summary") else IFS=',' read -r -a task_array <<< "$task_choices" SELECTED_TASKS=() for task_num in "${task_array[@]}"; do case $task_num in 1) SELECTED_TASKS+=("update") ;; 2) SELECTED_TASKS+=("deps") ;; 3) SELECTED_TASKS+=("system") ;; 4) SELECTED_TASKS+=("config") ;; 5) SELECTED_TASKS+=("packages") ;; 6) SELECTED_TASKS+=("user") ;; 7) SELECTED_TASKS+=("system_config") ;; 8) SELECTED_TASKS+=("ssh") ;; 9) SELECTED_TASKS+=("firewall") ;; 10) SELECTED_TASKS+=("fail2ban") ;; 11) SELECTED_TASKS+=("auto_updates") ;; 12) SELECTED_TASKS+=("time_sync") ;; 13) SELECTED_TASKS+=("kernel") ;; 14) SELECTED_TASKS+=("docker") ;; 15) SELECTED_TASKS+=("tailscale") ;; 16) SELECTED_TASKS+=("backup") ;; 17) SELECTED_TASKS+=("swap") ;; 18) SELECTED_TASKS+=("audit") ;; 19) SELECTED_TASKS+=("cleanup") ;; 20) SELECTED_TASKS+=("summary") ;; *) print_error "Invalid task number: $task_num. Skipping." ;; esac done fi if [[ ${#SELECTED_TASKS[@]} -eq 0 ]]; then print_error "No valid tasks selected. Exiting." exit 1 fi print_info "Selected tasks: ${SELECTED_TASKS[*]}" log "Selected tasks for execution: ${SELECTED_TASKS[*]}" } # --- Update Check --- run_update_check() { print_section "Checking for Script Updates" local latest_version if ! latest_version=$(curl -sL "$SCRIPT_URL" | grep '^CURRENT_VERSION=' | head -n 1 | awk -F'"' '{print $2}'); then print_warning "Could not check for updates. Check internet connection." log "Update check failed: Could not fetch script." return fi if [[ -z "$latest_version" ]]; then print_warning "Failed to parse version from remote script." log "Update check failed: Could not parse version." return fi if [[ "$(printf '%s\n' "$CURRENT_VERSION" "$latest_version" | sort -V | head -n 1)" == "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$latest_version" ]]; then print_success "A new version ($latest_version) is available!" if ! confirm "Update to version $latest_version now?"; then return fi local temp_dir=$(mktemp -d) trap 'rm -rf -- "$temp_dir"' EXIT local temp_script="$temp_dir/du_setup.sh" temp_checksum="$temp_dir/checksum.sha256" print_info "Downloading new script version..." if ! curl -sL "$SCRIPT_URL" -o "$temp_script"; then print_error "Failed to download new script. Update aborted." exit 1 fi print_info "Downloading checksum..." if ! curl -sL "$CHECKSUM_URL" -o "$temp_checksum"; then print_error "Failed to download checksum file. Update aborted." exit 1 fi print_info "Verifying checksum..." if ! (cd "$temp_dir" && sha256sum -c "checksum.sha256" --quiet); then print_error "Checksum verification failed. Update aborted." exit 1 fi print_info "Checking script syntax..." if ! bash -n "$temp_script"; then print_error "Downloaded script has syntax error. Update aborted." exit 1 fi if ! mv "$temp_script" "$0" || ! chmod +x "$0"; then print_error "Failed to replace script file." exit 1 fi trap - EXIT rm -rf -- "$temp_dir" print_success "Update successful. Please rerun the script." exit 0 else print_info "Running latest version ($CURRENT_VERSION)." fi } # --- Dependency Check --- check_dependencies() { print_section "Checking Dependencies" local missing_deps=() for dep in curl sudo gpg coreutils; do command -v "$dep" >/dev/null || missing_deps+=("$dep") done if [[ ${#missing_deps[@]} -gt 0 ]]; then print_info "Installing missing dependencies: ${missing_deps[*]}" if ! apt-get update -qq || ! apt-get install -y -qq "${missing_deps[@]}"; then print_error "Failed to install dependencies: ${missing_deps[*]}" exit 1 fi fi print_success "All dependencies installed." log "Dependency check completed." } # --- System Check --- check_system() { print_section "System Compatibility Check" if [[ $(id -u) -ne 0 ]]; then print_error "This script must be run as root." 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 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)$ ]]; then print_success "Compatible OS: $PRETTY_NAME" else print_warning "Untested OS: $PRETTY_NAME. Designed for Debian 12 or Ubuntu 20.04/22.04/24.04." if ! confirm "Continue anyway?"; then exit 1; fi fi else print_error "Not a Debian or Ubuntu system." exit 1 fi if ! dpkg -l openssh-server | grep -q ^ii; then print_warning "openssh-server not installed. Will be installed." 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." exit 1 fi if [[ ! -w /var/log ]]; then print_error "Cannot write to /var/log." exit 1 fi if [[ ! -w /etc/shadow ]]; then print_error "/etc/shadow not writable." exit 1 fi if [[ $(stat -c %a /etc/shadow) != "640" ]]; then chmod 640 /etc/shadow chown root:shadow /etc/shadow print_info "Fixed /etc/shadow permissions to 640." fi log "System check completed." } # --- Configuration Collection --- 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' exists." if confirm "Use existing user?"; then USER_EXISTS=true; break; fi else USER_EXISTS=false; break fi else print_error "Invalid username (lowercase, 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 pretty hostname (optional): ${NC}")" PRETTY_NAME 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."; 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}" echo -e " Username: $USERNAME" echo -e " Hostname: $SERVER_NAME" echo -e " SSH Port: $SSH_PORT" echo -e " Server IP: $SERVER_IP" if ! confirm "Continue with this configuration?" "y"; then exit 0; fi log "Configuration: USER=$USERNAME, HOST=$SERVER_NAME, PORT=$SSH_PORT" } # --- Package Installation --- install_packages() { print_section "Package Installation" print_info "Updating package lists and upgrading system..." if ! apt-get update -qq || ! DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq; then print_error "Failed to update/upgrade packages." exit 1 fi print_info "Installing essential packages..." if ! apt-get install -y -qq ufw fail2ban unattended-upgrades chrony rsync wget vim htop iotop nethogs netcat-traditional ncdu tree rsyslog cron jq gawk coreutils perl skopeo git openssh-client openssh-server; then print_error "Failed to install packages." exit 1 fi print_success "Packages installed." log "Package installation completed." } # --- User Setup --- setup_user() { print_section "User Management" if [[ -z "$USERNAME" ]]; then print_error "USERNAME not set." exit 1 fi if [[ $USER_EXISTS == false ]]; then print_info "Creating user '$USERNAME'..." if ! adduser --disabled-password --gecos "" "$USERNAME"; then print_error "Failed to create user '$USERNAME'." exit 1 fi while true; do read -sp "$(echo -e "${CYAN}New password for '$USERNAME' (Enter twice to skip): ${NC}")" PASS1 echo read -sp "$(echo -e "${CYAN}Retype password: ${NC}")" PASS2 echo if [[ -z "$PASS1" && -z "$PASS2" ]]; then print_warning "Password skipped. Using SSH key only." break elif [[ "$PASS1" == "$PASS2" ]]; then if echo "$USERNAME:$PASS1" | chpasswd; then print_success "Password set." break else print_error "Failed to set password." fi else print_error "Passwords do not match." fi done USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6) SSH_DIR="$USER_HOME/.ssh" AUTH_KEYS="$SSH_DIR/authorized_keys" if [[ ! -w "$USER_HOME" ]]; then chown "$USERNAME:$USERNAME" "$USER_HOME" chmod 700 "$USER_HOME" fi if confirm "Add SSH public key(s)?"; then while true; do read -rp "$(echo -e "${CYAN}Paste SSH public key: ${NC}")" SSH_PUBLIC_KEY if validate_ssh_key "$SSH_PUBLIC_KEY"; then mkdir -p "$SSH_DIR" chmod 700 "$SSH_DIR" chown "$USERNAME:$USERNAME" "$SSH_DIR" echo "$SSH_PUBLIC_KEY" >> "$AUTH_KEYS" awk '!seen[$0]++' "$AUTH_KEYS" > "$AUTH_KEYS.tmp" && mv "$AUTH_KEYS.tmp" "$AUTH_KEYS" chmod 600 "$AUTH_KEYS" chown "$USERNAME:$USERNAME" "$AUTH_KEYS" print_success "SSH key added." LOCAL_KEY_ADDED=true else print_error "Invalid SSH key format." fi if ! confirm "Add another key?" "n"; then break; fi done else print_info "Generating SSH key pair..." mkdir -p "$SSH_DIR" chmod 700 "$SSH_DIR" chown "$USERNAME:$USERNAME" "$SSH_DIR" sudo -u "$USERNAME" ssh-keygen -t ed25519 -f "$SSH_DIR/id_ed25519_user" -N "" -q cat "$SSH_DIR/id_ed25519_user.pub" >> "$AUTH_KEYS" chmod 600 "$AUTH_KEYS" chown "$USERNAME:$USERNAME" "$AUTH_KEYS" TEMP_KEY_FILE="/tmp/${USERNAME}_ssh_key_$(date +%s)" trap 'rm -f "$TEMP_KEY_FILE"' EXIT cp "$SSH_DIR/id_ed25519_user" "$TEMP_KEY_FILE" chmod 600 "$TEMP_KEY_FILE" echo -e "${YELLOW}Save PRIVATE key to ~/.ssh/${USERNAME}_key:${NC}" cat "$TEMP_KEY_FILE" echo -e "${CYAN}PUBLIC key:${NC}" cat "$SSH_DIR/id_ed25519_user.pub" echo -e "${CYAN}Set permissions: chmod 600 ~/.ssh/${USERNAME}_key${NC}" echo -e "${CYAN}Connect with: ssh -i ~/.ssh/${USERNAME}_key -p $SSH_PORT $USERNAME@$SERVER_IP${NC}" read -rp "$(echo -e "${CYAN}Press Enter after saving keys...${NC}")" print_success "SSH key generated." LOCAL_KEY_ADDED=true fi else print_info "Using existing user: $USERNAME" USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6) AUTH_KEYS="$USER_HOME/.ssh/authorized_keys" if [[ ! -s "$AUTH_KEYS" ]]; then print_warning "No valid SSH keys found in $AUTH_KEYS." fi fi if ! groups "$USERNAME" | grep -qw sudo; then usermod -aG sudo "$USERNAME" print_success "User added to sudo group." else print_info "User already in sudo group." fi log "User setup completed." } # --- System Configuration --- 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 while true; do read -rp "$(echo -e "${CYAN}Enter timezone (e.g., Europe/London) [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." else print_info "Timezone already set." fi break else print_error "Invalid timezone." fi done if confirm "Configure locales interactively?"; then dpkg-reconfigure locales fi if [[ $(hostnamectl --static) != "$SERVER_NAME" ]]; then hostnamectl set-hostname "$SERVER_NAME" hostnamectl set-hostname "$PRETTY_NAME" --pretty if grep -q "^127.0.1.1" /etc/hosts; then sed -i "s/^127.0.1.1.*/127.0.1.1\t$SERVER_NAME/" /etc/hosts else echo "127.0.1.1 $SERVER_NAME" >> /etc/hosts fi print_success "Hostname set to $SERVER_NAME." else print_info "Hostname already set." fi log "System configuration completed." } # --- SSH Hardening --- configure_ssh() { trap cleanup_and_exit ERR print_section "SSH Hardening" if ! dpkg -l openssh-server | grep -q ^ii; then print_error "openssh-server not installed." return 1 fi if [[ $ID == "ubuntu" ]] && systemctl is-active ssh.socket >/dev/null 2>&1; then SSH_SERVICE="ssh.socket" elif systemctl is-enabled sshd.service >/dev/null 2>&1 || systemctl is-active sshd.service >/dev/null 2>&1; then SSH_SERVICE="sshd.service" else SSH_SERVICE="ssh.service" fi PREVIOUS_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) AUTH_KEYS="$USER_HOME/.ssh/authorized_keys" if [[ $LOCAL_KEY_ADDED == false && ! -s "$AUTH_KEYS" ]]; then print_info "Generating SSH key..." mkdir -p "$USER_HOME/.ssh" chmod 700 "$USER_HOME/.ssh" chown "$USERNAME:$USERNAME" "$USER_HOME/.ssh" sudo -u "$USERNAME" ssh-keygen -t ed25519 -f "$USER_HOME/.ssh/id_ed25519" -N "" -q cat "$USER_HOME/.ssh/id_ed25519.pub" >> "$AUTH_KEYS" chmod 600 "$AUTH_KEYS" chown "$USERNAME:$USERNAME" "$AUTH_KEYS" print_success "SSH key generated." echo -e "${YELLOW}Public key:${NC}" cat "$USER_HOME/.ssh/id_ed25519.pub" LOCAL_KEY_ADDED=true fi print_warning "Test SSH: ssh -p $PREVIOUS_SSH_PORT $USERNAME@$SERVER_IP" if ! confirm "SSH connection successful?"; then print_error "SSH key authentication required." return 1 fi SSHD_BACKUP_FILE="$BACKUP_DIR/sshd_config.backup_$(date +%Y%m%d_%H%M%S)" cp /etc/ssh/sshd_config "$SSHD_BACKUP_FILE" if [[ $ID == "ubuntu" ]] && dpkg --compare-versions "$(lsb_release -rs)" ge "24.04"; then sed -i "s/^Port .*/Port $SSH_PORT/" /etc/ssh/sshd_config || echo "Port $SSH_PORT" >> /etc/ssh/sshd_config elif [[ "$SSH_SERVICE" == "ssh.socket" ]]; then mkdir -p /etc/systemd/system/ssh.socket.d echo -e "[Socket]\nListenStream=\nListenStream=$SSH_PORT" > /etc/systemd/system/ssh.socket.d/override.conf else mkdir -p /etc/systemd/system/${SSH_SERVICE}.d echo -e "[Service]\nExecStart=\nExecStart=/usr/sbin/sshd -D -p $SSH_PORT" > /etc/systemd/system/${SSH_SERVICE}.d/override.conf fi mkdir -p /etc/ssh/sshd_config.d tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null < /dev/null <<'EOF' ****************************************************************************** 🔒AUTHORIZED ACCESS ONLY ════ all attempts are logged and reviewed ════ ****************************************************************************** EOF systemctl daemon-reload systemctl restart "$SSH_SERVICE" sleep 5 if ! ss -tuln | grep -q ":$SSH_PORT"; then print_error "SSH not listening on port $SSH_PORT." rollback_ssh_changes return 1 fi if ssh -p "$SSH_PORT" -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@localhost true 2>/dev/null; then print_error "Root SSH login still possible." rollback_ssh_changes return 1 fi print_warning "Test new SSH: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" for ((i=1; i<=3; i++)); do if confirm "New SSH connection successful?"; then print_success "SSH hardening completed." trap - ERR log "SSH hardening completed." return 0 fi print_info "Retry $i/3..." sleep 5 done print_error "SSH connection failed. Rolling back..." rollback_ssh_changes return 1 } # --- SSH Rollback --- rollback_ssh_changes() { print_info "Rolling back SSH to port $PREVIOUS_SSH_PORT..." rm -rf /etc/systemd/system/${SSH_SERVICE}.d /etc/systemd/system/ssh.socket.d /etc/ssh/sshd_config.d/99-hardening.conf 2>/dev/null if [[ -f "$SSHD_BACKUP_FILE" ]]; then cp "$SSHD_BACKUP_FILE" /etc/ssh/sshd_config print_info "Restored sshd_config." else print_error "Backup file $SSHD_BACKUP_FILE not found." return 1 fi if ! /usr/sbin/sshd -t >/tmp/sshd_config_test.log 2>&1; then print_error "Restored sshd_config invalid. Check /tmp/sshd_config_test.log." return 1 fi systemctl daemon-reload if [[ "$SSH_SERVICE" == "ssh.socket" ]]; then systemctl stop ssh.socket 2>/dev/null systemctl restart ssh.service systemctl restart ssh.socket else systemctl restart "$SSH_SERVICE" fi for ((i=1; i<=10; i++)); do if ss -tuln | grep -q ":$PREVIOUS_SSH_PORT"; then print_success "Rollback successful." return 0 fi sleep 3 done print_error "Rollback failed. Check systemctl status $SSH_SERVICE." return 1 } # --- Firewall Configuration --- configure_firewall() { print_section "Firewall Configuration (UFW)" if ufw status | grep -q "Status: active"; then print_info "UFW already enabled." else ufw default deny incoming ufw default allow outgoing fi if ! ufw status | grep -qw "$SSH_PORT/tcp"; then ufw allow "$SSH_PORT"/tcp comment 'Custom SSH' print_success "SSH rule added for port $SSH_PORT." fi if confirm "Allow HTTP (port 80)?"; then ufw allow http comment 'HTTP' print_success "HTTP allowed." fi if confirm "Allow HTTPS (port 443)?"; then ufw allow https comment 'HTTPS' print_success "HTTPS allowed." fi if confirm "Allow Tailscale (UDP 41641)?"; then ufw allow 41641/udp comment 'Tailscale VPN' print_success "Tailscale allowed." fi if confirm "Add custom ports?"; then while true; do read -rp "$(echo -e "${CYAN}Enter ports (e.g., 8080/tcp 123/udp): ${NC}")" CUSTOM_PORTS if [[ -z "$CUSTOM_PORTS" ]]; then break; fi local valid=true for port in $CUSTOM_PORTS; do if ! validate_ufw_port "$port"; then print_error "Invalid port: $port." valid=false fi done if [[ "$valid" == true ]]; then for port in $CUSTOM_PORTS; do ufw allow "$port" comment "Custom port $port" print_success "Added rule for $port." done break fi done fi if ! ufw status | grep -q "Status: active"; then ufw --force enable print_success "UFW enabled." fi print_success "Firewall configured." ufw status | tee -a "$LOG_FILE" log "Firewall configuration completed." } # --- Fail2Ban Configuration --- configure_fail2ban() { print_section "Fail2Ban Configuration" if systemctl is-active --quiet fail2ban; then print_info "Fail2Ban already active." else systemctl enable --now fail2ban fi if ! [[ -f /etc/fail2ban/jail.d/sshd.conf ]]; then tee /etc/fail2ban/jail.d/sshd.conf > /dev/null < /dev/null < /dev/null < /dev/null < /dev/null </dev/null print_success "Kernel hardening applied." log "Kernel hardening completed." } # --- Docker Installation --- install_docker() { print_section "Docker Installation" if command -v docker >/dev/null 2>&1; then print_info "Docker already installed." return 0 fi if ! confirm "Install Docker?"; then print_info "Skipping Docker installation." return 0 fi print_info "Installing Docker..." if ! apt-get update -qq || ! apt-get install -y -qq apt-transport-https ca-certificates curl gnupg lsb-release; then print_error "Failed to install Docker prerequisites." exit 1 fi curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list if ! apt-get update -qq || ! apt-get install -y -qq docker-ce docker-ce-cli containerd.io; then print_error "Failed to install Docker." exit 1 fi systemctl enable --now docker if systemctl is-active --quiet docker; then print_success "Docker installed and running." else print_error "Docker service failed." exit 1 fi log "Docker installation completed." } # --- Tailscale Installation --- install_tailscale() { print_section "Tailscale Installation" if command -v tailscale >/dev/null 2>&1; then print_info "Tailscale already installed." return 0 fi if ! confirm "Install Tailscale VPN?"; then print_info "Skipping Tailscale installation." return 0 fi curl -fsSL https://pkgs.tailscale.com/stable/${ID}/${VERSION_CODENAME}/tailscale.list > /etc/apt/sources.list.d/tailscale.list curl -fsSL https://pkgs.tailscale.com/stable/${ID}/${VERSION_CODENAME}/tailscale.asc | apt-key add - if ! apt-get update -qq || ! apt-get install -y -qq tailscale; then print_error "Failed to install Tailscale." exit 1 fi systemctl enable --now tailscaled print_info "Configuring Tailscale..." read -rp "$(echo -e "${CYAN}Enter Tailscale auth key: ${NC}")" AUTH_KEY if [[ -z "$AUTH_KEY" ]]; then print_error "Tailscale auth key required." return 1 fi TS_COMMAND="tailscale up --auth-key=$AUTH_KEY --operator=$USERNAME" if ! $TS_COMMAND; then print_warning "Tailscale connection failed. Run manually: $TS_COMMAND" return 0 fi for ((i=1; i<=3; i++)); do if TS_IPS=$(tailscale ip 2>/dev/null); then TS_IPV4=$(echo "$TS_IPS" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1) if [[ -n "$TS_IPV4" ]]; then print_success "Tailscale connected. IPv4: $TS_IPV4" log "Tailscale connected: $TS_IPV4" return 0 fi fi print_info "Waiting for Tailscale ($i/3)..." sleep 5 done print_warning "Tailscale connection not verified." log "Tailscale connection not verified." } # --- Backup Configuration --- setup_backup() { print_section "Backup Configuration" if [[ -f /etc/cron.d/du-backup ]]; then print_info "Backup cron job already configured." return 0 fi if ! confirm "Configure automated backups?"; then print_info "Skipping backup configuration." return 0 fi read -rp "$(echo -e "${CYAN}Enter backup server hostname: ${NC}")" BACKUP_HOST read -rp "$(echo -e "${CYAN}Enter backup server SSH port [22]: ${NC}")" BACKUP_PORT BACKUP_PORT=${BACKUP_PORT:-22} if ! validate_backup_port "$BACKUP_PORT"; then print_error "Invalid backup port." return 1 fi read -rp "$(echo -e "${CYAN}Enter backup server username: ${NC}")" BACKUP_USER read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /backups/server1): ${NC}")" REMOTE_BACKUP_PATH read -rp "$(echo -e "${CYAN}Enter local directories to back up (space-separated): ${NC}")" BACKUP_DIRS read -rp "$(echo -e "${CYAN}Enter cron schedule (e.g., '0 2 * * *' for daily at 2 AM): ${NC}")" CRON_SCHEDULE if ! validate_cron_schedule "$CRON_SCHEDULE"; then print_error "Invalid cron schedule." return 1 fi USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6) SSH_DIR="$USER_HOME/.ssh" SSH_KEY="$SSH_DIR/id_ed25519_backup" if [[ ! -f "$SSH_KEY" ]]; then sudo -u "$USERNAME" ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -q print_success "Backup SSH key generated." echo -e "${YELLOW}Add this public key to the backup server's authorized_keys:${NC}" cat "$SSH_KEY.pub" read -rp "$(echo -e "${CYAN}Press Enter after adding the key...${NC}")" fi SSH_COMMAND="ssh -i $SSH_KEY -p $BACKUP_PORT -o StrictHostKeyChecking=no" print_info "Testing backup connection..." TEST_DIR=$(mktemp -d) timeout 10 rsync -avz --delete -e "$SSH_COMMAND" "$TEST_DIR/" "${BACKUP_USER}@${BACKUP_HOST}:${REMOTE_BACKUP_PATH}/test_backup/" >/tmp/backup_test.log 2>&1 if [[ $? -eq 0 ]]; then print_success "Backup test successful." rm -rf "$TEST_DIR" else print_error "Backup test failed. Check /tmp/backup_test.log." return 1 fi tee /usr/local/bin/backup.sh > /dev/null <> "$BACKUP_LOG" 2>&1 EOF chmod +x /usr/local/bin/backup.sh tee /etc/cron.d/du-backup > /dev/null <> /etc/fstab fi print_success "Swap configured." log "Swap configuration completed." } # --- Security Audit --- run_security_audit() { print_section "Security Audit" if ! command -v lynis >/dev/null 2>&1; then print_info "Installing Lynis..." if [[ $ID == "debian" ]]; then apt-get install -y -qq apt-transport-https echo "deb https://packages.cisofy.com/community/lynis/deb/ stable main" > /etc/apt/sources.list.d/lynis.list curl -s https://packages.cisofy.com/keys/cisofy-software-rpms-public.key | apt-key add - else apt-get install -y -qq lynis fi apt-get update -qq && apt-get install -y -qq lynis fi print_info "Running Lynis audit..." lynis audit system --quiet > /tmp/lynis_report.txt print_success "Lynis audit completed. Report: /tmp/lynis_report.txt" if [[ $ID == "debian" ]] && ! command -v debsecan >/dev/null 2>&1; then apt-get install -y -qq debsecan debsecan --suite "${VERSION_CODENAME}" > /tmp/debsecan_report.txt print_success "debsecan report generated: /tmp/debsecan_report.txt" fi log "Security audit completed." } # --- Cleanup and Exit --- cleanup_and_exit() { local exit_code=$? if [[ $exit_code -ne 0 && $(type -t rollback_ssh_changes) == "function" ]]; then print_error "Error occurred. Rolling back SSH..." rollback_ssh_changes fi exit $exit_code } # --- Final Cleanup --- final_cleanup() { print_section "Final Cleanup" apt-get update -qq && apt-get upgrade -y -qq apt-get autoremove -y -qq apt-get autoclean -qq print_success "System updated and cleaned." log "Final cleanup completed." } # --- Generate Summary --- generate_summary() { print_section "Generating Summary" { echo "Setup Report - $(date '+%Y-%m-%d %H:%M:%S')" echo "===================================" echo "Username: $USERNAME" echo "Hostname: $SERVER_NAME" echo "SSH Port: $SSH_PORT" echo "Server IP: $SERVER_IP" echo "Timezone: $TIMEZONE" echo "Tasks Executed: ${SELECTED_TASKS[*]}" if [[ -n "${FAILED_SERVICES[*]}" ]]; then echo "Failed Services: ${FAILED_SERVICES[*]}" else echo "Failed Services: None" fi if [[ -f /tmp/lynis_report.txt ]]; then echo "Lynis Suggestions:" grep "suggestion" /tmp/lynis_report.txt || echo " None" fi if [[ -f /tmp/debsecan_report.txt ]]; then echo "debsecan Vulnerabilities:" head -n 10 /tmp/debsecan_report.txt || echo " None" fi echo "Log File: $LOG_FILE" echo "Backup Directory: $BACKUP_DIR" echo "===================================" echo "Connect to server with: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" echo "Reboot recommended to apply all changes." } > "$REPORT_FILE" print_success "Summary generated: $REPORT_FILE" cat "$REPORT_FILE" log "Summary generated: $REPORT_FILE" } # --- Main Function --- main() { print_header mkdir -p /var/log touch "$LOG_FILE" chmod 640 "$LOG_FILE" if [[ ${#SELECTED_TASKS[@]} -eq 0 ]]; then select_tasks_interactive fi for task in "${SELECTED_TASKS[@]}"; do case $task in update) run_update_check ;; deps) check_dependencies ;; system) check_system ;; config) collect_config ;; packages) install_packages ;; user) setup_user ;; system_config) configure_system ;; ssh) configure_ssh ;; firewall) configure_firewall ;; fail2ban) configure_fail2ban ;; auto_updates) configure_auto_updates ;; time_sync) configure_time_sync ;; kernel) configure_kernel_hardening ;; docker) install_docker ;; tailscale) install_tailscale ;; backup) setup_backup ;; swap) configure_swap ;; audit) run_security_audit ;; cleanup) final_cleanup ;; summary) generate_summary ;; *) print_error "Unknown task: $task" ;; esac done print_success "Setup completed. Review $REPORT_FILE for details." if confirm "Reboot now to apply changes?"; then reboot fi } # --- Execute Main --- main