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
2025-06-30 19:22:31 +01:00
# Version: 0.52-rc1 | 2025-06-30
2025-06-28 11:43:10 +01:00
# Changelog:
2025-06-30 11:37:08 +01:00
# - v0.51: corrected repo links
2025-06-30 10:38:28 +01:00
# - v0.50: versioning format change and repo name change
2025-06-29 21:29:44 +01:00
# - v4.3: Add SHA256 integrity verification
2025-06-29 13:10:41 +01:00
# - v4.2: Added Security Audit Tools (Integrating Lynis and Optionally Debsecan) & option to do Backup Testing
2025-06-29 19:09:50 +01:00
# Fixed debsecan compatibility (Debian-only), added global BACKUP_LOG, added backup testing
2025-06-28 23:12:54 +01:00
# - v4.1: Added tailscale config to connect to tailscale or headscale server
2025-06-28 18:48:26 +01:00
# - v4.0: Added automated backup config. Mainly for Hetzner Storage Box but can be used for any rsync/SSH enabled remote solution.
# - v3.*: Improvements to script flow and fixed bugs which were found in tests at Oracle Cloud
2025-06-26 16:22:23 +01:00
#
2025-06-26 21:49:51 +01:00
# 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
2025-06-28 18:48:26 +01:00
# like Docker and Tailscale and automated backups to Hetzner storage box or any rsync location.
# It is designed to be idempotent, safe.
2025-06-30 11:37:08 +01:00
# README at GitHub: https://github.com/buildplan/du_setup/blob/main/README.md
2025-06-26 21:49:51 +01:00
#
# Prerequisites:
2025-06-30 10:35:43 +01:00
# - Run as root on a fresh Debian 12 or Ubuntu server (e.g., sudo ./du_setup.sh or run as root ./du_setup.sh).
2025-06-26 21:49:51 +01:00
# - Internet connectivity is required for package installation.
#
# Usage:
2025-06-30 11:37:08 +01:00
# Download: wget https://raw.githubusercontent.com/buildplan/du_setup/refs/heads/main/du_setup.sh
2025-06-30 10:35:43 +01:00
# Make it executable: chmod +x du_setup.sh
# Run it: sudo ./du_setup.sh [--quiet]
2025-06-26 21:49:51 +01:00
#
# Options:
2025-06-28 18:48:26 +01:00
# --quiet: Suppress non-critical output for automation. (Not recommended always best to review all the options)
2025-06-26 21:49:51 +01:00
#
# Notes:
2025-06-30 10:35:43 +01:00
# - The script creates a log file in /var/log/du_setup_*.log.
2025-06-26 21:49:51 +01:00
# - 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.
2025-06-26 16:22:23 +01:00
set -euo pipefail # Exit on error, undefined vars, pipe failures
# --- GLOBAL VARIABLES & CONFIGURATION ---
# Colors for output
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' # No Color
# Script variables
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
2025-06-30 10:35:43 +01:00
LOG_FILE = " /var/log/du_setup_ $( date +%Y%m%d_%H%M%S) .log "
2025-06-29 18:38:12 +01:00
BACKUP_LOG = "/var/log/backup_rsync.log"
2025-06-26 16:22:23 +01:00
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 = ""
2025-06-26 21:49:51 +01:00
ID = "" # This will be populated from /etc/os-release
2025-06-29 20:08:19 +01:00
FAILED_SERVICES = ( )
2025-06-26 16:22:23 +01:00
# --- PARSE ARGUMENTS ---
while [ [ $# -gt 0 ] ] ; do
case $1 in
--quiet) VERBOSE = false; shift ; ;
*) shift ; ;
esac
done
# --- 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 } ║ ║ ${ NC } "
2025-06-26 22:19:35 +01:00
echo -e " ${ CYAN } ║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║ ${ NC } "
2025-06-30 19:22:31 +01:00
echo -e " ${ CYAN } ║ v0.52-rc | 2025-06-30 ║ ${ NC } "
2025-06-27 14:53:54 +01:00
echo -e " ${ CYAN } ║ ║ ${ 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( ) {
2025-06-27 12:56:02 +01:00
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 "
}
2025-06-26 21:49:51 +01:00
# --- USER INTERACTION ---
2025-06-26 16:22:23 +01:00
confirm( ) {
local prompt = " $1 "
local default = " ${ 2 :- n } "
local response
[ [ $VERBOSE = = false ] ] && return 0
if [ [ $default = = "y" ] ] ; then
prompt = " $prompt [Y/n]: "
else
prompt = " $prompt [y/N]: "
fi
while true; do
read -rp " $( echo -e " ${ CYAN } $prompt ${ NC } " ) " response
response = ${ response ,, }
if [ [ -z $response ] ] ; then
response = $default
fi
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( ) {
local username = " $1 "
[ [ " $username " = ~ ^[ a-z_] [ a-z0-9_-] *$ && ${# username } -le 32 ] ]
}
validate_hostname( ) {
local hostname = " $1 "
[ [ " $hostname " = ~ ^[ a-zA-Z0-9] [ a-zA-Z0-9.-] { 0,253} [ a-zA-Z0-9] $ && ! " $hostname " = ~ \. \. ] ]
}
validate_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " -ge 1024 && " $port " -le 65535 ] ]
}
2025-06-28 12:24:23 +01:00
validate_backup_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " -ge 1 && " $port " -le 65535 ] ]
}
2025-06-26 16:22:23 +01:00
validate_ssh_key( ) {
local key = " $1 "
[ [ -n " $key " && " $key " = ~ ^( ssh-rsa| ecdsa-sha2-nistp256| ecdsa-sha2-nistp384| ecdsa-sha2-nistp521| ssh-ed25519) \ ] ]
}
validate_timezone( ) {
local tz = " $1 "
[ [ -e " /usr/share/zoneinfo/ $tz " ] ]
}
validate_swap_size( ) {
2025-06-26 22:19:35 +01:00
local size_upper = " ${ 1 ^^ } " # Convert to uppercase for case-insensitivity
[ [ " $size_upper " = ~ ^[ 0-9] +[ MG] $ && " ${ size_upper %[MG] } " -ge 1 ] ]
2025-06-26 16:22:23 +01:00
}
validate_ufw_port( ) {
local port = " $1 "
2025-06-26 21:49:51 +01:00
# Matches port (e.g., 8080) or port/protocol (e.g., 8080/tcp, 123/udp)
2025-06-26 16:22:23 +01:00
[ [ " $port " = ~ ^[ 0-9] +( /tcp| /udp) ?$ ] ]
}
convert_to_bytes( ) {
2025-06-26 22:19:35 +01:00
local size_upper = " ${ 1 ^^ } " # Convert to uppercase for case-insensitivity
local unit = " ${ size_upper : -1 } "
local 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
}
2025-06-26 21:49:51 +01:00
# --- CORE FUNCTIONS ---
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
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" )
2025-06-26 16:22:23 +01:00
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 21:49:51 +01:00
print_success "Dependencies installed."
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
print_success "All essential dependencies are installed."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
log "Dependency check completed."
2025-06-26 16:22:23 +01:00
}
check_system( ) {
print_section "System Compatibility Check"
if [ [ $( id -u) -ne 0 ] ] ; then
2025-06-30 10:35:43 +01:00
print_error "This script must be run as root (e.g., sudo ./du_setup.sh)."
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 (like swap) will be skipped."
fi
if [ [ -f /etc/os-release ] ] ; then
source /etc/os-release
2025-06-27 00:36:26 +01:00
ID = $ID # Populate global ID variable
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
2025-06-26 16:22:23 +01:00
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
2025-06-26 21:49:51 +01:00
# Preliminary SSH service check
2025-06-26 16:22:23 +01:00
if ! dpkg -l openssh-server | grep -q ^ii; then
print_warning "openssh-server not installed. It will be installed in the next step."
2025-06-26 21:49:51 +01:00
else
2025-06-26 16:22:23 +01:00
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."
2025-06-26 21:49:51 +01:00
elif ps aux | grep -q "[s]shd" ; then
print_warning "Preliminary check: SSH daemon running but no standard service detected."
2025-06-26 16:22:23 +01:00
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
2025-06-26 21:49:51 +01:00
# 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
2025-06-26 22:19:35 +01:00
local SHADOW_PERMS
2025-06-26 16:22:23 +01:00
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"
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 ' already exists. "
if confirm "Use this 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. Use lowercase letters, 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 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
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 "
2025-06-26 21:49:51 +01:00
if ! confirm "\nContinue with this configuration?" "y" ; then print_info "Exiting." ; exit 0; fi
2025-06-26 16:22:23 +01:00
log " Configuration collected: USER= $USERNAME , HOST= $SERVER_NAME , PORT= $SSH_PORT "
}
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 or upgrade system packages."
exit 1
fi
print_info "Installing essential packages..."
if ! apt-get install -y -qq \
ufw fail2ban unattended-upgrades chrony \
2025-06-28 11:43:10 +01:00
rsync wget vim htop iotop nethogs netcat-traditional ncdu \
tree rsyslog cron jq gawk coreutils perl skopeo git \
2025-06-26 21:49:51 +01:00
openssh-client openssh-server; then
2025-06-26 16:22:23 +01:00
print_error "Failed to install one or more essential packages."
exit 1
fi
print_success "Essential packages installed."
log "Package installation completed."
}
setup_user( ) {
print_section "User Management"
2025-06-26 22:19:35 +01:00
local USER_HOME SSH_DIR AUTH_KEYS PASS1 PASS2 SSH_PUBLIC_KEY
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
if ! id " $USERNAME " & >/dev/null; then
print_error " User ' $USERNAME ' creation verification failed. "
exit 1
fi
2025-06-26 22:19:35 +01:00
print_info " Set a password for ' $USERNAME ' (required for sudo, or press Enter twice to skip for key-only access): "
2025-06-26 16:22:23 +01:00
while true; do
read -sp " $( echo -e " ${ CYAN } New password: ${ NC } " ) " PASS1
echo
read -sp " $( echo -e " ${ CYAN } Retype new password: ${ NC } " ) " PASS2
echo
if [ [ -z " $PASS1 " && -z " $PASS2 " ] ] ; then
print_warning "Password skipped. Relying on SSH key authentication."
log " Password setting skipped for ' $USERNAME '. "
break
elif [ [ " $PASS1 " = = " $PASS2 " ] ] ; then
2025-06-26 22:19:35 +01:00
# **SECURITY FIX**: Do not tee chpasswd output to log file.
if echo " $USERNAME : $PASS1 " | chpasswd >/dev/null 2>& 1; then
2025-06-26 16:22:23 +01:00
print_success " Password for ' $USERNAME ' updated. "
break
else
2025-06-26 22:19:35 +01:00
print_error "Failed to set password. This could be a permissions issue."
2025-06-26 16:22:23 +01:00
print_info "Try again or press Enter twice to skip."
log " Failed to set password for ' $USERNAME '. "
fi
else
print_error "Passwords do not match. Please try again."
fi
done
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
AUTH_KEYS = " $SSH_DIR /authorized_keys "
2025-06-27 14:53:54 +01:00
if confirm "Add SSH public key(s) from your local machine now?" ; then
while true; do # Loop to allow adding multiple keys
local SSH_PUBLIC_KEY # Declare locally to avoid issues
read -rp " $( echo -e " ${ CYAN } Paste your full SSH public key: ${ NC } " ) " SSH_PUBLIC_KEY
if validate_ssh_key " $SSH_PUBLIC_KEY " ; then
mkdir -p " $SSH_DIR "
chmod 700 " $SSH_DIR "
echo " $SSH_PUBLIC_KEY " >> " $AUTH_KEYS "
# De-duplicate keys after adding the new one
awk '!seen[$0]++' " $AUTH_KEYS " > " $AUTH_KEYS .tmp " && mv " $AUTH_KEYS .tmp " " $AUTH_KEYS "
chmod 600 " $AUTH_KEYS "
chown -R " $USERNAME : $USERNAME " " $SSH_DIR "
print_success "SSH public key added."
log " Added SSH public key for ' $USERNAME '. "
LOCAL_KEY_ADDED = true # Set this flag to true since at least one key was added
else
print_error "Invalid SSH key format. It should start with 'ssh-rsa', 'ecdsa-*', or 'ssh-ed25519'."
fi
if ! confirm "Do you have another SSH public key to add?" "n" ; then
print_info "Finished adding SSH keys."
break # User answered 'n', break the loop
fi
done
fi
2025-06-26 16:22:23 +01:00
print_success " User ' $USERNAME ' created. "
else
print_info " Using existing user: $USERNAME "
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
AUTH_KEYS = " $SSH_DIR /authorized_keys "
fi
print_info " Adding ' $USERNAME ' to sudo group... "
if ! groups " $USERNAME " | grep -qw sudo; then
if ! usermod -aG sudo " $USERNAME " ; then
print_error " Failed to add ' $USERNAME ' to sudo group. "
exit 1
fi
print_success "User added to sudo group."
else
print_info " User ' $USERNAME ' is already in the sudo group. "
fi
if getent group sudo | grep -qw " $USERNAME " ; then
print_success " Sudo group membership confirmed for ' $USERNAME '. "
else
print_warning " Sudo group membership verification failed. Please check manually with 'sudo -l' as $USERNAME . "
fi
log "User management completed."
}
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
2025-06-26 16:22:23 +01:00
print_info "Configuring timezone..."
2025-06-26 21:49:51 +01:00
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
2025-06-26 16:22:23 +01:00
if confirm "Configure system locales interactively?" ; then
dpkg-reconfigure locales
else
print_info "Skipping locale configuration."
fi
print_info "Configuring hostname..."
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 configured: $SERVER_NAME "
else
print_info " Hostname already set to $SERVER_NAME . "
fi
log "System configuration completed."
}
configure_ssh( ) {
print_section "SSH Hardening"
2025-06-26 22:19:35 +01:00
local CURRENT_SSH_PORT USER_HOME SSH_DIR SSH_KEY AUTH_KEYS NEW_SSH_CONFIG
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
# Ensure openssh-server is installed
2025-06-26 16:22:23 +01:00
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
2025-06-30 17:23:39 +01:00
# Detect SSH service name, prefer socket activation on Ubuntu if active
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"
print_info " Using SSH socket activation: $SSH_SERVICE "
elif [ [ $ID = = "ubuntu" ] ] && { systemctl is-enabled ssh.service >/dev/null 2>& 1 || systemctl is-active ssh.service >/dev/null 2>& 1; } ; then
2025-06-26 21:49:51 +01:00
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
2025-06-27 00:54:10 +01:00
print_warning "SSH daemon running but no standard service detected."
SSH_SERVICE = "ssh.service" # Default for Debian
2025-06-26 21:49:51 +01:00
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
2025-06-27 00:54:10 +01:00
print_error "Failed to start SSH daemon manually."
2025-06-26 21:49:51 +01:00
exit 1
fi
print_success "SSH daemon started manually."
fi
2025-06-26 16:22:23 +01:00
else
2025-06-26 22:19:35 +01:00
print_error "No SSH service or daemon detected. Please verify openssh-server installation and daemon status."
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
print_info " Using SSH service: $SSH_SERVICE "
2025-06-26 16:22:23 +01:00
log " Detected SSH service: $SSH_SERVICE "
2025-06-26 21:49:51 +01:00
# Ensure SSH service is enabled and running
2025-06-26 16:22:23 +01:00
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. "
exit 1
fi
print_success " SSH service enabled: $SSH_SERVICE "
fi
if ! systemctl is-active " $SSH_SERVICE " >/dev/null 2>& 1; then
if ! systemctl start " $SSH_SERVICE " >/dev/null 2>& 1; then
print_error " Failed to start $SSH_SERVICE . Attempting manual start... "
if ! /usr/sbin/sshd; then
print_error "Failed to start SSH daemon manually."
exit 1
fi
print_success "SSH daemon started manually."
fi
fi
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 "
2025-06-26 21:49:51 +01:00
if [ [ $LOCAL_KEY_ADDED = = false ] ] && [ [ ! -s " $AUTH_KEYS " ] ] ; then
2025-06-26 16:22:23 +01:00
print_info "No local key provided and no existing keys found. Generating new SSH key..."
mkdir -p " $SSH_DIR "
chmod 700 " $SSH_DIR "
sudo -u " $USERNAME " ssh-keygen -t ed25519 -f " $SSH_KEY " -N "" -q
cat " $SSH_KEY .pub " >> " $AUTH_KEYS "
chmod 600 " $AUTH_KEYS "
chown -R " $USERNAME : $USERNAME " " $SSH_DIR "
print_success "SSH key generated."
echo -e " ${ YELLOW } Public key for remote access: ${ NC } "
cat " $SSH_KEY .pub " | tee -a " $LOG_FILE "
echo -e " ${ YELLOW } Copy this key to your local ~/.ssh/authorized_keys or use 'ssh-copy-id -p $CURRENT_SSH_PORT $USERNAME @ $SERVER_IP ' from your local machine. ${ NC } "
else
print_info "SSH key(s) already present or added. Skipping key generation."
fi
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 } "
2025-06-27 12:25:51 +01:00
2025-06-26 16:22:23 +01:00
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
print_info "Backing up original SSH config..."
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-26 16:22:23 +01:00
2025-06-30 17:23:39 +01:00
# Update SSH port in sshd_config for Ubuntu 24.04+ socket activation
if [ [ $ID = = "ubuntu" ] ] && dpkg --compare-versions " $( lsb_release -rs) " ge "24.04" ; then
print_info "Updating SSH port in /etc/ssh/sshd_config for Ubuntu 24.04+ socket activation..."
if ! grep -q "^Port" /etc/ssh/sshd_config; then
echo " Port $SSH_PORT " >> /etc/ssh/sshd_config
else
sed -i " s/^Port .*/Port $SSH_PORT / " /etc/ssh/sshd_config
fi
else
# Fallback for older versions or non-Ubuntu
if [ [ " $SSH_SERVICE " = = "ssh.socket" ] ] ; then
print_info " Configuring SSH socket to listen on port $SSH_PORT ... "
NEW_SSH_CONFIG = $( mktemp)
tee " $NEW_SSH_CONFIG " > /dev/null <<EOF
2025-06-27 19:23:28 +01:00
[ Socket]
ListenStream =
ListenStream = $SSH_PORT
EOF
2025-06-30 17:23:39 +01:00
mkdir -p /etc/systemd/system/ssh.socket.d
mv " $NEW_SSH_CONFIG " /etc/systemd/system/ssh.socket.d/override.conf
chmod 644 /etc/systemd/system/ssh.socket.d/override.conf
else
print_info " Configuring SSH service to listen on port $SSH_PORT ... "
NEW_SSH_CONFIG = $( mktemp)
tee " $NEW_SSH_CONFIG " > /dev/null <<EOF
2025-06-27 00:54:10 +01:00
[ Service]
ExecStart =
ExecStart = /usr/sbin/sshd -D -p $SSH_PORT
EOF
2025-06-30 17:23:39 +01:00
mkdir -p /etc/systemd/system/ssh.service.d
mv " $NEW_SSH_CONFIG " /etc/systemd/system/ssh.service.d/override.conf
chmod 644 /etc/systemd/system/ssh.service.d/override.conf
fi
2025-06-27 19:23:28 +01:00
fi
2025-06-28 11:43:10 +01:00
2025-06-27 00:54:10 +01:00
# Apply additional hardening via sshd_config.d
2025-06-26 16:22:23 +01:00
NEW_SSH_CONFIG = $( mktemp)
tee " $NEW_SSH_CONFIG " > /dev/null <<EOF
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
X11Forwarding no
PrintMotd no
Banner /etc/issue.net
EOF
if [ [ -f /etc/ssh/sshd_config.d/99-hardening.conf ] ] && cmp -s " $NEW_SSH_CONFIG " /etc/ssh/sshd_config.d/99-hardening.conf; then
print_info "SSH configuration already hardened. Skipping."
rm -f " $NEW_SSH_CONFIG "
else
print_info "Creating or updating hardened SSH configuration..."
2025-06-26 22:19:35 +01:00
mkdir -p /etc/ssh/sshd_config.d
2025-06-26 17:12:59 +01:00
mv " $NEW_SSH_CONFIG " /etc/ssh/sshd_config.d/99-hardening.conf
2025-06-26 16:22:23 +01:00
chmod 644 /etc/ssh/sshd_config.d/99-hardening.conf
tee /etc/issue.net > /dev/null <<'EOF'
******************************************************************************
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
fi
2025-06-27 00:54:10 +01:00
print_info "Reloading systemd and restarting SSH service..."
systemctl daemon-reload
2025-06-30 17:23:39 +01:00
if [ [ $SSH_SERVICE = = "ssh.socket" ] ] ; then
if ! systemctl restart " $SSH_SERVICE " ; then
print_error "SSH socket failed to restart! Reverting changes..."
rollback_ssh_changes
exit 1
fi
else
if ! systemctl restart " $SSH_SERVICE " ; then
print_error "SSH service failed to restart! Reverting changes..."
rollback_ssh_changes
exit 1
fi
2025-06-27 00:54:10 +01:00
fi
# Wait and verify port binding
sleep 5
if ! ss -tuln | grep -q " : $SSH_PORT " ; then
print_error " SSH not listening on port $SSH_PORT after restart! Reverting changes... "
2025-06-30 17:23:39 +01:00
rollback_ssh_changes
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-27 00:54:10 +01:00
print_success " SSH service restarted on port $SSH_PORT . "
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
# Verify root SSH is disabled
2025-06-26 16:22:23 +01:00
print_info "Verifying root SSH login is disabled..."
if ssh -p " $SSH_PORT " -o BatchMode = yes -o ConnectTimeout = 5 root@localhost true 2>/dev/null; then
2025-06-26 22:19:35 +01:00
print_error "Root SSH login is still possible! Check SSH configuration."
2025-06-30 17:23:39 +01:00
rollback_ssh_changes
2025-06-26 16:22:23 +01:00
exit 1
else
print_success "Confirmed: Root SSH login is disabled."
fi
print_warning "CRITICAL: Test new SSH connection in a SEPARATE terminal NOW!"
print_info " Use: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP "
2025-06-30 17:23:39 +01:00
print_info " If login fails, ensure client uses the new port and check server logs (e.g., journalctl -u $SSH_SERVICE ). "
2025-06-26 21:49:51 +01:00
2025-06-27 00:36:26 +01:00
# Retry loop for SSH connection test
local retry_count = 0
local max_retries = 3
while ( ( retry_count < max_retries ) ) ; do
if confirm "Was the new SSH connection successful?" ; then
break
else
( ( retry_count++ ) )
if ( ( retry_count < max_retries ) ) ; then
print_info " Retrying SSH connection test ( $retry_count / $max_retries )... "
sleep 5
else
2025-06-30 19:22:31 +01:00
print_error "Aborting. Initiating rollback to original configuration..."
2025-06-30 17:23:39 +01:00
rollback_ssh_changes
2025-06-30 19:22:31 +01:00
if ss -tuln | grep -q " : $CURRENT_SSH_PORT " ; then
print_success " Rollback successful. SSH restored on original port $CURRENT_SSH_PORT . "
else
print_error "Rollback failed. SSH may not be accessible. Please investigate manually."
fi
2025-06-27 00:36:26 +01:00
exit 1
fi
fi
done
2025-06-26 16:22:23 +01:00
log "SSH hardening completed."
}
2025-06-30 17:23:39 +01:00
rollback_ssh_changes( ) {
print_info "Rolling back SSH configuration changes..."
2025-06-30 19:22:31 +01:00
print_info "Removing override and hardening files..."
2025-06-30 17:23:39 +01:00
rm -f /etc/systemd/system/ssh.service.d/override.conf
rm -f /etc/systemd/system/ssh.socket.d/override.conf
cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config
rm -f /etc/ssh/sshd_config.d/99-hardening.conf
2025-06-30 19:22:31 +01:00
print_info " Reloading systemd and restarting $SSH_SERVICE ... "
2025-06-30 17:23:39 +01:00
systemctl daemon-reload
if ! systemctl restart " $SSH_SERVICE " ; then
print_warning " Failed to restart $SSH_SERVICE after rollback. Attempting manual start... "
/usr/sbin/sshd || true
fi
sleep 5
if ss -tuln | grep -q " : $CURRENT_SSH_PORT " ; then
print_success " Rollback successful. SSH restored on original port $CURRENT_SSH_PORT . "
else
print_error "Rollback failed. SSH may not be accessible. Please investigate manually."
fi
}
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
print_info "Configuring UFW default policies..."
ufw default deny incoming
ufw default allow outgoing
fi
if ! ufw status | grep -qw " $SSH_PORT /tcp " ; then
print_info " Adding SSH rule for port $SSH_PORT ... "
ufw allow " $SSH_PORT " /tcp comment 'Custom SSH'
else
print_info " SSH rule for port $SSH_PORT already exists. "
fi
2025-06-26 21:49:51 +01:00
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."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
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."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
fi
2025-06-29 13:40:03 +01:00
if confirm "Allow Tailscale traffic (UDP 41641)?" ; then
if ! ufw status | grep -qw "41641/udp" ; then
ufw allow 41641/udp comment 'Tailscale VPN'
print_success "Tailscale traffic (UDP 41641) allowed."
log "Added UFW rule for Tailscale (41641/udp)."
else
print_info "Tailscale rule (UDP 41641) already exists."
fi
fi
2025-06-26 21:49:51 +01:00
if confirm "Add additional custom ports (e.g., 8080/tcp, 123/udp)?" ; then
while true; do
2025-06-26 22:19:35 +01:00
local CUSTOM_PORTS # Make variable local to the loop
2025-06-26 21:49:51 +01:00
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
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 format: $port . Use <port>[/tcp|/udp]. "
valid = false
2025-06-26 16:22:23 +01:00
break
fi
2025-06-26 21:49:51 +01:00
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
2025-06-29 13:40:03 +01:00
local CUSTOM_COMMENT
read -rp " $( echo -e " ${ CYAN } Enter comment for $port (e.g., 'My App Port'): ${ NC } " ) " CUSTOM_COMMENT
if [ [ -z " $CUSTOM_COMMENT " ] ] ; then
CUSTOM_COMMENT = " Custom port $port "
fi
# Sanitize comment to avoid breaking UFW command
CUSTOM_COMMENT = $( echo " $CUSTOM_COMMENT " | tr -d "'\"\\" )
ufw allow " $port " comment " $CUSTOM_COMMENT "
print_success " Added rule for $port with comment ' $CUSTOM_COMMENT '. "
log " Added UFW rule for $port with comment ' $CUSTOM_COMMENT '. "
2025-06-26 16:22:23 +01:00
fi
done
2025-06-26 21:49:51 +01:00
break
else
print_info "Please try again."
fi
done
2025-06-26 16:22:23 +01:00
fi
print_info "Enabling firewall..."
if ! ufw --force enable; then
print_error "Failed to enable UFW. Check 'journalctl -u ufw' for details."
exit 1
fi
if ufw status | grep -q "Status: active" ; then
print_success "Firewall is active."
else
print_error "UFW failed to activate. Check 'journalctl -u ufw' for details."
exit 1
fi
2025-06-29 13:40:03 +01:00
print_warning " ACTION REQUIRED: Check your VPS provider's edge firewall to allow opened ports (e.g., $SSH_PORT /tcp, 41641/udp for Tailscale). "
2025-06-26 16:22:23 +01:00
ufw status verbose | tee -a " $LOG_FILE "
log "Firewall configuration completed."
}
2025-06-28 12:51:55 +01:00
configure_fail2ban( ) {
print_section "Fail2Ban Configuration"
# Set the SSH port for Fail2Ban to monitor.
local SSH_PORTS_TO_MONITOR = " $SSH_PORT "
local NEW_FAIL2BAN_CONFIG
NEW_FAIL2BAN_CONFIG = $( mktemp)
tee " $NEW_FAIL2BAN_CONFIG " > /dev/null <<EOF
[ DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
backend = auto
[ sshd]
enabled = true
port = $SSH_PORTS_TO_MONITOR
logpath = %( sshd_log) s
backend = %( sshd_backend) s
EOF
if [ [ -f /etc/fail2ban/jail.local ] ] && cmp -s " $NEW_FAIL2BAN_CONFIG " /etc/fail2ban/jail.local; then
print_info "Fail2Ban configuration already correct. Skipping."
rm -f " $NEW_FAIL2BAN_CONFIG "
elif [ [ -f /etc/fail2ban/jail.local ] ] && grep -q "\[sshd\]" /etc/fail2ban/jail.local; then
print_info "Fail2Ban jail.local exists. Updating SSH port..."
sed -i " s/^\(port\s*=\s*\).*/\1 $SSH_PORTS_TO_MONITOR / " /etc/fail2ban/jail.local
rm -f " $NEW_FAIL2BAN_CONFIG "
else
print_info "Creating Fail2Ban local jail configuration..."
mv " $NEW_FAIL2BAN_CONFIG " /etc/fail2ban/jail.local
chmod 644 /etc/fail2ban/jail.local
fi
print_info "Enabling and restarting Fail2Ban..."
systemctl enable fail2ban
systemctl restart fail2ban
sleep 2
if systemctl is-active --quiet fail2ban; then
print_success " Fail2Ban is active and monitoring port(s) $SSH_PORTS_TO_MONITOR . "
fail2ban-client status sshd | tee -a " $LOG_FILE "
else
print_error "Fail2Ban service failed to start."
exit 1
fi
log "Fail2Ban configuration completed."
}
configure_auto_updates( ) {
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
2025-06-29 19:09:50 +01:00
# Check for existing unattended-upgrades configuration
if [ [ -f /etc/apt/apt.conf.d/50unattended-upgrades ] ] && grep -q "Unattended-Upgrade::Allowed-Origins" /etc/apt/apt.conf.d/50unattended-upgrades; then
print_info "Existing unattended-upgrades configuration found. Verify with 'cat /etc/apt/apt.conf.d/50unattended-upgrades'."
fi
2025-06-28 12:51:55 +01:00
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_info "Skipping automatic security updates."
fi
log "Automatic updates configuration completed."
}
install_docker( ) {
if ! confirm "Install Docker Engine (Optional)?" ; then
print_info "Skipping Docker installation."
return 0
fi
print_section "Docker Installation"
if command -v docker >/dev/null 2>& 1; then
print_info "Docker already installed."
return 0
fi
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
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..."
local NEW_DOCKER_CONFIG
NEW_DOCKER_CONFIG = $( mktemp)
tee " $NEW_DOCKER_CONFIG " > /dev/null <<EOF
{
"log-driver" : "json-file" ,
"log-opts" : { "max-size" : "10m" , "max-file" : "3" } ,
"live-restore" : true
}
EOF
mkdir -p /etc/docker
if [ [ -f /etc/docker/daemon.json ] ] && cmp -s " $NEW_DOCKER_CONFIG " /etc/docker/daemon.json; then
print_info "Docker daemon configuration already correct. Skipping."
rm -f " $NEW_DOCKER_CONFIG "
else
mv " $NEW_DOCKER_CONFIG " /etc/docker/daemon.json
chmod 644 /etc/docker/daemon.json
fi
systemctl daemon-reload
systemctl enable --now docker
print_info "Running Docker sanity check..."
if sudo -u " $USERNAME " docker run --rm hello-world 2>& 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_warning " NOTE: ' $USERNAME ' must log out and back in to use Docker without sudo. "
log "Docker installation completed."
}
install_tailscale( ) {
2025-06-28 22:13:17 +01:00
if ! confirm "Install and configure Tailscale VPN (Optional)?" ; then
2025-06-28 12:51:55 +01:00
print_info "Skipping Tailscale installation."
2025-06-28 22:13:17 +01:00
log "Tailscale installation skipped by user."
2025-06-28 12:51:55 +01:00
return 0
fi
2025-06-28 22:13:17 +01:00
print_section "Tailscale VPN Installation and Configuration"
2025-06-28 12:51:55 +01:00
if command -v tailscale >/dev/null 2>& 1; then
print_info "Tailscale already installed."
2025-06-29 20:08:19 +01:00
if systemctl is-active --quiet tailscaled && tailscale ip >/dev/null 2>& 1; then
local TS_IPS TS_IPV4
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
print_success " Tailscale service is active and connected. Node IPv4 in tailnet: $TS_IPV4 "
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 22:13:17 +01:00
return 0
else
print_warning "Tailscale installed but service is not active or not connected."
fi
else
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
log "Tailscale installation failed: Invalid install script."
return 0
fi
if ! /tmp/tailscale_install.sh; then
print_error "Failed to install Tailscale."
rm -f /tmp/tailscale_install.sh
log "Tailscale installation failed."
return 0
fi
rm -f /tmp/tailscale_install.sh
print_success "Tailscale installation complete."
log "Tailscale installation completed."
fi
# --- Configure Tailscale Connection ---
2025-06-29 20:08:19 +01:00
if systemctl is-active --quiet tailscaled && tailscale ip >/dev/null 2>& 1; then
local TS_IPS TS_IPV4
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
print_info " Tailscale is already connected. Node IPv4 in tailnet: $TS_IPV4 "
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 12:51:55 +01:00
return 0
fi
2025-06-28 22:13:17 +01:00
print_info "Configuring Tailscale connection..."
echo -e " ${ CYAN } Choose Tailscale connection method: ${ NC } "
echo -e " 1) Standard Tailscale (requires pre-auth key from https://login.tailscale.com/admin)"
echo -e " 2) Custom Tailscale server (requires server URL and pre-auth key)"
read -rp " $( echo -e " ${ CYAN } Enter choice (1-2) [1]: ${ NC } " ) " TS_CONNECTION
TS_CONNECTION = ${ TS_CONNECTION :- 1 }
local AUTH_KEY LOGIN_SERVER = ""
if [ [ " $TS_CONNECTION " = = "2" ] ] ; then
while true; do
read -rp " $( echo -e " ${ CYAN } Enter Tailscale server URL (e.g., https://ts.mydomain.cloud): ${ NC } " ) " LOGIN_SERVER
if [ [ " $LOGIN_SERVER " = ~ ^https://[ a-zA-Z0-9.-] +( :[ 0-9] +) ?$ ] ] ; then break; else print_error "Invalid URL. Must start with https://. Try again." ; fi
done
2025-06-28 12:51:55 +01:00
fi
2025-06-28 22:13:17 +01:00
while true; do
read -rp " $( echo -e " ${ CYAN } Enter Tailscale pre-auth key: ${ NC } " ) " AUTH_KEY
if [ [ " $TS_CONNECTION " = = "1" && " $AUTH_KEY " = ~ ^tskey-auth- ] ] ; then break
elif [ [ " $TS_CONNECTION " = = "2" && -n " $AUTH_KEY " ] ] ; then
print_warning " Ensure the pre-auth key is valid for your custom Tailscale server ( $LOGIN_SERVER ). "
break
else
print_error "Invalid key format. For standard connection, key must start with 'tskey-auth-'. For custom server, key cannot be empty."
fi
done
local TS_COMMAND = "tailscale up"
if [ [ " $TS_CONNECTION " = = "2" ] ] ; then
TS_COMMAND = " $TS_COMMAND --login-server= $LOGIN_SERVER "
fi
TS_COMMAND = " $TS_COMMAND --auth-key= $AUTH_KEY --operator= $USERNAME "
print_info " Connecting to Tailscale with: $TS_COMMAND "
if ! $TS_COMMAND ; then
print_warning "Failed to connect to Tailscale. Possible issues: invalid pre-auth key, network restrictions, or server unavailability."
print_info "Please run the following command manually after resolving the issue:"
echo -e " ${ CYAN } $TS_COMMAND ${ NC } "
log " Tailscale connection failed: $TS_COMMAND "
else
2025-06-28 23:12:54 +01:00
# Verify connection status with retries
local RETRIES = 3
local DELAY = 5
local CONNECTED = false
2025-06-29 20:08:19 +01:00
local TS_IPS TS_IPV4
2025-06-28 23:12:54 +01:00
for ( ( i = 1; i<= RETRIES; i++) ) ; do
2025-06-29 20:08:19 +01:00
if tailscale ip >/dev/null 2>& 1; then
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
if [ [ -n " $TS_IPV4 " && " $TS_IPV4 " != "Unknown" ] ] ; then
CONNECTED = true
break
fi
2025-06-28 23:12:54 +01:00
fi
print_info " Waiting for Tailscale to connect ( $i / $RETRIES )... "
sleep $DELAY
done
if $CONNECTED ; then
2025-06-29 20:08:19 +01:00
print_success " Tailscale connected successfully. Node IPv4 in tailnet: $TS_IPV4 "
2025-06-28 22:44:03 +01:00
log " Tailscale connected: $TS_COMMAND "
2025-06-28 23:12:54 +01:00
# Store connection details for summary
2025-06-28 23:50:46 +01:00
echo " ${ LOGIN_SERVER :- https : //controlplane.tailscale.com } " > /tmp/tailscale_server
2025-06-29 20:08:19 +01:00
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 23:12:54 +01:00
echo "None" > /tmp/tailscale_flags
2025-06-28 22:44:03 +01:00
else
2025-06-29 20:08:19 +01:00
print_warning "Tailscale connection attempt succeeded, but no IPs assigned."
print_info "Please verify with 'tailscale ip' and run the following command manually if needed:"
2025-06-28 22:44:03 +01:00
echo -e " ${ CYAN } $TS_COMMAND ${ NC } "
log " Tailscale connection not verified: $TS_COMMAND "
2025-06-28 23:50:46 +01:00
tailscale status > /tmp/tailscale_status.txt 2>& 1
log "Tailscale status output saved to /tmp/tailscale_status.txt for debugging"
2025-06-28 22:44:03 +01:00
fi
2025-06-28 12:51:55 +01:00
fi
2025-06-28 22:13:17 +01:00
# --- Configure Additional Flags ---
2025-06-28 22:44:03 +01:00
print_info "Select additional Tailscale options to configure (comma-separated, e.g., 1,3):"
echo -e " ${ CYAN } 1) SSH (--ssh) - WARNING: May restrict server access to Tailscale connections only ${ NC } "
echo -e " ${ CYAN } 2) Advertise as Exit Node (--advertise-exit-node) ${ NC } "
echo -e " ${ CYAN } 3) Accept DNS (--accept-dns) ${ NC } "
echo -e " ${ CYAN } 4) Accept Routes (--accept-routes) ${ NC } "
echo -e " ${ CYAN } Enter numbers (1-4) or leave blank to skip: ${ NC } "
read -rp " " TS_FLAG_CHOICES
local TS_FLAGS = ""
if [ [ -n " $TS_FLAG_CHOICES " ] ] ; then
if echo " $TS_FLAG_CHOICES " | grep -q "1" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --ssh "
fi
2025-06-28 22:44:03 +01:00
if echo " $TS_FLAG_CHOICES " | grep -q "2" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --advertise-exit-node "
fi
2025-06-28 22:44:03 +01:00
if echo " $TS_FLAG_CHOICES " | grep -q "3" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --accept-dns "
fi
2025-06-28 22:44:03 +01:00
if echo " $TS_FLAG_CHOICES " | grep -q "4" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --accept-routes "
fi
if [ [ -n " $TS_FLAGS " ] ] ; then
TS_COMMAND = "tailscale up"
if [ [ " $TS_CONNECTION " = = "2" ] ] ; then
TS_COMMAND = " $TS_COMMAND --login-server= $LOGIN_SERVER "
fi
TS_COMMAND = " $TS_COMMAND --auth-key= $AUTH_KEY --operator= $USERNAME $TS_FLAGS "
print_info " Reconfiguring Tailscale with additional options: $TS_COMMAND "
if ! $TS_COMMAND ; then
print_warning "Failed to reconfigure Tailscale with additional options."
print_info "Please run the following command manually after resolving the issue:"
echo -e " ${ CYAN } $TS_COMMAND ${ NC } "
log " Tailscale reconfiguration failed: $TS_COMMAND "
else
2025-06-28 23:12:54 +01:00
# Verify reconfiguration status with retries
local RETRIES = 3
local DELAY = 5
local CONNECTED = false
2025-06-29 20:08:19 +01:00
local TS_IPS TS_IPV4
2025-06-28 23:12:54 +01:00
for ( ( i = 1; i<= RETRIES; i++) ) ; do
2025-06-29 20:08:19 +01:00
if tailscale ip >/dev/null 2>& 1; then
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
if [ [ -n " $TS_IPV4 " && " $TS_IPV4 " != "Unknown" ] ] ; then
CONNECTED = true
break
fi
2025-06-28 23:12:54 +01:00
fi
print_info " Waiting for Tailscale to connect ( $i / $RETRIES )... "
sleep $DELAY
done
if $CONNECTED ; then
2025-06-29 20:08:19 +01:00
print_success " Tailscale reconfigured with additional options. Node IPv4 in tailnet: $TS_IPV4 "
2025-06-28 22:44:03 +01:00
log " Tailscale reconfigured: $TS_COMMAND "
2025-06-30 13:26:43 +01:00
# Store flags and IPs for summary
echo " $TS_FLAGS " | sed 's/ --/ /g' | sed 's/^ *//' > /tmp/tailscale_flags
2025-06-29 20:08:19 +01:00
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 22:44:03 +01:00
else
2025-06-29 20:08:19 +01:00
print_warning "Tailscale reconfiguration attempt succeeded, but no IPs assigned."
print_info "Please verify with 'tailscale ip' and run the following command manually if needed:"
2025-06-28 22:44:03 +01:00
echo -e " ${ CYAN } $TS_COMMAND ${ NC } "
log " Tailscale reconfiguration not verified: $TS_COMMAND "
2025-06-28 23:50:46 +01:00
tailscale status > /tmp/tailscale_status.txt 2>& 1
log "Tailscale status output saved to /tmp/tailscale_status.txt for debugging"
2025-06-28 22:44:03 +01:00
fi
2025-06-28 22:13:17 +01:00
fi
else
2025-06-28 22:44:03 +01:00
print_info "No valid Tailscale options selected."
log "No valid Tailscale options selected."
2025-06-28 22:13:17 +01:00
fi
2025-06-28 22:44:03 +01:00
else
print_info "No additional Tailscale options selected."
log "No additional Tailscale options applied."
2025-06-28 22:13:17 +01:00
fi
print_success "Tailscale setup complete."
2025-06-29 20:08:19 +01:00
print_info "Verify status: tailscale ip"
2025-06-28 22:13:17 +01:00
log "Tailscale setup completed."
2025-06-28 12:51:55 +01:00
}
2025-06-28 11:43:10 +01:00
setup_backup( ) {
print_section "Backup Configuration (rsync over SSH)"
if ! confirm "Configure rsync-based backups to a remote SSH server?" ; then
print_info "Skipping backup configuration."
2025-06-28 14:51:26 +01:00
log "Backup configuration skipped by user."
2025-06-28 11:43:10 +01:00
return 0
fi
2025-06-28 14:51:26 +01:00
# --- Pre-flight Check ---
if [ [ -z " $USERNAME " ] ] || ! id " $USERNAME " >/dev/null 2>& 1; then
print_error " Cannot configure backup: valid admin user (' $USERNAME ') not found. "
log "Backup configuration failed: USERNAME variable not set or user does not exist."
return 1
2025-06-28 12:41:22 +01:00
fi
2025-06-28 11:43:10 +01:00
local ROOT_SSH_DIR = "/root/.ssh"
local ROOT_SSH_KEY = " $ROOT_SSH_DIR /id_ed25519 "
2025-06-28 14:51:26 +01:00
local BACKUP_SCRIPT_PATH = "/root/run_backup.sh"
local EXCLUDE_FILE_PATH = "/root/rsync_exclude.txt"
local CRON_MARKER = "#-*- managed by setup_harden script -*-"
# --- Generate SSH Key for Root ---
2025-06-28 11:43:10 +01:00
if [ [ ! -f " $ROOT_SSH_KEY " ] ] ; then
2025-06-28 14:51:26 +01:00
print_info "Generating a dedicated SSH key for root's backup job..."
mkdir -p " $ROOT_SSH_DIR " && chmod 700 " $ROOT_SSH_DIR "
ssh-keygen -t ed25519 -f " $ROOT_SSH_KEY " -N "" -q
2025-06-28 11:43:10 +01:00
chown -R root:root " $ROOT_SSH_DIR "
2025-06-28 14:51:26 +01:00
print_success " Root SSH key generated at $ROOT_SSH_KEY "
log "Generated root SSH key for backups."
2025-06-28 11:43:10 +01:00
else
2025-06-28 14:51:26 +01:00
print_info " Existing root SSH key found at $ROOT_SSH_KEY . "
2025-06-28 11:43:10 +01:00
fi
2025-06-28 16:05:32 +01:00
# --- Collect Backup Destination Details with Retry Loops ---
2025-06-28 15:25:37 +01:00
local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH SSH_COPY_ID_FLAGS = ""
2025-06-28 16:05:32 +01:00
while true; do
read -rp " $( echo -e " ${ CYAN } Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${ NC } " ) " BACKUP_DEST
if [ [ " $BACKUP_DEST " = ~ ^[ a-zA-Z0-9._-] +@[ a-zA-Z0-9.-] +$ ] ] ; then break; else print_error "Invalid format. Expected user@host. Please try again." ; fi
done
while true; do
read -rp " $( echo -e " ${ CYAN } Enter destination SSH port (Hetzner uses 23) [22]: ${ NC } " ) " BACKUP_PORT
BACKUP_PORT = ${ BACKUP_PORT :- 22 }
if [ [ " $BACKUP_PORT " = ~ ^[ 0-9] +$ && " $BACKUP_PORT " -ge 1 && " $BACKUP_PORT " -le 65535 ] ] ; then break; else print_error "Invalid port. Must be between 1 and 65535. Please try again." ; fi
done
while true; do
read -rp " $( echo -e " ${ CYAN } Enter remote backup path (e.g., /home/my_backups/): ${ NC } " ) " REMOTE_BACKUP_PATH
if [ [ " $REMOTE_BACKUP_PATH " = ~ ^/[ ^[ :space:] ] */$ ] ] ; then break; else print_error "Invalid path. Must start and end with '/' and contain no spaces. Please try again." ; fi
done
2025-06-28 14:51:26 +01:00
print_info " Backup target set to: ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } on port ${ BACKUP_PORT } "
2025-06-28 16:05:32 +01:00
# --- Hetzner Specific Handling ---
if confirm "Is this backup destination a Hetzner Storage Box (requires special -s flag for key copy)?" ; then
SSH_COPY_ID_FLAGS = "-s"
2025-06-28 15:25:37 +01:00
print_info "Hetzner Storage Box mode enabled. Using '-s' for ssh-copy-id."
fi
2025-06-28 14:51:26 +01:00
# --- Handle SSH Key Copy ---
echo -e " ${ CYAN } Choose how to copy the root SSH key: ${ NC } "
2025-06-28 16:05:32 +01:00
echo -e " 1) Automate with password (requires sshpass, password stored briefly in memory)"
2025-06-28 14:51:26 +01:00
echo -e " 2) Manual copy (recommended)"
read -rp " $( echo -e " ${ CYAN } Enter choice (1-2) [2]: ${ NC } " ) " KEY_COPY_CHOICE
KEY_COPY_CHOICE = ${ KEY_COPY_CHOICE :- 2 }
if [ [ " $KEY_COPY_CHOICE " = = "1" ] ] ; then
if ! command -v sshpass >/dev/null 2>& 1; then
print_info "Installing sshpass for automated key copying..."
2025-06-28 16:30:39 +01:00
apt-get update -qq && apt-get install -y -qq sshpass || { print_warning "Failed to install sshpass. Falling back to manual copy." ; KEY_COPY_CHOICE = 2; }
2025-06-28 13:17:23 +01:00
fi
2025-06-28 14:51:26 +01:00
if [ [ " $KEY_COPY_CHOICE " = = "1" ] ] ; then
2025-06-28 16:05:32 +01:00
read -sp " $( echo -e " ${ CYAN } Enter password for $BACKUP_DEST : ${ NC } " ) " BACKUP_PASSWORD; echo
2025-06-28 16:30:39 +01:00
# Ensure ~/.ssh/ exists on remote for Hetzner
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
ssh -p " $BACKUP_PORT " " $BACKUP_DEST " "mkdir -p ~/.ssh && chmod 700 ~/.ssh" 2>/dev/null || print_warning "Failed to create ~/.ssh on remote server."
fi
if SSHPASS = " $BACKUP_PASSWORD " sshpass -e ssh-copy-id -p " $BACKUP_PORT " -i " $ROOT_SSH_KEY .pub " $SSH_COPY_ID_FLAGS " $BACKUP_DEST " 2>& 1 | tee /tmp/ssh-copy-id.log; then
2025-06-28 14:51:26 +01:00
print_success "SSH key copied successfully."
2025-06-28 13:17:23 +01:00
else
2025-06-28 16:30:39 +01:00
print_error "Automated SSH key copy failed. Error details in /tmp/ssh-copy-id.log."
print_info "Please verify the password and ensure ~/.ssh/authorized_keys is writable on the remote server."
2025-06-28 14:51:26 +01:00
KEY_COPY_CHOICE = 2
2025-06-28 12:11:20 +01:00
fi
fi
2025-06-28 11:43:10 +01:00
fi
2025-06-28 14:51:26 +01:00
if [ [ " $KEY_COPY_CHOICE " = = "2" ] ] ; then
print_warning "ACTION REQUIRED: Copy the root SSH key to the backup destination."
2025-06-28 16:30:39 +01:00
echo -e "This will allow the root user to connect without a password for automated backups."
2025-06-28 16:05:32 +01:00
echo -e " ${ YELLOW } The root user's public key is: ${ NC } " ; cat " ${ ROOT_SSH_KEY } .pub " ; echo
2025-06-28 14:51:26 +01:00
echo -e " ${ YELLOW } Run the following command from this server's terminal to copy the key: ${ NC } "
2025-06-28 16:05:32 +01:00
echo -e " ${ CYAN } ssh-copy-id -p \" ${ BACKUP_PORT } \" -i \" ${ ROOT_SSH_KEY } .pub\" ${ SSH_COPY_ID_FLAGS } \" ${ BACKUP_DEST } \" ${ NC } " ; echo
2025-06-28 16:30:39 +01:00
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
print_info " For Hetzner, ensure ~/.ssh/ exists on the remote server: ssh -p \" $BACKUP_PORT \" \" $BACKUP_DEST \" \"mkdir -p ~/.ssh && chmod 700 ~/.ssh\" "
fi
2025-06-28 14:51:26 +01:00
fi
2025-06-28 16:05:32 +01:00
# --- SSH Connection Test ---
2025-06-28 14:51:26 +01:00
if confirm "Test SSH connection to the backup destination (recommended)?" ; then
2025-06-28 16:05:32 +01:00
print_info "Testing SSH connection (timeout: 10 seconds)..."
2025-06-28 16:30:39 +01:00
if [ [ ! -f " $ROOT_SSH_DIR /known_hosts " ] ] || ! grep -q " $BACKUP_DEST " " $ROOT_SSH_DIR /known_hosts " ; then
print_warning "SSH key may not be copied yet. Connection test may fail."
fi
local test_command = " ssh -p \" $BACKUP_PORT \" -o BatchMode=yes -o ConnectTimeout=10 \" $BACKUP_DEST \" true "
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
test_command = " sftp -P \" $BACKUP_PORT \" -o BatchMode=yes -o ConnectTimeout=10 \" $BACKUP_DEST \" <<< 'quit' "
fi
if eval " $test_command " 2>/dev/null; then
2025-06-28 15:25:37 +01:00
print_success "SSH connection to backup destination successful!"
else
2025-06-28 14:51:26 +01:00
print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open."
2025-06-28 16:05:32 +01:00
print_info " - Copy key: ssh-copy-id -p \" $BACKUP_PORT \" -i \" $ROOT_SSH_KEY .pub\" $SSH_COPY_ID_FLAGS \" $BACKUP_DEST \" "
2025-06-28 14:51:26 +01:00
print_info " - Check port: nc -zv $( echo \" $BACKUP_DEST \" | cut -d'@' -f2) \" $BACKUP_PORT \" "
2025-06-28 16:05:32 +01:00
print_info " - Ensure key is in ~/.ssh/authorized_keys on the backup server."
2025-06-28 16:30:39 +01:00
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
print_info " - For Hetzner, ensure ~/.ssh/ exists: ssh -p \" $BACKUP_PORT \" \" $BACKUP_DEST \" \"mkdir -p ~/.ssh && chmod 700 ~/.ssh\" "
fi
2025-06-28 11:43:10 +01:00
fi
fi
2025-06-28 16:30:39 +01:00
2025-06-28 14:51:26 +01:00
# --- Create Exclude File ---
print_info " Creating rsync exclude file at $EXCLUDE_FILE_PATH ... "
tee " $EXCLUDE_FILE_PATH " > /dev/null <<'EOF'
# Default Exclusions
2025-06-28 11:43:10 +01:00
.cache/
.docker/
.local/
2025-06-28 14:51:26 +01:00
.npm/
.vscode-server/
*.log
2025-06-28 11:43:10 +01:00
*.tmp
2025-06-28 14:51:26 +01:00
node_modules/
.bash_history
.wget-hsts
2025-06-28 11:43:10 +01:00
EOF
2025-06-28 14:51:26 +01:00
if confirm "Add more directories/files to the exclude list?" ; then
read -rp " $( echo -e " ${ CYAN } Enter items separated by spaces (e.g., Videos/ 'My Documents/'): ${ NC } " ) " -a extra_excludes
2025-06-28 16:05:32 +01:00
for item in " ${ extra_excludes [@] } " ; do echo " $item " >> " $EXCLUDE_FILE_PATH " ; done
2025-06-28 11:43:10 +01:00
fi
2025-06-28 14:51:26 +01:00
chmod 600 " $EXCLUDE_FILE_PATH "
2025-06-28 11:43:10 +01:00
print_success "Rsync exclude file created."
2025-06-28 14:51:26 +01:00
# --- Collect Cron Schedule ---
2025-06-28 16:55:55 +01:00
local CRON_SCHEDULE = "5 3 * * *"
print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help."
read -rp " $( echo -e " ${ CYAN } Enter schedule (default: daily at 3:05 AM) [ ${ CRON_SCHEDULE } ]: ${ NC } " ) " input
CRON_SCHEDULE = " ${ input :- $CRON_SCHEDULE } "
if ! echo " $CRON_SCHEDULE " | grep -qE '^((\*\/)?[0-9,-]+|\*)\s+(((\*\/)?[0-9,-]+|\*)\s+){3}((\*\/)?[0-9,-]+|\*|[0-6])$' ; then
print_error " Invalid cron expression. Using default: ${ CRON_SCHEDULE } "
fi
2025-06-28 14:51:26 +01:00
# --- Collect Notification Details ---
local NOTIFICATION_SETUP = "none" NTFY_URL = "" NTFY_TOKEN = "" DISCORD_WEBHOOK = ""
if confirm "Enable backup status notifications?" ; then
2025-06-28 16:05:32 +01:00
echo -e " ${ CYAN } Select notification method: 1) ntfy.sh 2) Discord [1]: ${ NC } " ; read -r n_choice
2025-06-28 14:51:26 +01:00
if [ [ " $n_choice " = = "2" ] ] ; then
NOTIFICATION_SETUP = "discord"
read -rp " $( echo -e " ${ CYAN } Enter Discord Webhook URL: ${ NC } " ) " DISCORD_WEBHOOK
2025-06-28 17:42:48 +01:00
if [ [ ! " $DISCORD_WEBHOOK " = ~ ^https://discord.com/api/webhooks/ ] ] ; then
print_error "Invalid Discord webhook URL."
log "Invalid Discord webhook URL provided."
return 1
fi
2025-06-28 14:51:26 +01:00
else
NOTIFICATION_SETUP = "ntfy"
read -rp " $( echo -e " ${ CYAN } Enter ntfy URL/topic (e.g., https://ntfy.sh/my-backups): ${ NC } " ) " NTFY_URL
read -rp " $( echo -e " ${ CYAN } Enter ntfy Access Token (optional): ${ NC } " ) " NTFY_TOKEN
2025-06-28 17:42:48 +01:00
if [ [ ! " $NTFY_URL " = ~ ^https?:// ] ] ; then
print_error "Invalid ntfy URL."
log "Invalid ntfy URL provided."
return 1
fi
2025-06-28 14:51:26 +01:00
fi
2025-06-28 11:43:10 +01:00
fi
2025-06-28 14:51:26 +01:00
# --- Generate the Backup Script ---
print_info " Generating the backup script at $BACKUP_SCRIPT_PATH ... "
2025-06-28 17:42:48 +01:00
if ! tee " $BACKUP_SCRIPT_PATH " > /dev/null <<EOF
2025-06-28 11:43:10 +01:00
#!/bin/bash
2025-06-28 14:51:26 +01:00
# Generated by server setup script on $(date)
2025-06-28 16:05:32 +01:00
set -Euo pipefail; umask 077
2025-06-28 14:51:26 +01:00
# --- CONFIGURATION ---
LOCAL_DIR = " /home/ ${ USERNAME } / "
REMOTE_DEST = " ${ BACKUP_DEST } "
REMOTE_PATH = " ${ REMOTE_BACKUP_PATH } "
SSH_PORT = " ${ BACKUP_PORT } "
EXCLUDE_FILE = " ${ EXCLUDE_FILE_PATH } "
LOG_FILE = "/var/log/backup_rsync.log"
LOCK_FILE = "/tmp/backup_rsync.lock"
HOSTNAME = "\$(hostname -f)"
NOTIFICATION_SETUP = " ${ NOTIFICATION_SETUP } "
NTFY_URL = " ${ NTFY_URL } "
NTFY_TOKEN = " ${ NTFY_TOKEN } "
DISCORD_WEBHOOK = " ${ DISCORD_WEBHOOK } "
2025-06-28 11:43:10 +01:00
EOF
2025-06-28 17:42:48 +01:00
then
print_error " Failed to create backup script at $BACKUP_SCRIPT_PATH . "
log " Failed to create backup script at $BACKUP_SCRIPT_PATH . "
return 1
fi
if ! tee -a " $BACKUP_SCRIPT_PATH " > /dev/null <<'EOF'
2025-06-28 16:05:32 +01:00
# --- BACKUP SCRIPT LOGIC ---
2025-06-28 11:43:10 +01:00
send_notification( ) {
2025-06-28 16:05:32 +01:00
local status = " $1 " message = " $2 " title color
if [ [ " $status " = = "SUCCESS" ] ] ; then title = " ✅ Backup SUCCESS: $HOSTNAME " ; color = 3066993; else title = " ❌ Backup FAILED: $HOSTNAME " ; color = 15158332; fi
2025-06-28 14:51:26 +01:00
if [ [ " $NOTIFICATION_SETUP " = = "ntfy" ] ] ; then
2025-06-28 16:05:32 +01:00
curl -s -H " Title: $title " ${ NTFY_TOKEN : +-H " Authorization: Bearer $NTFY_TOKEN " } -d " $message " " $NTFY_URL " > /dev/null 2>& 1
2025-06-28 14:51:26 +01:00
elif [ [ " $NOTIFICATION_SETUP " = = "discord" ] ] ; then
2025-06-28 16:05:32 +01:00
local escaped_message = $( echo " $message " | sed 's/"/\\"/g' | sed 's/\\/\\\\/g' | sed ':a;N;$!ba;s/\n/\\n/g' )
local json_payload = $( printf '{"embeds": [{"title": "%s", "description": "%s", "color": %d}]}' " $title " " $escaped_message " " $color " )
2025-06-28 14:51:26 +01:00
curl -s -H "Content-Type: application/json" -d " $json_payload " " $DISCORD_WEBHOOK " > /dev/null 2>& 1
2025-06-28 11:43:10 +01:00
fi
}
2025-06-28 16:05:32 +01:00
# --- DEPENDENCY & LOCKING ---
2025-06-28 16:30:39 +01:00
for cmd in rsync flock numfmt awk; do if ! command -v " $cmd " & >/dev/null; then send_notification "FAILURE" " FATAL: ' $cmd ' not found. " ; exit 10; fi ; done
2025-06-28 16:05:32 +01:00
exec 200>" $LOCK_FILE " ; flock -n 200 || { echo "Backup already running." ; exit 1; }
# --- LOG ROTATION ---
touch " $LOG_FILE " ; chmod 600 " $LOG_FILE " ; if [ [ -f " $LOG_FILE " && $( stat -c%s " $LOG_FILE " ) -gt 10485760 ] ] ; then mv " $LOG_FILE " " ${ LOG_FILE } .1 " ; fi
2025-06-28 14:51:26 +01:00
echo " --- Starting Backup at $( date) --- " >> " $LOG_FILE "
2025-06-28 16:05:32 +01:00
# --- RSYNC COMMAND ---
rsync_output = $( rsync -avz --delete --stats --exclude-from= " $EXCLUDE_FILE " -e " ssh -p $SSH_PORT " " $LOCAL_DIR " " ${ REMOTE_DEST } : ${ REMOTE_PATH } " 2>& 1)
rsync_exit_code = $? ; echo " $rsync_output " >> " $LOG_FILE "
# --- NOTIFICATION ---
2025-06-28 14:51:26 +01:00
if [ [ $rsync_exit_code -eq 0 ] ] ; then
data_transferred = $( echo " $rsync_output " | grep 'Total transferred file size' | awk '{print $5}' | sed 's/,//g' )
2025-06-28 15:25:37 +01:00
human_readable = $( numfmt --to= iec-i --suffix= B --format= "%.2f" " $data_transferred " 2>/dev/null || echo "0 B" )
2025-06-28 16:05:32 +01:00
message = " Backup completed successfully.\nData Transferred: ${ human_readable } "
2025-06-28 14:51:26 +01:00
send_notification "SUCCESS" " $message "
2025-06-28 11:43:10 +01:00
else
2025-06-28 14:51:26 +01:00
message = " rsync failed with exit code ${ rsync_exit_code } . Check log for details. "
send_notification "FAILURE" " $message "
2025-06-28 11:43:10 +01:00
fi
EOF
2025-06-28 17:42:48 +01:00
then
print_error " Failed to append to backup script at $BACKUP_SCRIPT_PATH . "
log " Failed to append to backup script at $BACKUP_SCRIPT_PATH . "
return 1
fi
if ! chmod 700 " $BACKUP_SCRIPT_PATH " ; then
print_error " Failed to set permissions on $BACKUP_SCRIPT_PATH . "
log " Failed to set permissions on $BACKUP_SCRIPT_PATH . "
return 1
fi
2025-06-28 14:51:26 +01:00
print_success "Backup script created."
2025-06-29 13:10:41 +01:00
# --- Backup test ---
test_backup
2025-06-28 14:51:26 +01:00
# --- Configure Cron Job ---
print_info "Configuring root cron job..."
2025-06-28 17:42:48 +01:00
# Ensure crontab is writable
local CRON_DIR = "/var/spool/cron/crontabs"
mkdir -p " $CRON_DIR "
chmod 1730 " $CRON_DIR "
chown root:crontab " $CRON_DIR "
# Validate inputs
if [ [ -z " $CRON_SCHEDULE " || -z " $BACKUP_SCRIPT_PATH " ] ] ; then
print_error "Cron schedule or backup script path is empty."
log " Cron configuration failed: CRON_SCHEDULE=' $CRON_SCHEDULE ', BACKUP_SCRIPT_PATH=' $BACKUP_SCRIPT_PATH ' "
return 1
fi
if [ [ ! -f " $BACKUP_SCRIPT_PATH " ] ] ; then
print_error " Backup script $BACKUP_SCRIPT_PATH does not exist. "
log " Cron configuration failed: Backup script $BACKUP_SCRIPT_PATH not found. "
return 1
fi
# Create temporary cron file
local TEMP_CRON
TEMP_CRON = $( mktemp)
if ! crontab -u root -l 2>/dev/null | grep -v " $CRON_MARKER " > " $TEMP_CRON " ; then
print_warning "No existing crontab found or error reading crontab. Creating new one."
: > " $TEMP_CRON " # Create empty file
fi
echo " $CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER " >> " $TEMP_CRON "
if ! crontab -u root " $TEMP_CRON " 2>& 1 | tee -a " $LOG_FILE " ; then
print_error "Failed to configure cron job."
log "Cron configuration failed: Error updating crontab."
rm -f " $TEMP_CRON "
return 1
fi
rm -f " $TEMP_CRON "
2025-06-28 14:51:26 +01:00
print_success " Backup cron job scheduled: $CRON_SCHEDULE "
2025-06-28 11:43:10 +01:00
log "Backup configuration completed."
}
2025-06-29 13:10:41 +01:00
test_backup( ) {
print_section "Backup Configuration Test"
if [ [ ! -f /root/run_backup.sh ] ] ; then
print_error "Backup script not found. Cannot run test."
log "Backup test failed: /root/run_backup.sh not found."
return 1
fi
if ! confirm "Run a test backup to verify configuration?" ; then
print_info "Skipping backup test."
log "Backup test skipped by user."
return 0
fi
local BACKUP_DEST = $( grep "^REMOTE_DEST=" /root/run_backup.sh | cut -d'"' -f2 || echo "unknown" )
local BACKUP_PORT = $( grep "^SSH_PORT=" /root/run_backup.sh | cut -d'"' -f2 || echo "22" )
local REMOTE_BACKUP_PATH = $( grep "^REMOTE_PATH=" /root/run_backup.sh | cut -d'"' -f2 || echo "unknown" )
local BACKUP_LOG = "/var/log/backup_rsync.log"
if [ [ " $BACKUP_DEST " = = "unknown" || " $REMOTE_BACKUP_PATH " = = "unknown" ] ] ; then
print_error "Invalid backup configuration in /root/run_backup.sh."
log "Backup test failed: Invalid configuration in /root/run_backup.sh."
return 1
fi
# Create a temporary test file
local TEST_DIR = " /root/test_backup_ $( date +%Y%m%d_%H%M%S) "
mkdir -p " $TEST_DIR "
echo "Test file for backup verification" > " $TEST_DIR /test.txt "
chmod 600 " $TEST_DIR /test.txt "
print_info " Running test backup to $BACKUP_DEST : $REMOTE_BACKUP_PATH ... "
local RSYNC_OUTPUT
RSYNC_OUTPUT = $( rsync -avz --delete -e " ssh -p $BACKUP_PORT " " $TEST_DIR / " " ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } test_backup/ " 2>& 1)
local RSYNC_EXIT_CODE = $?
echo " --- Test Backup at $( date) --- " >> " $BACKUP_LOG "
echo " $RSYNC_OUTPUT " >> " $BACKUP_LOG "
if [ [ $RSYNC_EXIT_CODE -eq 0 ] ] ; then
2025-06-29 18:38:12 +01:00
echo "Test backup successful" >> " $BACKUP_LOG "
2025-06-29 13:10:41 +01:00
print_success " Test backup successful! Check $BACKUP_LOG for details. "
log "Test backup successful."
else
print_error " Test backup failed (exit code: $RSYNC_EXIT_CODE ). Check $BACKUP_LOG for details. "
print_info "Troubleshooting steps:"
print_info " - Verify SSH key: cat /root/.ssh/id_ed25519.pub"
print_info " - Copy key: ssh-copy-id -p \" $BACKUP_PORT \" -i /root/.ssh/id_ed25519.pub \" $BACKUP_DEST \" "
print_info " - Test SSH: ssh -p \" $BACKUP_PORT \" \" $BACKUP_DEST \" true "
log " Test backup failed with exit code $RSYNC_EXIT_CODE . "
fi
# Clean up test directory
rm -rf " $TEST_DIR "
print_success "Backup test completed."
log "Backup test completed."
}
2025-06-26 16:22:23 +01:00
configure_swap( ) {
2025-06-26 21:49:51 +01:00
if [ [ $IS_CONTAINER = = true ] ] ; then
print_info "Swap configuration skipped in container."
2025-06-26 16:22:23 +01:00
return 0
fi
2025-06-26 21:49:51 +01:00
print_section "Swap Configuration"
2025-06-29 19:09:50 +01:00
# Check for existing swap partition
if lsblk -r | grep -q '\[SWAP\]' ; then
print_warning "Existing swap partition found. Verify with 'lsblk -f'. Proceed with caution."
fi
2025-06-26 21:49:51 +01:00
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
2025-06-26 22:19:35 +01:00
local SWAP_SIZE
2025-06-26 21:49:51 +01:00
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
2025-06-26 22:19:35 +01:00
local REQUIRED_SPACE
2025-06-26 21:49:51 +01:00
REQUIRED_SPACE = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 22:19:35 +01:00
local AVAILABLE_SPACE
2025-06-26 21:49:51 +01:00
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
2025-06-26 22:19:35 +01:00
local SWAP_SIZE
2025-06-26 21:49:51 +01:00
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
2025-06-26 22:19:35 +01:00
local REQUIRED_SPACE
2025-06-26 21:49:51 +01:00
REQUIRED_SPACE = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 22:19:35 +01:00
local AVAILABLE_SPACE
2025-06-26 21:49:51 +01:00
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."
2025-06-26 22:19:35 +01:00
rm -f /swapfile || true
2025-06-26 21:49:51 +01:00
exit 1
fi
2025-06-27 13:47:19 +01:00
# Check for existing swap entry in /etc/fstab to prevent duplicates
if grep -q '^/swapfile ' /etc/fstab; then
print_info "Swap entry already exists in /etc/fstab. Skipping."
else
2025-06-26 21:49:51 +01:00
echo '/swapfile none swap sw 0 0' >> /etc/fstab
2025-06-27 13:47:19 +01:00
print_success "Swap entry added to /etc/fstab."
2025-06-26 21:49:51 +01:00
fi
print_success " Swap file created: $SWAP_SIZE "
2025-06-26 16:22:23 +01:00
fi
2025-06-27 12:56:02 +01:00
print_info "Configuring swap settings..."
local SWAPPINESS = 10
local CACHE_PRESSURE = 50
if confirm "Customize swap settings (vm.swappiness and vm.vfs_cache_pressure)?" ; then
while true; do
read -rp " $( echo -e " ${ CYAN } Enter vm.swappiness (0-100) [default: $SWAPPINESS ]: ${ NC } " ) " INPUT_SWAPPINESS
INPUT_SWAPPINESS = ${ INPUT_SWAPPINESS :- $SWAPPINESS }
if [ [ " $INPUT_SWAPPINESS " = ~ ^[ 0-9] +$ && " $INPUT_SWAPPINESS " -ge 0 && " $INPUT_SWAPPINESS " -le 100 ] ] ; then
SWAPPINESS = $INPUT_SWAPPINESS
break
else
print_error "Invalid value for vm.swappiness. Must be between 0 and 100."
fi
done
while true; do
read -rp " $( echo -e " ${ CYAN } Enter vm.vfs_cache_pressure (1-1000) [default: $CACHE_PRESSURE ]: ${ NC } " ) " INPUT_CACHE_PRESSURE
INPUT_CACHE_PRESSURE = ${ INPUT_CACHE_PRESSURE :- $CACHE_PRESSURE }
if [ [ " $INPUT_CACHE_PRESSURE " = ~ ^[ 0-9] +$ && " $INPUT_CACHE_PRESSURE " -ge 1 && " $INPUT_CACHE_PRESSURE " -le 1000 ] ] ; then
CACHE_PRESSURE = $INPUT_CACHE_PRESSURE
break
else
print_error "Invalid value for vm.vfs_cache_pressure. Must be between 1 and 1000."
fi
done
else
print_info " Using default swap settings (vm.swappiness= $SWAPPINESS , vm.vfs_cache_pressure= $CACHE_PRESSURE ). "
fi
2025-06-26 22:19:35 +01:00
local NEW_SWAP_CONFIG
2025-06-26 21:49:51 +01:00
NEW_SWAP_CONFIG = $( mktemp)
tee " $NEW_SWAP_CONFIG " > /dev/null <<EOF
2025-06-27 12:56:02 +01:00
vm.swappiness= $SWAPPINESS
vm.vfs_cache_pressure= $CACHE_PRESSURE
2025-06-26 21:49:51 +01:00
EOF
2025-06-27 13:47:19 +01:00
# Check if sysctl settings are already correct to prevent duplicates
2025-06-26 21:49:51 +01:00
if [ [ -f /etc/sysctl.d/99-swap.conf ] ] && cmp -s " $NEW_SWAP_CONFIG " /etc/sysctl.d/99-swap.conf; then
2025-06-27 13:47:19 +01:00
print_info "Swap settings already correct in /etc/sysctl.d/99-swap.conf. Skipping."
2025-06-26 21:49:51 +01:00
rm -f " $NEW_SWAP_CONFIG "
else
2025-06-27 13:47:19 +01:00
# Check for conflicting settings in /etc/sysctl.conf or other sysctl files
local sysctl_conflicts = false
for file in /etc/sysctl.conf /etc/sysctl.d/*.conf; do
if [ [ -f " $file " && " $file " != "/etc/sysctl.d/99-swap.conf" ] ] ; then
if grep -E '^(vm\.swappiness|vm\.vfs_cache_pressure)=' " $file " >/dev/null; then
print_warning " Existing swap settings found in $file . Manual review recommended. "
sysctl_conflicts = true
fi
fi
done
2025-06-26 21:49:51 +01:00
mv " $NEW_SWAP_CONFIG " /etc/sysctl.d/99-swap.conf
chmod 644 /etc/sysctl.d/99-swap.conf
sysctl -p /etc/sysctl.d/99-swap.conf >/dev/null
2025-06-27 13:47:19 +01:00
if [ [ $sysctl_conflicts = = true ] ] ; then
print_warning "Potential conflicting sysctl settings detected. Verify with 'sysctl -a | grep -E \"vm\.swappiness|vm\.vfs_cache_pressure\"'."
else
print_success "Swap settings applied to /etc/sysctl.d/99-swap.conf."
fi
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
print_success "Swap configured successfully."
swapon --show | tee -a " $LOG_FILE "
free -h | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
log "Swap configuration completed."
}
2025-06-26 21:49:51 +01:00
configure_time_sync( ) {
2025-06-26 16:22:23 +01:00
print_section "Time Synchronization"
2025-06-26 21:49:51 +01:00
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 "
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
print_error "Chrony service failed to start."
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 17:12:59 +01:00
log "Time synchronization completed."
2025-06-26 16:22:23 +01:00
}
2025-06-29 13:10:41 +01:00
configure_security_audit( ) {
print_section "Security Audit Configuration"
2025-06-29 18:38:12 +01:00
if ! confirm "Run a security audit with Lynis (and optionally debsecan on Debian)?" ; then
print_info "Security audit skipped."
2025-06-29 13:10:41 +01:00
log "Security audit skipped by user."
2025-06-29 18:38:12 +01:00
AUDIT_RAN = false
2025-06-29 13:10:41 +01:00
return 0
fi
2025-06-29 18:38:12 +01:00
AUDIT_LOG = " /var/log/setup_harden_security_audit_ $( date +%Y%m%d_%H%M%S) .log "
2025-06-29 13:10:41 +01:00
touch " $AUDIT_LOG " && chmod 600 " $AUDIT_LOG "
2025-06-29 18:38:12 +01:00
AUDIT_RAN = true
HARDENING_INDEX = ""
DEBSECAN_VULNS = "Not run"
2025-06-29 13:10:41 +01:00
2025-06-29 18:38:12 +01:00
# Install and run Lynis
2025-06-29 13:10:41 +01:00
print_info "Installing Lynis..."
if ! apt-get update -qq; then
2025-06-29 18:38:12 +01:00
print_error "Failed to update package lists. Cannot install Lynis."
log "apt-get update failed for Lynis installation."
return 1
elif ! apt-get install -y -qq lynis; then
print_warning "Failed to install Lynis. Skipping Lynis audit."
log "Lynis installation failed."
2025-06-29 13:10:41 +01:00
else
2025-06-29 23:34:32 +01:00
print_info "Running Lynis audit (non-interactive mode, this will take a few minutes)..."
2025-06-30 13:26:43 +01:00
print_warning " Review audit results in $AUDIT_LOG for security recommendations. "
2025-06-29 18:38:12 +01:00
if lynis audit system --quick >> " $AUDIT_LOG " 2>& 1; then
2025-06-29 13:10:41 +01:00
print_success " Lynis audit completed. Check $AUDIT_LOG for details. "
2025-06-29 18:38:12 +01:00
log "Lynis audit completed successfully."
# Extract hardening index
HARDENING_INDEX = $( grep -oP "Hardening index : \K\d+" " $AUDIT_LOG " || echo "Unknown" )
# Append Lynis system log for persistence
cat /var/log/lynis.log >> " $AUDIT_LOG " 2>/dev/null
2025-06-29 13:10:41 +01:00
else
print_error " Lynis audit failed. Check $AUDIT_LOG for details. "
log "Lynis audit failed."
fi
fi
2025-06-29 18:38:12 +01:00
# Check if system is Debian before running debsecan
source /etc/os-release
if [ [ " $ID " = = "debian" ] ] ; then
if confirm "Also run debsecan to check for package vulnerabilities?" ; then
print_info "Installing debsecan..."
if ! apt-get install -y -qq debsecan; then
print_warning "Failed to install debsecan. Skipping debsecan audit."
log "debsecan installation failed."
2025-06-29 13:10:41 +01:00
else
2025-06-29 18:38:12 +01:00
print_info "Running debsecan audit..."
if debsecan --suite " $VERSION_CODENAME " >> " $AUDIT_LOG " 2>& 1; then
DEBSECAN_VULNS = $( grep -c "CVE-" " $AUDIT_LOG " || echo "0" )
print_success " debsecan audit completed. Found $DEBSECAN_VULNS vulnerabilities. "
log " debsecan audit completed with $DEBSECAN_VULNS vulnerabilities. "
else
print_error " debsecan audit failed. Check $AUDIT_LOG for details. "
log "debsecan audit failed."
DEBSECAN_VULNS = "Failed"
fi
2025-06-29 13:10:41 +01:00
fi
2025-06-29 18:38:12 +01:00
else
print_info "debsecan audit skipped."
log "debsecan audit skipped by user."
2025-06-29 13:10:41 +01:00
fi
else
2025-06-29 18:38:12 +01:00
print_info "debsecan is not supported on Ubuntu. Skipping debsecan audit."
log "debsecan audit skipped (Ubuntu detected)."
DEBSECAN_VULNS = "Not supported on Ubuntu"
2025-06-29 13:10:41 +01:00
fi
print_warning " Review audit results in $AUDIT_LOG for security recommendations. "
log "Security audit configuration completed."
}
2025-06-26 21:49:51 +01:00
final_cleanup( ) {
print_section "Final System Cleanup"
print_info "Running final system update and cleanup..."
if ! apt-get update -qq; then
2025-06-26 22:19:35 +01:00
print_warning "Failed to update package lists during final cleanup."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
if ! apt-get upgrade -y -qq || ! apt-get --purge autoremove -y -qq || ! apt-get autoclean -y -qq; then
2025-06-26 22:19:35 +01:00
print_warning "Final system cleanup failed on one or more commands."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
systemctl daemon-reload
print_success "Final system update and cleanup complete."
log "Final system cleanup completed."
}
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. "
2025-06-29 20:08:19 +01:00
FAILED_SERVICES += ( " $service " )
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
done
if ufw status | grep -q "Status: active" ; then
print_success "Service ufw is active."
else
print_error "Service ufw is NOT active."
2025-06-29 20:08:19 +01:00
FAILED_SERVICES += ( "ufw" )
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
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."
2025-06-29 20:08:19 +01:00
FAILED_SERVICES += ( "docker" )
2025-06-26 21:49:51 +01:00
fi
2025-06-26 17:12:59 +01:00
fi
2025-06-28 22:13:17 +01:00
local TS_COMMAND = ""
2025-06-28 11:43:10 +01:00
if command -v tailscale >/dev/null 2>& 1; then
2025-06-29 20:08:19 +01:00
if systemctl is-active --quiet tailscaled && tailscale ip >/dev/null 2>& 1; then
local TS_IPS TS_IPV4
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
2025-06-30 13:26:43 +01:00
print_success "Service tailscaled is active and connected."
2025-06-29 20:08:19 +01:00
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 11:43:10 +01:00
else
2025-06-30 13:26:43 +01:00
print_error "Service tailscaled is NOT active"
2025-06-29 20:08:19 +01:00
FAILED_SERVICES += ( "tailscaled" )
2025-06-28 22:13:17 +01:00
TS_COMMAND = $( grep "Tailscale connection failed: tailscale up" " $LOG_FILE " | tail -1 | sed 's/.*Tailscale connection failed: //' )
TS_COMMAND = ${ TS_COMMAND :- " tailscale up --operator= $USERNAME " }
2025-06-28 11:43:10 +01:00
fi
fi
2025-06-29 18:38:12 +01:00
if [ [ " $AUDIT_RAN " = = true ] ] ; then
print_success "Security audit performed."
else
print_info "Security audit not performed."
fi
2025-06-28 20:14:23 +01:00
echo -e " \n ${ GREEN } Server setup and hardening script has finished successfully. ${ NC } \n "
2025-06-26 21:49:51 +01:00
echo -e " ${ YELLOW } Configuration Summary: ${ NC } "
2025-06-28 20:14:23 +01:00
printf " %-16s%s\n" "Admin User:" " $USERNAME "
printf " %-16s%s\n" "Hostname:" " $SERVER_NAME "
printf " %-16s%s\n" "SSH Port:" " $SSH_PORT "
printf " %-16s%s\n" "Server IP:" " $SERVER_IP "
2025-06-28 17:42:48 +01:00
if [ [ -f /root/run_backup.sh ] ] ; then
local CRON_SCHEDULE = $( crontab -u root -l 2>/dev/null | grep -F "/root/run_backup.sh" | awk '{print $1, $2, $3, $4, $5}' || echo "Not configured" )
local NOTIFICATION_STATUS = "None"
local BACKUP_DEST = $( grep "^REMOTE_DEST=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown" )
local BACKUP_PORT = $( grep "^SSH_PORT=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown" )
local REMOTE_BACKUP_PATH = $( grep "^REMOTE_PATH=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown" )
if grep -q "NTFY_URL=" /root/run_backup.sh && ! grep -q 'NTFY_URL=""' /root/run_backup.sh; then
2025-06-28 11:43:10 +01:00
NOTIFICATION_STATUS = "ntfy"
2025-06-28 17:42:48 +01:00
elif grep -q "DISCORD_WEBHOOK=" /root/run_backup.sh && ! grep -q 'DISCORD_WEBHOOK=""' /root/run_backup.sh; then
2025-06-28 11:43:10 +01:00
NOTIFICATION_STATUS = "Discord"
2025-06-28 17:42:48 +01:00
fi
2025-06-28 20:14:23 +01:00
echo -e " Remote Backup: ${ GREEN } Enabled ${ NC } "
printf " %-16s%s\n" "- Backup Script:" "/root/run_backup.sh"
printf " %-16s%s\n" "- Destination:" " $BACKUP_DEST "
printf " %-16s%s\n" "- SSH Port:" " $BACKUP_PORT "
printf " %-16s%s\n" "- Remote Path:" " $REMOTE_BACKUP_PATH "
printf " %-16s%s\n" "- Cron Schedule:" " $CRON_SCHEDULE "
printf " %-16s%s\n" "- Notifications:" " $NOTIFICATION_STATUS "
2025-06-29 18:38:12 +01:00
if [ [ -f " $BACKUP_LOG " ] ] && grep -q "Test backup successful" " $BACKUP_LOG " 2>/dev/null; then
2025-06-29 23:34:32 +01:00
printf " %-16s%s\n" "- Test Status:" " ${ GREEN } Successful ${ NC } "
2025-06-29 18:38:12 +01:00
elif [ [ -f " $BACKUP_LOG " ] ] ; then
printf " %-16s%s\n" "- Test Status:" " Failed (check $BACKUP_LOG ) "
else
printf " %-16s%s\n" "- Test Status:" "Not run"
fi
2025-06-28 11:43:10 +01:00
else
2025-06-28 20:14:23 +01:00
echo -e " Remote Backup: ${ RED } Not configured ${ NC } "
2025-06-28 11:43:10 +01:00
fi
2025-06-28 22:13:17 +01:00
if command -v tailscale >/dev/null 2>& 1; then
2025-06-29 20:08:19 +01:00
local TS_SERVER = $( cat /tmp/tailscale_server 2>/dev/null || echo "https://controlplane.tailscale.com" )
2025-06-30 13:26:43 +01:00
local TS_IPS_RAW = $( cat /tmp/tailscale_ips.txt 2>/dev/null || echo "Not connected" )
# --- FIX: Format IPs to be on a single line ---
local TS_IPS = $( echo " $TS_IPS_RAW " | paste -sd ", " -)
2025-06-29 20:08:19 +01:00
local TS_FLAGS = $( cat /tmp/tailscale_flags 2>/dev/null || echo "None" )
2025-06-28 22:13:17 +01:00
echo -e " Tailscale: ${ GREEN } Enabled ${ NC } "
printf " %-16s%s\n" "- Server:" " $TS_SERVER "
2025-06-29 20:08:19 +01:00
printf " %-16s%s\n" "- Tailscale IPs:" " $TS_IPS "
2025-06-28 22:13:17 +01:00
printf " %-16s%s\n" "- Flags:" " $TS_FLAGS "
else
echo -e " Tailscale: ${ RED } Not configured ${ NC } "
fi
2025-06-29 18:38:12 +01:00
if [ [ " $AUDIT_RAN " = = true ] ] ; then
echo -e " Security Audit: ${ GREEN } Performed ${ NC } "
printf " %-16s%s\n" "- Audit Log:" " $AUDIT_LOG "
printf " %-16s%s\n" "- Hardening Index:" " ${ HARDENING_INDEX :- Unknown } "
printf " %-16s%s\n" "- Vulnerabilities:" " $DEBSECAN_VULNS "
else
echo -e " Security Audit: ${ RED } Not run ${ NC } "
fi
2025-06-26 21:49:51 +01:00
echo
2025-06-28 20:14:23 +01:00
printf " ${ PURPLE } %-16s%s ${ NC } \n " "Log File:" " $LOG_FILE "
printf " ${ PURPLE } %-16s%s ${ NC } \n " "Backups:" " $BACKUP_DIR "
2025-06-26 21:49:51 +01:00
echo
2025-06-28 20:14:23 +01:00
echo -e " ${ YELLOW } Post-Reboot Verification Steps: ${ NC } "
printf " %-20s ${ CYAN } %s ${ NC } \n " "- SSH access:" " ssh -p $SSH_PORT $USERNAME @ $SERVER_IP "
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Firewall rules:" "sudo ufw status verbose"
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Time sync:" "chronyc tracking"
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Fail2Ban status:" "sudo fail2ban-client status sshd"
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Swap status:" "sudo swapon --show && free -h"
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Hostname:" "hostnamectl"
2025-06-26 21:49:51 +01:00
if command -v docker >/dev/null 2>& 1; then
2025-06-28 20:14:23 +01:00
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Docker status:" "docker ps"
2025-06-26 21:49:51 +01:00
fi
if command -v tailscale >/dev/null 2>& 1; then
2025-06-29 20:37:04 +01:00
printf " %-20s ${ CYAN } %s ${ NC } \n " "- Tailscale status:" "tailscale status"
2025-06-26 21:49:51 +01:00
fi
2025-06-28 17:42:48 +01:00
if [ [ -f /root/run_backup.sh ] ] ; then
2025-06-28 20:14:23 +01:00
echo -e " Remote Backup:"
2025-06-30 13:26:43 +01:00
printf " %-18s ${ CYAN } %s ${ NC } \n " "- Verify SSH key:" "sudo cat /root/.ssh/id_ed25519.pub"
2025-06-28 20:14:23 +01:00
printf " %-18s ${ CYAN } %s ${ NC } \n " "- Copy key if needed:" " ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST "
printf " %-18s ${ CYAN } %s ${ NC } \n " "- Test backup:" "sudo /root/run_backup.sh"
2025-06-29 18:38:12 +01:00
printf " %-18s ${ CYAN } %s ${ NC } \n " "- Check logs:" " sudo less $BACKUP_LOG "
fi
if [ [ " $AUDIT_RAN " = = true ] ] ; then
echo -e " Security Audit:"
printf " %-18s ${ CYAN } %s ${ NC } \n " "- Check results:" " sudo less $AUDIT_LOG "
fi
2025-06-29 20:08:19 +01:00
if [ [ ${# FAILED_SERVICES [@] } -gt 0 ] ] ; then
print_warning " ACTION REQUIRED: The following services failed: ${ FAILED_SERVICES [*] } . Verify with 'systemctl status <service>'. "
fi
2025-06-28 22:13:17 +01:00
if [ [ -n " $TS_COMMAND " ] ] ; then
2025-06-29 20:08:19 +01:00
print_warning "ACTION REQUIRED: Tailscale connection failed. Run the following command to connect manually:"
echo -e " ${ CYAN } $TS_COMMAND ${ NC } "
fi
if [ [ -f /root/run_backup.sh && " $KEY_COPY_CHOICE " = = "2" ] ] ; then
print_warning " ACTION REQUIRED: Ensure the root SSH key (/root/.ssh/id_ed25519.pub) is copied to $BACKUP_DEST . "
2025-06-28 22:13:17 +01:00
fi
2025-06-29 20:08:19 +01:00
print_warning "ACTION REQUIRED: If remote backup is enabled, ensure the root SSH key is copied to the destination server."
2025-06-28 11:43:10 +01:00
print_warning "A reboot is required to apply all changes cleanly."
2025-06-26 21:49:51 +01:00
if [ [ $VERBOSE = = true ] ] ; then
if confirm "Reboot now?" "y" ; then
2025-06-28 23:12:54 +01:00
print_info "Rebooting, bye!..."
2025-06-26 21:49:51 +01:00
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'."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
log "Script finished successfully."
}
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
2025-06-26 16:22:23 +01:00
}
main( ) {
2025-06-26 21:49:51 +01:00
trap 'handle_error $LINENO' ERR
touch " $LOG_FILE " && chmod 600 " $LOG_FILE "
log "Starting Debian/Ubuntu hardening script."
2025-06-28 11:43:10 +01:00
2025-06-26 16:22:23 +01:00
print_header
check_dependencies
2025-06-26 21:49:51 +01:00
check_system
2025-06-26 16:22:23 +01:00
collect_config
install_packages
setup_user
configure_system
configure_ssh
configure_firewall
configure_fail2ban
configure_auto_updates
2025-06-26 21:49:51 +01:00
configure_time_sync
2025-06-26 16:22:23 +01:00
install_docker
install_tailscale
2025-06-28 11:43:10 +01:00
setup_backup
2025-06-26 16:22:23 +01:00
configure_swap
2025-06-29 13:10:41 +01:00
configure_security_audit
2025-06-26 21:49:51 +01:00
final_cleanup
generate_summary
2025-06-26 16:22:23 +01:00
}
2025-06-26 21:49:51 +01:00
# Run main function
2025-06-26 20:11:59 +01:00
main " $@ "