Files
du_setup/du_setup.sh

1186 lines
44 KiB
Bash
Raw Normal View History

2025-06-26 16:22:23 +01:00
#!/bin/bash
2025-06-26 21:49:51 +01:00
# Debian 12 and Ubuntu Server Hardening Interactive Script
# Version: 0.62 | 2025-08-04
2025-06-28 11:43:10 +01:00
# Changelog:
# - v0.62: Added modular execution with interactive menu and --tasks flag
2025-08-03 23:05:44 +01:00
# - v0.61: Display Lynis suggestions in summary, hide tailscale auth key, cleanup temp files
2025-07-15 19:20:26 +01:00
# - 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
2025-06-26 16:22:23 +01:00
#
2025-06-26 21:49:51 +01:00
# 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.
2025-06-26 21:49:51 +01:00
#
# Usage:
# sudo -E ./du_setup.sh [--quiet] [--tasks=<task1,task2,...]
2025-06-26 21:49:51 +01:00
#
# Options:
# --quiet: Suppress non-critical output
# --tasks: Comma-separated tasks (e.g., ssh,firewall,swap)
2025-06-26 21:49:51 +01:00
#
# Notes:
# - Run as root on Debian 12 or Ubuntu 20.04/22.04/24.04.
# - Logs to /var/log/du_setup_*.log, backups to /root/setup_harden_backup_*.
# - Test in a VM before production use.
2025-06-26 16:22:23 +01:00
set -euo pipefail
2025-06-26 16:22:23 +01:00
2025-07-15 15:13:58 +01:00
# --- Update Configuration ---
2025-08-03 23:05:44 +01:00
CURRENT_VERSION="0.61"
2025-07-15 15:13:58 +01:00
SCRIPT_URL="https://raw.githubusercontent.com/buildplan/du_setup/refs/heads/main/du_setup.sh"
CHECKSUM_URL="${SCRIPT_URL}.sha256"
# --- Global Variables ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="/var/log/du_setup_$(date +%Y%m%d_%H%M%S).log"
BACKUP_LOG="/var/log/backup_rsync.log"
REPORT_FILE="/var/log/du_setup_report_$(date +%Y%m%d_%H%M%S).txt"
VERBOSE=true
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=""
FAILED_SERVICES=()
SELECTED_TASKS=()
# --- Parse Arguments ---
while [[ $# -gt 0 ]]; do
case $1 in
--quiet) VERBOSE=false; shift ;;
--tasks=*)
IFS=',' read -r -a SELECTED_TASKS <<< "${1#*=}"
shift
;;
*) shift ;;
esac
done
2025-06-26 16:22:23 +01:00
# --- Colors for Output ---
2025-07-02 20:22:10 +01:00
if command -v tput >/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 ---
2025-06-26 16:22:23 +01:00
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
print_header() {
[[ $VERBOSE == false ]] && return
echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}"
2025-06-26 22:19:35 +01:00
echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}"
echo -e "${CYAN}║ v0.21 | 2025-08-43 ║${NC}"
2025-06-26 16:22:23 +01:00
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"
2025-06-26 16:22:23 +01:00
}
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 ---
2025-06-26 16:22:23 +01:00
confirm() {
local prompt="$1" default="${2:-n}" response
2025-06-26 16:22:23 +01:00
[[ $VERBOSE == false ]] && return 0
prompt="$prompt [${default^^}/$( [[ $default == "y" ]] && echo "n" || echo "y" )]: "
2025-06-26 16:22:23 +01:00
while true; do
read -rp "$(echo -e "${CYAN}$prompt${NC}")" response
response=${response,,}
response=${response:-$default}
2025-06-26 16:22:23 +01:00
case $response in
y|yes) return 0 ;;
n|no) return 1 ;;
*) echo -e "${RED}Please answer yes or no.${NC}" ;;
esac
done
}
# --- Validation Functions ---
2025-06-26 16:22:23 +01:00
validate_username() {
[[ "$1" =~ ^[a-z_][a-z0-9_-]*$ && ${#1} -le 32 ]]
2025-06-26 16:22:23 +01:00
}
validate_hostname() {
[[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$ && ! "$1" =~ \.\. ]]
2025-06-26 16:22:23 +01:00
}
validate_port() {
[[ "$1" =~ ^[0-9]+$ && "$1" -ge 1024 && "$1" -le 65535 ]]
2025-06-26 16:22:23 +01:00
}
2025-06-28 12:24:23 +01:00
validate_backup_port() {
[[ "$1" =~ ^[0-9]+$ && "$1" -ge 1 && "$1" -le 65535 ]]
2025-06-28 12:24:23 +01:00
}
2025-06-26 16:22:23 +01:00
validate_ssh_key() {
[[ -n "$1" && "$1" =~ ^(ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)\ ]]
2025-06-26 16:22:23 +01:00
}
validate_timezone() {
[[ -e "/usr/share/zoneinfo/$1" ]]
2025-06-26 16:22:23 +01:00
}
validate_swap_size() {
[[ "${1^^}" =~ ^[0-9]+[MG]$ && "${1%[MG]}" -ge 1 ]]
2025-06-26 16:22:23 +01:00
}
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
2025-06-26 16:22:23 +01:00
}
convert_to_bytes() {
local size_upper="${1^^}" unit="${size_upper: -1}" value="${size_upper%[MG]}"
2025-06-26 16:22:23 +01:00
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 ---
2025-07-15 15:13:58 +01:00
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."
2025-07-15 15:13:58 +01:00
return
fi
if [[ -z "$latest_version" ]]; then
print_warning "Failed to parse version from remote script."
log "Update check failed: Could not parse version."
2025-07-15 15:13:58 +01:00
return
fi
if [[ "$(printf '%s\n' "$CURRENT_VERSION" "$latest_version" | sort -V | head -n 1)" == "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$latest_version" ]]; then
2025-07-15 15:13:58 +01:00
print_success "A new version ($latest_version) is available!"
if ! confirm "Update to version $latest_version now?"; then
2025-07-15 15:13:58 +01:00
return
fi
local temp_dir=$(mktemp -d)
2025-07-15 15:13:58 +01:00
trap 'rm -rf -- "$temp_dir"' EXIT
local temp_script="$temp_dir/du_setup.sh" temp_checksum="$temp_dir/checksum.sha256"
2025-07-15 15:13:58 +01:00
print_info "Downloading new script version..."
if ! curl -sL "$SCRIPT_URL" -o "$temp_script"; then
print_error "Failed to download new script. Update aborted."
2025-07-15 15:13:58 +01:00
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."
2025-07-15 15:13:58 +01:00
exit 1
fi
print_info "Verifying checksum..."
if ! (cd "$temp_dir" && sha256sum -c "checksum.sha256" --quiet); then
print_error "Checksum verification failed. Update aborted."
2025-07-15 15:13:58 +01:00
exit 1
fi
print_info "Checking script syntax..."
if ! bash -n "$temp_script"; then
print_error "Downloaded script has syntax error. Update aborted."
2025-07-15 15:13:58 +01:00
exit 1
fi
if ! mv "$temp_script" "$0" || ! chmod +x "$0"; then
print_error "Failed to replace script file."
2025-07-15 15:13:58 +01:00
exit 1
fi
trap - EXIT
rm -rf -- "$temp_dir"
print_success "Update successful. Please rerun the script."
2025-07-15 15:13:58 +01:00
exit 0
else
print_info "Running latest version ($CURRENT_VERSION)."
2025-07-15 15:13:58 +01:00
fi
}
# --- Dependency Check ---
2025-06-26 21:49:51 +01:00
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
2025-06-26 21:49:51 +01:00
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
2025-06-26 17:12:59 +01:00
fi
2025-06-26 16:22:23 +01:00
fi
print_success "All dependencies installed."
2025-06-26 21:49:51 +01:00
log "Dependency check completed."
2025-06-26 16:22:23 +01:00
}
# --- System Check ---
2025-06-26 16:22:23 +01:00
check_system() {
print_section "System Compatibility Check"
if [[ $(id -u) -ne 0 ]]; then
print_error "This script must be run as root."
2025-06-26 16:22:23 +01:00
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."
2025-06-26 16:22:23 +01:00
fi
if [[ -f /etc/os-release ]]; then
source /etc/os-release
ID=$ID
2025-06-26 16:22:23 +01:00
if [[ $ID == "debian" && $VERSION_ID == "12" ]] || \
2025-06-26 21:49:51 +01:00
[[ $ID == "ubuntu" && $VERSION_ID =~ ^(20.04|22.04|24.04)$ ]]; then
print_success "Compatible OS: $PRETTY_NAME"
2025-06-26 16:22:23 +01:00
else
print_warning "Untested OS: $PRETTY_NAME. Designed for Debian 12 or Ubuntu 20.04/22.04/24.04."
2025-06-26 16:22:23 +01:00
if ! confirm "Continue anyway?"; then exit 1; fi
fi
else
print_error "Not a Debian or Ubuntu system."
2025-06-26 16:22:23 +01:00
exit 1
fi
if ! dpkg -l openssh-server | grep -q ^ii; then
print_warning "openssh-server not installed. Will be installed."
2025-06-26 16:22:23 +01:00
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."
2025-06-26 16:22:23 +01:00
exit 1
fi
if [[ ! -w /var/log ]]; then
print_error "Cannot write to /var/log."
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
if [[ ! -w /etc/shadow ]]; then
print_error "/etc/shadow not writable."
2025-06-26 21:49:51 +01:00
exit 1
fi
if [[ $(stat -c %a /etc/shadow) != "640" ]]; then
2025-06-26 16:22:23 +01:00
chmod 640 /etc/shadow
chown root:shadow /etc/shadow
print_info "Fixed /etc/shadow permissions to 640."
2025-06-26 16:22:23 +01:00
fi
log "System check completed."
2025-06-26 16:22:23 +01:00
}
# --- Configuration Collection ---
2025-06-26 16:22:23 +01:00
collect_config() {
print_section "Configuration Setup"
2025-06-26 21:49:51 +01:00
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
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
USER_EXISTS=false; break
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
else
print_error "Invalid username (lowercase, numbers, hyphens, underscores, max 32 chars)."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
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}
2025-06-26 21:49:51 +01:00
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
2025-06-26 21:49:51 +01:00
done
2025-06-26 16:22:23 +01:00
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}"
2025-06-26 22:19:35 +01:00
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"
2025-06-26 16:22:23 +01:00
}
# --- Package Installation ---
2025-06-26 16:22:23 +01:00
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."
2025-06-26 16:22:23 +01:00
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."
2025-06-26 16:22:23 +01:00
exit 1
fi
print_success "Packages installed."
2025-06-26 16:22:23 +01:00
log "Package installation completed."
}
# --- User Setup ---
2025-06-26 16:22:23 +01:00
setup_user() {
print_section "User Management"
2025-07-02 17:25:17 +01:00
if [[ -z "$USERNAME" ]]; then
print_error "USERNAME not set."
2025-07-02 17:25:17 +01:00
exit 1
fi
2025-06-26 16:22:23 +01:00
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
2025-06-26 16:22:23 +01:00
echo
read -sp "$(echo -e "${CYAN}Retype password: ${NC}")" PASS2
2025-06-26 16:22:23 +01:00
echo
if [[ -z "$PASS1" && -z "$PASS2" ]]; then
print_warning "Password skipped. Using SSH key only."
2025-06-26 16:22:23 +01:00
break
elif [[ "$PASS1" == "$PASS2" ]]; then
if echo "$USERNAME:$PASS1" | chpasswd; then
print_success "Password set."
2025-06-26 16:22:23 +01:00
break
else
print_error "Failed to set password."
2025-06-26 16:22:23 +01:00
fi
else
print_error "Passwords do not match."
2025-06-26 16:22:23 +01:00
fi
done
USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6)
SSH_DIR="$USER_HOME/.ssh"
AUTH_KEYS="$SSH_DIR/authorized_keys"
2025-07-02 16:35:44 +01:00
if [[ ! -w "$USER_HOME" ]]; then
chown "$USERNAME:$USERNAME" "$USER_HOME"
chmod 700 "$USER_HOME"
fi
if confirm "Add SSH public key(s)?"; then
2025-07-02 16:35:44 +01:00
while true; do
read -rp "$(echo -e "${CYAN}Paste SSH public key: ${NC}")" SSH_PUBLIC_KEY
2025-07-02 16:35:44 +01:00
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."
2025-07-02 16:35:44 +01:00
LOCAL_KEY_ADDED=true
else
print_error "Invalid SSH key format."
2025-07-02 16:35:44 +01:00
fi
if ! confirm "Add another key?" "n"; then break; fi
2025-07-02 16:35:44 +01:00
done
else
print_info "Generating SSH key pair..."
2025-07-02 16:35:44 +01:00
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
2025-07-02 22:09:11 +01:00
cat "$SSH_DIR/id_ed25519_user.pub" >> "$AUTH_KEYS"
2025-07-02 16:35:44 +01:00
chmod 600 "$AUTH_KEYS"
chown "$USERNAME:$USERNAME" "$AUTH_KEYS"
2025-07-02 17:25:17 +01:00
TEMP_KEY_FILE="/tmp/${USERNAME}_ssh_key_$(date +%s)"
trap 'rm -f "$TEMP_KEY_FILE"' EXIT
2025-07-02 22:09:11 +01:00
cp "$SSH_DIR/id_ed25519_user" "$TEMP_KEY_FILE"
2025-07-02 16:35:44 +01:00
chmod 600 "$TEMP_KEY_FILE"
echo -e "${YELLOW}Save PRIVATE key to ~/.ssh/${USERNAME}_key:${NC}"
2025-07-02 16:35:44 +01:00
cat "$TEMP_KEY_FILE"
echo -e "${CYAN}PUBLIC key:${NC}"
2025-07-02 22:09:11 +01:00
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."
2025-07-02 16:35:44 +01:00
LOCAL_KEY_ADDED=true
fi
2025-06-26 16:22:23 +01:00
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."
2025-07-02 16:35:44 +01:00
fi
2025-06-26 16:22:23 +01:00
fi
if ! groups "$USERNAME" | grep -qw sudo; then
usermod -aG sudo "$USERNAME"
2025-06-26 16:22:23 +01:00
print_success "User added to sudo group."
else
print_info "User already in sudo group."
2025-06-26 16:22:23 +01:00
fi
log "User setup completed."
2025-06-26 16:22:23 +01:00
}
# --- System Configuration ---
2025-06-26 16:22:23 +01:00
configure_system() {
print_section "System Configuration"
mkdir -p "$BACKUP_DIR" && chmod 700 "$BACKUP_DIR"
2025-06-26 21:49:51 +01:00
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
2025-06-26 21:49:51 +01:00
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."
2025-06-26 21:49:51 +01:00
fi
break
else
print_error "Invalid timezone."
2025-06-26 21:49:51 +01:00
fi
done
if confirm "Configure locales interactively?"; then
2025-06-26 16:22:23 +01:00
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."
2025-06-26 16:22:23 +01:00
else
print_info "Hostname already set."
2025-06-26 16:22:23 +01:00
fi
log "System configuration completed."
}
# --- SSH Hardening ---
2025-06-26 16:22:23 +01:00
configure_ssh() {
2025-06-30 21:10:35 +01:00
trap cleanup_and_exit ERR
2025-06-26 16:22:23 +01:00
print_section "SSH Hardening"
if ! dpkg -l openssh-server | grep -q ^ii; then
print_error "openssh-server not installed."
2025-06-30 21:10:35 +01:00
return 1
2025-06-26 16:22:23 +01:00
fi
2025-06-27 00:54:10 +01:00
if [[ $ID == "ubuntu" ]] && systemctl is-active ssh.socket >/dev/null 2>&1; then
SSH_SERVICE="ssh.socket"
2025-06-26 21:49:51 +01:00
elif systemctl is-enabled sshd.service >/dev/null 2>&1 || systemctl is-active sshd.service >/dev/null 2>&1; then
SSH_SERVICE="sshd.service"
2025-06-26 16:22:23 +01:00
else
SSH_SERVICE="ssh.service"
2025-06-26 16:22:23 +01:00
fi
2025-06-30 21:10:35 +01:00
PREVIOUS_SSH_PORT=$(ss -tuln | grep -E ":(22|.*$SSH_SERVICE.*)" | awk '{print $5}' | cut -d':' -f2 | head -n1 || echo "22")
2025-06-26 16:22:23 +01:00
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"
2025-06-26 16:22:23 +01:00
print_success "SSH key generated."
echo -e "${YELLOW}Public key:${NC}"
cat "$USER_HOME/.ssh/id_ed25519.pub"
LOCAL_KEY_ADDED=true
2025-06-26 16:22:23 +01:00
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."
2025-06-30 21:10:35 +01:00
return 1
2025-06-26 16:22:23 +01:00
fi
SSHD_BACKUP_FILE="$BACKUP_DIR/sshd_config.backup_$(date +%Y%m%d_%H%M%S)"
2025-06-26 21:49:51 +01:00
cp /etc/ssh/sshd_config "$SSHD_BACKUP_FILE"
2025-06-30 17:23:39 +01:00
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
2025-06-30 21:10:35 +01:00
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
2025-06-30 17:23:39 +01:00
else
2025-06-30 21:10:35 +01:00
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
2025-06-27 19:23:28 +01:00
fi
2025-06-30 21:10:35 +01:00
mkdir -p /etc/ssh/sshd_config.d
tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<EOF
2025-06-26 16:22:23 +01:00
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
X11Forwarding no
PrintMotd no
Banner /etc/issue.net
EOF
2025-06-30 21:10:35 +01:00
tee /etc/issue.net > /dev/null <<'EOF'
2025-06-26 16:22:23 +01:00
******************************************************************************
2025-06-27 15:50:40 +01:00
🔒AUTHORIZED ACCESS ONLY
2025-06-30 19:22:31 +01:00
════ all attempts are logged and reviewed ════
2025-06-26 16:22:23 +01:00
******************************************************************************
EOF
2025-06-27 00:54:10 +01:00
systemctl daemon-reload
2025-06-30 21:10:35 +01:00
systemctl restart "$SSH_SERVICE"
2025-06-27 00:54:10 +01:00
sleep 5
if ! ss -tuln | grep -q ":$SSH_PORT"; then
print_error "SSH not listening on port $SSH_PORT."
rollback_ssh_changes
2025-06-30 21:10:35 +01:00
return 1
2025-06-26 16:22:23 +01:00
fi
2025-06-30 21:10:35 +01:00
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
2025-06-30 21:10:35 +01:00
return 1
2025-06-26 16:22:23 +01:00
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
2025-06-27 00:36:26 +01:00
fi
print_info "Retry $i/3..."
sleep 5
2025-06-27 00:36:26 +01:00
done
print_error "SSH connection failed. Rolling back..."
rollback_ssh_changes
return 1
2025-06-26 16:22:23 +01:00
}
# --- SSH Rollback ---
2025-06-30 17:23:39 +01:00
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
2025-06-30 19:36:22 +01:00
if [[ -f "$SSHD_BACKUP_FILE" ]]; then
cp "$SSHD_BACKUP_FILE" /etc/ssh/sshd_config
print_info "Restored sshd_config."
2025-06-30 19:36:22 +01:00
else
print_error "Backup file $SSHD_BACKUP_FILE not found."
return 1
2025-06-30 19:36:22 +01:00
fi
2025-07-02 13:48:34 +01:00
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
2025-07-02 13:48:34 +01:00
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
2025-07-02 13:48:34 +01:00
else
systemctl restart "$SSH_SERVICE"
2025-07-02 13:48:34 +01:00
fi
2025-06-30 21:10:35 +01:00
for ((i=1; i<=10; i++)); do
if ss -tuln | grep -q ":$PREVIOUS_SSH_PORT"; then
print_success "Rollback successful."
return 0
2025-06-30 21:10:35 +01:00
fi
2025-07-02 13:48:34 +01:00
sleep 3
2025-06-30 21:10:35 +01:00
done
print_error "Rollback failed. Check systemctl status $SSH_SERVICE."
return 1
2025-06-30 17:23:39 +01:00
}
# --- Firewall Configuration ---
2025-06-26 16:22:23 +01:00
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."
2025-06-26 16:22:23 +01:00
fi
if confirm "Allow HTTP (port 80)?"; then
ufw allow http comment 'HTTP'
print_success "HTTP allowed."
2025-06-26 21:49:51 +01:00
fi
if confirm "Allow HTTPS (port 443)?"; then
ufw allow https comment 'HTTPS'
print_success "HTTPS allowed."
2025-06-26 21:49:51 +01:00
fi
if confirm "Allow Tailscale (UDP 41641)?"; then
ufw allow 41641/udp comment 'Tailscale VPN'
print_success "Tailscale allowed."
2025-06-29 13:40:03 +01:00
fi
if confirm "Add custom ports?"; then
2025-06-26 21:49:51 +01:00
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
2025-06-26 22:19:35 +01:00
local valid=true
2025-06-26 21:49:51 +01:00
for port in $CUSTOM_PORTS; do
if ! validate_ufw_port "$port"; then
print_error "Invalid port: $port."
2025-06-26 21:49:51 +01:00
valid=false
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
done
if [[ "$valid" == true ]]; then
for port in $CUSTOM_PORTS; do
ufw allow "$port" comment "Custom port $port"
print_success "Added rule for $port."
2025-06-26 16:22:23 +01:00
done
2025-06-26 21:49:51 +01:00
break
fi
done
2025-06-26 16:22:23 +01:00
fi
if ! ufw status | grep -q "Status: active"; then
ufw --force enable
print_success "UFW enabled."
2025-06-26 16:22:23 +01:00
fi
print_success "Firewall configured."
ufw status | tee -a "$LOG_FILE"
2025-06-26 16:22:23 +01:00
log "Firewall configuration completed."
}
# --- Fail2Ban Configuration ---
2025-06-28 12:51:55 +01:00
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 <<EOF
2025-06-28 12:51:55 +01:00
[sshd]
enabled = true
2025-07-07 21:08:21 +01:00
port = $SSH_PORT
maxretry = 5
bantime = 3600
findtime = 600
EOF
print_success "Fail2Ban SSH jail configured."
fi
if ! [[ -f /etc/fail2ban/jail.d/ufw-probes.conf ]]; then
tee /etc/fail2ban/jail.d/ufw-probes.conf > /dev/null <<EOF
2025-07-07 21:08:21 +01:00
[ufw-probes]
enabled = true
banaction = ufw
port = 0:65535
filter = ufw
2025-07-07 21:08:21 +01:00
logpath = /var/log/ufw.log
maxretry = 5
bantime = 3600
findtime = 600
2025-06-28 12:51:55 +01:00
EOF
print_success "Fail2Ban UFW jail configured."
2025-07-07 21:22:47 +01:00
fi
2025-06-28 12:51:55 +01:00
systemctl restart fail2ban
if systemctl is-active --quiet fail2ban; then
print_success "Fail2Ban configured."
2025-06-28 12:51:55 +01:00
else
print_error "Fail2Ban service failed."
exit 1
2025-06-28 12:51:55 +01:00
fi
log "Fail2Ban configuration completed."
}
# --- Auto Updates ---
2025-06-28 12:51:55 +01:00
configure_auto_updates() {
print_section "Automatic Updates"
if [[ -f /etc/apt/apt.conf.d/50unattended-upgrades ]]; then
print_info "Unattended-upgrades already configured."
else
tee /etc/apt/apt.conf.d/50unattended-upgrades > /dev/null <<EOF
Unattended-Upgrades::Allowed-Origins {
"${ID} ${VERSION_CODENAME}:security";
"${ID} ${VERSION_CODENAME}-security";
};
Unattended-Upgrades::Package-Blacklist { };
Unattended-Upgrades::AutoFixInterruptedDpkg "true";
Unattended-Upgrades::MinimalSteps "true";
Unattended-Upgrades::InstallOnShutdown "false";
Unattended-Upgrades::Remove-Unused-Dependencies "true";
Unattended-Upgrades::Automatic-Reboot "false";
EOF
tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null <<EOF
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF
systemctl enable --now unattended-upgrades
print_success "Unattended-upgrades configured."
fi
if systemctl is-active --quiet unattended-upgrades; then
print_success "Automatic updates enabled."
else
print_error "Unattended-upgrades failed."
exit 1
fi
log "Automatic updates configured."
}
# --- Time Synchronization ---
configure_time_sync() {
print_section "Time Synchronization"
systemctl enable --now chrony
sleep 2
if systemctl is-active --quiet chrony; then
print_success "Chrony active."
chronyc tracking | tee -a "$LOG_FILE"
2025-06-28 12:51:55 +01:00
else
print_error "Chrony failed to start."
exit 1
2025-06-28 12:51:55 +01:00
fi
log "Time synchronization completed."
2025-06-28 12:51:55 +01:00
}
# --- Kernel Hardening ---
configure_kernel_hardening() {
print_section "Kernel Hardening"
if [[ -f /etc/sysctl.d/99-du-hardening.conf ]]; then
print_info "Kernel hardening already applied."
return 0
fi
tee /etc/sysctl.d/99-du-hardening.conf > /dev/null <<EOF
kernel.unprivileged_bpf_disabled=1
kernel.yama.ptrace_scope=1
fs.protected_symlinks=1
fs.protected_hardlinks=1
kernel.core_pattern=|/bin/false
kernel.modules_disabled=0
kernel.randomize_va_space=2
kernel.dmesg_restrict=1
kernel.perf_event_paranoid=2
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_rfc1337=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
EOF
sysctl -p /etc/sysctl.d/99-du-hardening.conf >/dev/null
print_success "Kernel hardening applied."
log "Kernel hardening completed."
}
# --- Docker Installation ---
2025-06-28 12:51:55 +01:00
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."
2025-06-28 12:51:55 +01:00
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
2025-06-28 12:51:55 +01:00
fi
systemctl enable --now docker
if systemctl is-active --quiet docker; then
print_success "Docker installed and running."
2025-06-28 12:51:55 +01:00
else
print_error "Docker service failed."
2025-06-28 12:51:55 +01:00
exit 1
fi
log "Docker installation completed."
}
# --- Tailscale Installation ---
2025-06-28 12:51:55 +01:00
install_tailscale() {
print_section "Tailscale Installation"
2025-06-28 12:51:55 +01:00
if command -v tailscale >/dev/null 2>&1; then
print_info "Tailscale already installed."
2025-06-28 12:51:55 +01:00
return 0
fi
if ! confirm "Install Tailscale VPN?"; then
print_info "Skipping Tailscale installation."
2025-07-04 14:16:27 +01:00
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
2025-06-28 12:51:55 +01:00
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
2025-06-28 22:13:17 +01:00
fi
TS_COMMAND="tailscale up --auth-key=$AUTH_KEY --operator=$USERNAME"
2025-06-28 22:13:17 +01:00
if ! $TS_COMMAND; then
print_warning "Tailscale connection failed. Run manually: $TS_COMMAND"
return 0
2025-06-28 12:51:55 +01:00
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
2025-06-28 22:13:17 +01:00
fi
fi
print_info "Waiting for Tailscale ($i/3)..."
sleep 5
done
print_warning "Tailscale connection not verified."
log "Tailscale connection not verified."
2025-06-28 12:51:55 +01:00
}
# --- Backup Configuration ---
2025-06-28 11:43:10 +01:00
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
2025-06-28 11:43:10 +01:00
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."
2025-06-28 14:51:26 +01:00
return 1
2025-06-28 12:41:22 +01:00
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
2025-06-28 16:55:55 +01:00
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
2025-06-28 11:43:10 +01:00
fi
tee /usr/local/bin/backup.sh > /dev/null <<EOF
2025-06-28 11:43:10 +01:00
#!/bin/bash
rsync -avz --delete -e "$SSH_COMMAND" $BACKUP_DIRS "${BACKUP_USER}@${BACKUP_HOST}:${REMOTE_BACKUP_PATH}/" >> "$BACKUP_LOG" 2>&1
2025-06-28 11:43:10 +01:00
EOF
chmod +x /usr/local/bin/backup.sh
tee /etc/cron.d/du-backup > /dev/null <<EOF
$CRON_SCHEDULE $USERNAME /usr/local/bin/backup.sh
2025-06-28 11:43:10 +01:00
EOF
print_success "Backup configured."
2025-06-28 11:43:10 +01:00
log "Backup configuration completed."
}
# --- Swap Configuration ---
2025-06-26 16:22:23 +01:00
configure_swap() {
print_section "Swap Configuration"
2025-06-26 21:49:51 +01:00
if [[ $IS_CONTAINER == true ]]; then
print_warning "Swap not supported in containers."
2025-06-26 16:22:23 +01:00
return 0
fi
if [[ -f /swapfile ]]; then
print_info "Swap file already exists."
return 0
2025-06-26 16:22:23 +01:00
fi
read -rp "$(echo -e "${CYAN}Enter swap size (e.g., 2G, 512M): ${NC}")" SWAP_SIZE
if ! validate_swap_size "$SWAP_SIZE"; then
print_error "Invalid swap size."
return 1
2025-06-27 12:56:02 +01:00
fi
SWAP_BYTES=$(convert_to_bytes "$SWAP_SIZE")
print_info "Creating swap file of $SWAP_SIZE..."
fallocate -l "$SWAP_BYTES" /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
if ! grep -q "/swapfile" /etc/fstab; then
echo "/swapfile none swap sw 0 0" >> /etc/fstab
2025-06-26 16:22:23 +01:00
fi
print_success "Swap configured."
2025-06-26 16:22:23 +01:00
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 ---
2025-06-26 21:49:51 +01:00
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."
2025-06-26 21:49:51 +01:00
}
# --- Generate Summary ---
2025-06-26 21:49:51 +01:00
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[*]}"
2025-06-29 18:38:12 +01:00
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"
2025-06-26 16:22:23 +01:00
}
# --- Main Function ---
2025-06-26 16:22:23 +01:00
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
2025-06-26 16:22:23 +01:00
}
# --- Execute Main ---
main