2025-06-26 16:22:23 +01:00
#!/bin/bash
# Debian/Ubuntu Server Setup and Hardening Script
# Version: 4.0 | 2025-06-26
# Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS, 24.10 (experimental)
#
# See README.md for full documentation.
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 ) "
LOG_FILE = " /var/log/setup_harden_debian_ubuntu_ $( date +%Y%m%d_%H%M%S) .log "
VERBOSE = true
CONFIG_FILE = ""
BACKUP_DIR = " /root/setup_harden_backup_ $( date +%Y%m%d_%H%M%S) "
IS_CONTAINER = false
SSHD_BACKUP_FILE = ""
LOCAL_KEY_ADDED = false
SSH_SERVICE = ""
ID = ""
SKIPPED_SETTINGS = ( )
PROMPTED_SETTINGS = ( )
# --- PARSE ARGUMENTS ---
while [ [ $# -gt 0 ] ] ; do
case $1 in
--quiet) VERBOSE = false; shift ; ;
--config) CONFIG_FILE = " $2 " ; shift 2 ; ;
*) shift ; ;
esac
done
# --- LOGGING & PRINT FUNCTIONS ---
log( ) {
echo " $( date '+%Y-%m-%d %H:%M:%S' ) - $1 " >> " $LOG_FILE "
}
print_header( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ CYAN } ╔═════════════════════════════════════════════════════════════════╗ ${ NC } "
echo -e " ${ CYAN } ║ ║ ${ NC } "
echo -e " ${ CYAN } ║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║ ${ NC } "
echo -e " ${ CYAN } ║ v4.0 | 2025-06-26 ║ ${ NC } "
echo -e " ${ CYAN } ╚═════════════════════════════════════════════════════════════════╝ ${ NC } "
echo
}
print_section( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " \n ${ BLUE } ▓▓▓ $1 ▓▓▓ ${ NC } " | tee -a " $LOG_FILE "
echo -e " ${ BLUE } $( printf '═%.0s' { 1..65} ) ${ NC } "
}
print_success( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ GREEN } ✓ $1 ${ NC } " | tee -a " $LOG_FILE "
}
print_error( ) {
echo -e " ${ RED } ✗ $1 ${ NC } " | tee -a " $LOG_FILE "
}
print_warning( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ YELLOW } ⚠ $1 ${ NC } " | tee -a " $LOG_FILE "
}
print_info( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ PURPLE } ℹ $1 ${ NC } " | tee -a " $LOG_FILE "
}
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 ] ]
}
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( ) {
local size = " $1 "
[ [ " $size " = ~ ^[ 0-9] +[ MG] $ ] ] && [ [ " ${ size %[MG] } " -ge 1 ] ]
}
validate_ufw_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +( /tcp| /udp) ?$ ] ]
}
validate_url( ) {
local url = " $1 "
[ [ " $url " = ~ ^https?://[ a-zA-Z0-9.-] +( :[ 0-9] +) ?( /.*) ?$ ] ]
}
validate_email( ) {
local email = " $1 "
[ [ " $email " = ~ ^[ a-zA-Z0-9._%+-] +@[ a-zA-Z0-9.-] +\. [ a-zA-Z] { 2,} $ ] ]
}
validate_smtp_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " = ~ ^( 25| 2525| 8025| 587| 80| 465| 8465| 443) $ ] ]
}
validate_ntfy_token( ) {
local token = " $1 "
[ [ " $token " = ~ ^tk_[ a-zA-Z0-9_-] +$ || -z " $token " ] ]
}
convert_to_bytes( ) {
local size = " $1 "
local unit = " ${ size : -1 } "
local value = " ${ size %[MG] } "
if [ [ " $unit " = = "G" ] ] ; then
echo $(( value * 1024 * 1024 * 1024 ))
elif [ [ " $unit " = = "M" ] ] ; then
echo $(( value * 1024 * 1024 ))
else
echo 0
fi
}
# --- CONFIG FILE LOADING ---
load_config( ) {
local config_file = " $1 "
if [ [ ! -f " $config_file " ] ] ; then
print_error " Config file $config_file not found. "
return 1
fi
print_info " Loaded configuration from $config_file "
log " Loaded configuration from $config_file "
source " $config_file "
# Initialize defaults
USERNAME = " ${ USERNAME :- } "
HOSTNAME = " ${ HOSTNAME :- } "
SSH_PORT = " ${ SSH_PORT :- 5595 } "
TIMEZONE = " ${ TIMEZONE :- Etc /UTC } "
SWAP_SIZE = " ${ SWAP_SIZE :- 2G } "
UFW_PORTS = " ${ UFW_PORTS :- } "
AUTO_UPDATES = " ${ AUTO_UPDATES :- no } "
INSTALL_DOCKER = " ${ INSTALL_DOCKER :- no } "
INSTALL_TAILSCALE = " ${ INSTALL_TAILSCALE :- no } "
TAILSCALE_LOGIN_SERVER = " ${ TAILSCALE_LOGIN_SERVER :- } "
TAILSCALE_AUTH_KEY = " ${ TAILSCALE_AUTH_KEY :- } "
TAILSCALE_OPERATOR = " ${ TAILSCALE_OPERATOR :- ${ USERNAME :- } } "
TAILSCALE_ACCEPT_DNS = " ${ TAILSCALE_ACCEPT_DNS :- yes } "
TAILSCALE_ACCEPT_ROUTES = " ${ TAILSCALE_ACCEPT_ROUTES :- yes } "
SMTP_SERVER = " ${ SMTP_SERVER :- } "
SMTP_PORT = " ${ SMTP_PORT :- } "
2025-06-26 16:49:03 +01:00
SMTP_USER = " ${ SMTP_USER :- } "
SMTP_PASS = " ${ SMTP_PASS :- } "
2025-06-26 16:22:23 +01:00
SMTP_FROM = " ${ SMTP_FROM :- } "
SMTP_TO = " ${ SMTP_TO :- } "
NTFY_SERVER = " ${ NTFY_SERVER :- } "
NTFY_TOKEN = " ${ NTFY_TOKEN :- } "
# Validate required fields
local errors = ( )
if [ [ -z " $USERNAME " ] ] ; then
errors += ( "Missing USERNAME" )
elif ! validate_username " $USERNAME " ; then
errors += ( "Invalid USERNAME" )
fi
if [ [ -z " $HOSTNAME " ] ] ; then
errors += ( "Missing HOSTNAME" )
elif ! validate_hostname " $HOSTNAME " ; then
errors += ( "Invalid HOSTNAME" )
fi
if [ [ -z " $SSH_PORT " ] ] ; then
errors += ( "Missing SSH_PORT" )
elif ! validate_port " $SSH_PORT " ; then
errors += ( "Invalid SSH_PORT" )
fi
if [ [ -z " $TIMEZONE " ] ] ; then
errors += ( "Missing TIMEZONE" )
elif ! validate_timezone " $TIMEZONE " ; then
errors += ( "Invalid TIMEZONE" )
fi
if [ [ -n " $SWAP_SIZE " && ! " $SWAP_SIZE " = ~ ^[ 0-9] +[ MG] $ ] ] ; then
errors += ( "Invalid SWAP_SIZE" )
fi
if [ [ -n " $UFW_PORTS " ] ] ; then
for port in ${ UFW_PORTS //,/ } ; do
if ! validate_ufw_port " $port " ; then
errors += ( " Invalid UFW_PORTS format: $port " )
fi
done
fi
if [ [ -n " $AUTO_UPDATES " && ! " $AUTO_UPDATES " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid AUTO_UPDATES (must be yes/no)" )
fi
if [ [ -n " $INSTALL_DOCKER " && ! " $INSTALL_DOCKER " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid INSTALL_DOCKER (must be yes/no)" )
fi
if [ [ -n " $INSTALL_TAILSCALE " && ! " $INSTALL_TAILSCALE " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid INSTALL_TAILSCALE (must be yes/no)" )
fi
if [ [ " $INSTALL_TAILSCALE " = = "yes" && -n " $TAILSCALE_LOGIN_SERVER " ] ] ; then
if ! validate_url " $TAILSCALE_LOGIN_SERVER " ; then
errors += ( "Invalid TAILSCALE_LOGIN_SERVER" )
fi
if [ [ -z " $TAILSCALE_AUTH_KEY " ] ] ; then
2025-06-26 16:49:03 +01:00
errors += ( "Missing TAILSCALE_AUTH_KEY" )
2025-06-26 16:22:23 +01:00
fi
if [ [ -z " $TAILSCALE_OPERATOR " ] ] ; then
errors += ( "Missing TAILSCALE_OPERATOR" )
elif ! validate_username " $TAILSCALE_OPERATOR " ; then
errors += ( "Invalid TAILSCALE_OPERATOR" )
fi
if [ [ -n " $TAILSCALE_ACCEPT_DNS " && ! " $TAILSCALE_ACCEPT_DNS " = ~ ^( yes| no) $ ] ] ; then
2025-06-26 16:49:03 +01:00
errors += ( "Invalid TAILSCALE_ACCEPT_DNS" )
2025-06-26 16:22:23 +01:00
fi
if [ [ -n " $TAILSCALE_ACCEPT_ROUTES " && ! " $TAILSCALE_ACCEPT_ROUTES " = ~ ^( yes| no) $ ] ] ; then
2025-06-26 16:49:03 +01:00
errors += ( "Invalid TAILSCALE_ACCEPT_ROUTES" )
2025-06-26 16:22:23 +01:00
fi
fi
if [ [ -n " $SMTP_SERVER " ] ] ; then
if [ [ ! " $SMTP_SERVER " = ~ ^[ a-zA-Z0-9.-] +$ ] ] ; then
errors += ( "Invalid SMTP_SERVER" )
fi
if [ [ -z " $SMTP_PORT " ] ] ; then
errors += ( "Missing SMTP_PORT" )
elif ! validate_smtp_port " $SMTP_PORT " ; then
errors += ( "Invalid SMTP_PORT" )
fi
2025-06-26 16:49:03 +01:00
if [ [ -z " $SMTP_USER " ] ] ; then
errors += ( "Missing SMTP_USER" )
fi
if [ [ -z " $SMTP_PASS " ] ] ; then
errors += ( "Missing SMTP_PASS" )
fi
2025-06-26 16:22:23 +01:00
if [ [ -z " $SMTP_FROM " ] ] ; then
errors += ( "Missing SMTP_FROM" )
elif ! validate_email " $SMTP_FROM " ; then
errors += ( "Invalid SMTP_FROM" )
fi
if [ [ -z " $SMTP_TO " ] ] ; then
errors += ( "Missing SMTP_TO" )
elif ! validate_email " $SMTP_TO " ; then
errors += ( "Invalid SMTP_TO" )
fi
fi
if [ [ -n " $NTFY_SERVER " ] ] ; then
if ! validate_url " $NTFY_SERVER " ; then
errors += ( "Invalid NTFY_SERVER" )
fi
if [ [ -z " $NTFY_TOKEN " && " $VERBOSE " = = false ] ] ; then
errors += ( "Missing NTFY_TOKEN (skipping ntfy in quiet mode)" )
SKIPPED_SETTINGS += ( "ntfy" )
NTFY_SERVER = ""
NTFY_TOKEN = ""
elif [ [ -n " $NTFY_TOKEN " && ! " $NTFY_TOKEN " = ~ ^tk_[ a-zA-Z0-9_-] +$ ] ] ; then
errors += ( "Invalid NTFY_TOKEN" )
fi
fi
if [ [ ${# errors [@] } -gt 0 ] ] ; then
for error in " ${ errors [@] } " ; do
print_error " $error "
done
if [ [ $VERBOSE = = true ] ] ; then
print_info "Prompting for missing/invalid required settings..."
[ [ -z " $USERNAME " || ! $( validate_username " $USERNAME " ) ] ] && prompt_username
[ [ -z " $HOSTNAME " || ! $( validate_hostname " $HOSTNAME " ) ] ] && prompt_hostname
[ [ -z " $SSH_PORT " || ! $( validate_port " $SSH_PORT " ) ] ] && prompt_ssh_port
[ [ -z " $TIMEZONE " || ! $( validate_timezone " $TIMEZONE " ) ] ] && prompt_timezone
[ [ -n " $SWAP_SIZE " && ! $( validate_swap_size " $SWAP_SIZE " ) ] ] && prompt_swap_size
if [ [ -n " $UFW_PORTS " ] ] ; then
local valid_ports = true
for port in ${ UFW_PORTS //,/ } ; do
if ! validate_ufw_port " $port " ; then
valid_ports = false
break
fi
done
[ [ " $valid_ports " = = false ] ] && prompt_ufw_ports
fi
[ [ -n " $AUTO_UPDATES " && ! " $AUTO_UPDATES " = ~ ^( yes| no) $ ] ] && prompt_auto_updates
[ [ -n " $INSTALL_DOCKER " && ! " $INSTALL_DOCKER " = ~ ^( yes| no) $ ] ] && prompt_install_docker
[ [ -n " $INSTALL_TAILSCALE " && ! " $INSTALL_TAILSCALE " = ~ ^( yes| no) $ ] ] && prompt_install_tailscale
if [ [ " $INSTALL_TAILSCALE " = = "yes" && -n " $TAILSCALE_LOGIN_SERVER " ] ] ; then
[ [ ! $( validate_url " $TAILSCALE_LOGIN_SERVER " ) ] ] && prompt_tailscale_login_server
[ [ -z " $TAILSCALE_AUTH_KEY " ] ] && prompt_tailscale_auth_key
[ [ -z " $TAILSCALE_OPERATOR " || ! $( validate_username " $TAILSCALE_OPERATOR " ) ] ] && prompt_tailscale_operator
[ [ -n " $TAILSCALE_ACCEPT_DNS " && ! " $TAILSCALE_ACCEPT_DNS " = ~ ^( yes| no) $ ] ] && prompt_tailscale_accept_dns
[ [ -n " $TAILSCALE_ACCEPT_ROUTES " && ! " $TAILSCALE_ACCEPT_ROUTES " = ~ ^( yes| no) $ ] ] && prompt_tailscale_accept_routes
fi
if [ [ -n " $SMTP_SERVER " ] ] ; then
[ [ ! " $SMTP_SERVER " = ~ ^[ a-zA-Z0-9.-] +$ ] ] && prompt_smtp_server
[ [ -z " $SMTP_PORT " || ! $( validate_smtp_port " $SMTP_PORT " ) ] ] && prompt_smtp_port
2025-06-26 16:49:03 +01:00
[ [ -z " $SMTP_USER " ] ] && prompt_smtp_user
[ [ -z " $SMTP_PASS " ] ] && prompt_smtp_pass
2025-06-26 16:22:23 +01:00
[ [ -z " $SMTP_FROM " || ! $( validate_email " $SMTP_FROM " ) ] ] && prompt_smtp_from
[ [ -z " $SMTP_TO " || ! $( validate_email " $SMTP_TO " ) ] ] && prompt_smtp_to
fi
if [ [ -n " $NTFY_SERVER " ] ] ; then
[ [ ! $( validate_url " $NTFY_SERVER " ) ] ] && prompt_ntfy_server
[ [ -z " $NTFY_TOKEN " || ! $( validate_ntfy_token " $NTFY_TOKEN " ) ] ] && prompt_ntfy_token
fi
return 0
else
print_error "Invalid or missing configuration in quiet mode. Exiting."
exit 1
fi
fi
return 0
}
2025-06-26 16:49:03 +01:00
# --- USER PROMPT FUNCTIONS ---
2025-06-26 16:22:23 +01:00
prompt_username( ) {
while true; do
read -rp " $( echo -e " ${ CYAN } Enter username for new admin user: ${ NC } " ) " USERNAME
if validate_username " $USERNAME " ; then
if id " $USERNAME " & >/dev/null; then
print_warning " User ' $USERNAME ' already exists. "
if confirm "Use this existing user?" ; then USER_EXISTS = true; break; fi
else
USER_EXISTS = false; break
fi
else
print_error "Invalid username. Use lowercase letters, numbers, hyphens, underscores (max 32 chars)."
fi
done
PROMPTED_SETTINGS += ( "USERNAME" )
}
prompt_hostname( ) {
while true; do
read -rp " $( echo -e " ${ CYAN } Enter server hostname: ${ NC } " ) " HOSTNAME
if validate_hostname " $HOSTNAME " ; then
SERVER_NAME = " $HOSTNAME "
PRETTY_NAME = " ${ PRETTY_NAME :- $HOSTNAME } "
break
else
print_error "Invalid hostname."
fi
done
PROMPTED_SETTINGS += ( "HOSTNAME" )
}
prompt_ssh_port( ) {
while true; do
read -rp " $( echo -e " ${ CYAN } Enter custom SSH port (1024-65535) [5595]: ${ NC } " ) " SSH_PORT
SSH_PORT = ${ SSH_PORT :- 5595 }
if validate_port " $SSH_PORT " ; then break; else print_error "Invalid port number." ; fi
done
PROMPTED_SETTINGS += ( "SSH_PORT" )
}
prompt_timezone( ) {
while true; do
read -rp " $( echo -e " ${ CYAN } Enter desired timezone (e.g., Etc/UTC, America/New_York) [Etc/UTC]: ${ NC } " ) " TIMEZONE
TIMEZONE = ${ TIMEZONE :- Etc /UTC }
if validate_timezone " $TIMEZONE " ; then break; else print_error "Invalid timezone." ; fi
done
PROMPTED_SETTINGS += ( "TIMEZONE" )
}
prompt_swap_size( ) {
while true; do
read -rp " $( echo -e " ${ CYAN } Enter swap file size (e.g., 2G, 512M) [2G]: ${ NC } " ) " SWAP_SIZE
SWAP_SIZE = ${ SWAP_SIZE :- 2G }
if validate_swap_size " $SWAP_SIZE " ; then break; else print_error "Invalid size. Use format like '2G' or '512M'." ; fi
done
PROMPTED_SETTINGS += ( "SWAP_SIZE" )
}
prompt_ufw_ports( ) {
if confirm "Add custom UFW ports (e.g., 80/tcp, 443/tcp)?" ; then
while true; do
read -rp " $( echo -e " ${ CYAN } Enter ports (comma-separated, e.g., 80/tcp,443/tcp): ${ NC } " ) " UFW_PORTS
if [ [ -z " $UFW_PORTS " ] ] ; then
print_info "No custom ports entered. Skipping."
UFW_PORTS = ""
break
fi
local valid = true
for port in ${ UFW_PORTS //,/ } ; do
if ! validate_ufw_port " $port " ; then
print_error " Invalid port format: $port . Use <port>[/tcp|/udp]. "
valid = false
break
fi
done
if [ [ " $valid " = = true ] ] ; then break; fi
done
PROMPTED_SETTINGS += ( "UFW_PORTS" )
else
UFW_PORTS = ""
fi
}
prompt_auto_updates( ) {
AUTO_UPDATES = "no"
confirm "Enable automatic security updates?" && AUTO_UPDATES = "yes"
PROMPTED_SETTINGS += ( "AUTO_UPDATES" )
}
prompt_install_docker( ) {
INSTALL_DOCKER = "no"
confirm "Install Docker Engine?" && INSTALL_DOCKER = "yes"
PROMPTED_SETTINGS += ( "INSTALL_DOCKER" )
}
prompt_install_tailscale( ) {
INSTALL_TAILSCALE = "no"
confirm "Install Tailscale VPN?" && INSTALL_TAILSCALE = "yes"
PROMPTED_SETTINGS += ( "INSTALL_TAILSCALE" )
}
prompt_tailscale_login_server( ) {
read -rp " $( echo -e " ${ CYAN } Enter Tailscale login server (e.g., https://hs.mydomain.com, press Enter to skip): ${ NC } " ) " TAILSCALE_LOGIN_SERVER
if [ [ -n " $TAILSCALE_LOGIN_SERVER " && ! $( validate_url " $TAILSCALE_LOGIN_SERVER " ) ] ] ; then
print_error "Invalid Tailscale login server URL."
TAILSCALE_LOGIN_SERVER = ""
fi
PROMPTED_SETTINGS += ( "TAILSCALE_LOGIN_SERVER" )
}
prompt_tailscale_auth_key( ) {
read -rp " $( echo -e " ${ CYAN } Enter Tailscale auth key: ${ NC } " ) " TAILSCALE_AUTH_KEY
PROMPTED_SETTINGS += ( "TAILSCALE_AUTH_KEY" )
}
prompt_tailscale_operator( ) {
read -rp " $( echo -e " ${ CYAN } Enter Tailscale operator username [ $USERNAME ]: ${ NC } " ) " TAILSCALE_OPERATOR
TAILSCALE_OPERATOR = ${ TAILSCALE_OPERATOR :- $USERNAME }
if [ [ -n " $TAILSCALE_OPERATOR " && ! $( validate_username " $TAILSCALE_OPERATOR " ) ] ] ; then
print_error "Invalid Tailscale operator username."
TAILSCALE_OPERATOR = " $USERNAME "
fi
PROMPTED_SETTINGS += ( "TAILSCALE_OPERATOR" )
}
prompt_tailscale_accept_dns( ) {
TAILSCALE_ACCEPT_DNS = "yes"
confirm "Accept Tailscale DNS?" "y" || TAILSCALE_ACCEPT_DNS = "no"
PROMPTED_SETTINGS += ( "TAILSCALE_ACCEPT_DNS" )
}
prompt_tailscale_accept_routes( ) {
TAILSCALE_ACCEPT_ROUTES = "yes"
confirm "Accept Tailscale routes?" "y" || TAILSCALE_ACCEPT_ROUTES = "no"
PROMPTED_SETTINGS += ( "TAILSCALE_ACCEPT_ROUTES" )
}
prompt_smtp_server( ) {
read -rp " $( echo -e " ${ CYAN } Enter SMTP server [mail.smtp2go.com]: ${ NC } " ) " SMTP_SERVER
SMTP_SERVER = ${ SMTP_SERVER :- mail .smtp2go.com }
if [ [ ! " $SMTP_SERVER " = ~ ^[ a-zA-Z0-9.-] +$ ] ] ; then
print_error "Invalid SMTP server."
SMTP_SERVER = ""
fi
PROMPTED_SETTINGS += ( "SMTP_SERVER" )
}
prompt_smtp_port( ) {
read -rp " $( echo -e " ${ CYAN } Enter SMTP port [587]: ${ NC } " ) " SMTP_PORT
SMTP_PORT = ${ SMTP_PORT :- 587 }
if ! validate_smtp_port " $SMTP_PORT " ; then
print_error "Invalid SMTP port (use 25,2525,8025,587,80,465,8465,443)."
SMTP_PORT = ""
fi
PROMPTED_SETTINGS += ( "SMTP_PORT" )
}
2025-06-26 16:49:03 +01:00
prompt_smtp_user( ) {
read -rp " $( echo -e " ${ CYAN } Enter SMTP username: ${ NC } " ) " SMTP_USER
if [ [ -z " $SMTP_USER " ] ] ; then
print_error "SMTP username cannot be empty."
SMTP_USER = ""
fi
PROMPTED_SETTINGS += ( "SMTP_USER" )
}
prompt_smtp_pass( ) {
read -sp " $( echo -e " ${ CYAN } Enter SMTP password: ${ NC } " ) " SMTP_PASS
echo
if [ [ -z " $SMTP_PASS " ] ] ; then
print_error "SMTP password cannot be empty."
SMTP_PASS = ""
fi
PROMPTED_SETTINGS += ( "SMTP_PASS" )
}
2025-06-26 16:22:23 +01:00
prompt_smtp_from( ) {
read -rp " $( echo -e " ${ CYAN } Enter SMTP from email address: ${ NC } " ) " SMTP_FROM
if [ [ -z " $SMTP_FROM " || ! $( validate_email " $SMTP_FROM " ) ] ] ; then
print_error "Invalid SMTP from email."
SMTP_FROM = ""
fi
PROMPTED_SETTINGS += ( "SMTP_FROM" )
}
prompt_smtp_to( ) {
read -rp " $( echo -e " ${ CYAN } Enter SMTP to email address: ${ NC } " ) " SMTP_TO
if [ [ -z " $SMTP_TO " || ! $( validate_email " $SMTP_TO " ) ] ] ; then
print_error "Invalid SMTP to email."
SMTP_TO = ""
fi
PROMPTED_SETTINGS += ( "SMTP_TO" )
}
prompt_ntfy_server( ) {
read -rp " $( echo -e " ${ CYAN } Enter ntfy server URL (e.g., https://ntfy.mydomain.com/ovps, press Enter to skip): ${ NC } " ) " NTFY_SERVER
if [ [ -n " $NTFY_SERVER " && ! $( validate_url " $NTFY_SERVER " ) ] ] ; then
print_error "Invalid ntfy server URL."
NTFY_SERVER = ""
fi
PROMPTED_SETTINGS += ( "NTFY_SERVER" )
}
prompt_ntfy_token( ) {
read -rp " $( echo -e " ${ CYAN } Enter ntfy token: ${ NC } " ) " NTFY_TOKEN
if [ [ -n " $NTFY_TOKEN " && ! $( validate_ntfy_token " $NTFY_TOKEN " ) ] ] ; then
print_error "Invalid ntfy token (must start with tk_)."
NTFY_TOKEN = ""
fi
PROMPTED_SETTINGS += ( "NTFY_TOKEN" )
}
# --- CORE FUNCTIONS ---
check_dependencies( ) {
print_section "Checking Dependencies"
local missing_deps = ( )
command -v curl >/dev/null || missing_deps += ( "curl" )
command -v sudo >/dev/null || missing_deps += ( "sudo" )
command -v gpg >/dev/null || missing_deps += ( "gpg" )
command -v postmap >/dev/null || missing_deps += ( "postfix" )
if [ [ ${# missing_deps [@] } -gt 0 ] ] ; then
print_info " Installing missing dependencies: ${ missing_deps [*] } "
if ! apt-get update -qq || ! apt-get install -y -qq " ${ missing_deps [@] } " ; then
print_error " Failed to install dependencies: ${ missing_deps [*] } "
exit 1
fi
print_success "Dependencies installed."
else
print_success "All essential dependencies are installed."
fi
log "Dependency check completed."
}
check_system( ) {
print_section "System Compatibility Check"
if [ [ $( id -u) -ne 0 ] ] ; then
print_error "This script must be run as root (e.g., sudo ./setup_harden_debian_ubuntu.sh)."
exit 1
fi
print_success "Running with root privileges."
if [ [ -f /proc/1/cgroup ] ] && grep -qE '(docker|lxc|kubepod)' /proc/1/cgroup; then
IS_CONTAINER = true
print_warning "Container environment detected. Some features (like swap) will be skipped."
fi
if [ [ -f /etc/os-release ] ] ; then
source /etc/os-release
ID = " $ID "
if [ [ $ID = = "debian" && $VERSION_ID = = "12" ] ] || \
[ [ $ID = = "ubuntu" && $VERSION_ID = ~ ^( 20.04| 22.04| 24.04| 24.10) $ ] ] ; then
print_success " Compatible OS detected: $PRETTY_NAME "
else
print_warning " Script not tested on $PRETTY_NAME . This is for Debian 12 or Ubuntu 20.04/22.04/24.04 LTS. "
if ! confirm "Continue anyway?" ; then exit 1; fi
fi
else
print_error "This does not appear to be a Debian or Ubuntu system."
exit 1
fi
if ! dpkg -l openssh-server | grep -q ^ii; then
print_warning "openssh-server not installed. It will be installed in the next step."
else
if systemctl is-enabled ssh.service >/dev/null 2>& 1 || systemctl is-active ssh.service >/dev/null 2>& 1; then
print_info "Preliminary check: ssh.service detected."
elif systemctl is-enabled sshd.service >/dev/null 2>& 1 || systemctl is-active sshd.service >/dev/null 2>& 1; then
print_info "Preliminary check: sshd.service detected."
elif ps aux | grep -q "[s]shd" ; then
print_warning "Preliminary check: SSH daemon running but no standard service detected."
else
print_warning "No SSH service or daemon detected. Ensure SSH is working after package installation."
fi
fi
if curl -s --head https://deb.debian.org >/dev/null || curl -s --head https://archive.ubuntu.com >/dev/null; then
print_success "Internet connectivity confirmed."
else
print_error "No internet connectivity. Please check your network."
exit 1
fi
if [ [ ! -w /var/log ] ] ; then
print_error "Failed to write to /var/log. Cannot create log file."
exit 1
fi
SHADOW_PERMS = $( stat -c %a /etc/shadow)
if [ [ " $SHADOW_PERMS " != "640" ] ] ; then
print_info "Fixing /etc/shadow permissions to 640..."
chmod 640 /etc/shadow
chown root:shadow /etc/shadow
log "Fixed /etc/shadow permissions to 640."
fi
log "System compatibility check completed."
}
collect_config( ) {
print_section "Configuration Setup"
if [ [ -n " $CONFIG_FILE " || -f "/etc/setup_harden.conf" ] ] ; then
CONFIG_FILE = ${ CONFIG_FILE :- /etc/setup_harden.conf }
if ! load_config " $CONFIG_FILE " ; then
if [ [ $VERBOSE = = false ] ] ; then
print_error "Configuration file invalid in quiet mode. Exiting."
exit 1
else
print_info "Falling back to interactive mode due to config issues."
full_interactive_config
fi
fi
else
full_interactive_config
fi
SERVER_IP = $( curl -s https://ifconfig.me 2>/dev/null || echo "unknown" )
print_info " Detected server IP: $SERVER_IP "
echo -e " \n ${ YELLOW } Configuration Summary: ${ NC } "
echo -e " Username: $USERNAME "
echo -e " Hostname: $SERVER_NAME "
echo -e " SSH Port: $SSH_PORT "
echo -e " Server IP: $SERVER_IP "
if [ [ ${# PROMPTED_SETTINGS [@] } -gt 0 ] ] ; then
echo -e " Prompted: ${ PROMPTED_SETTINGS [*] } "
fi
if [ [ ${# SKIPPED_SETTINGS [@] } -gt 0 ] ] ; then
echo -e " Skipped: ${ SKIPPED_SETTINGS [*] } "
fi
if [ [ " $VERBOSE " = = true ] ] ; then
if ! confirm "\nContinue with this configuration?" "y" ; then print_info "Exiting." ; exit 0; fi
fi
log " Configuration collected: USER= $USERNAME , HOST= $SERVER_NAME , PORT= $SSH_PORT "
}
full_interactive_config( ) {
prompt_username
prompt_hostname
read -rp " $( echo -e " ${ CYAN } Enter a 'pretty' hostname (optional): ${ NC } " ) " PRETTY_NAME
[ [ -z " $PRETTY_NAME " ] ] && PRETTY_NAME = " $SERVER_NAME "
prompt_ssh_port
prompt_timezone
prompt_auto_updates
prompt_install_docker
prompt_install_tailscale
if [ [ " $INSTALL_TAILSCALE " = = "yes" ] ] ; then
prompt_tailscale_login_server
if [ [ -n " $TAILSCALE_LOGIN_SERVER " ] ] ; then
prompt_tailscale_auth_key
prompt_tailscale_operator
prompt_tailscale_accept_dns
prompt_tailscale_accept_routes
fi
fi
if confirm "Configure system monitoring with SMTP and/or ntfy?" ; then
prompt_smtp_server
if [ [ -n " $SMTP_SERVER " ] ] ; then
prompt_smtp_port
2025-06-26 16:49:03 +01:00
prompt_smtp_user
prompt_smtp_pass
2025-06-26 16:22:23 +01:00
prompt_smtp_from
prompt_smtp_to
fi
prompt_ntfy_server
if [ [ -n " $NTFY_SERVER " ] ] ; then
prompt_ntfy_token
fi
fi
}
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 \
rsync wget vim htop iotop nethogs ncdu tree \
rsyslog cron jq gawk coreutils perl skopeo git \
openssh-client openssh-server postfix libsasl2-modules curl; then
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"
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
print_info " Set a password for ' $USERNAME ' (required, or press Enter twice to skip for key-only access): "
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
if echo " $USERNAME : $PASS1 " | chpasswd 2>& 1 | tee -a " $LOG_FILE " ; then
print_success " Password for ' $USERNAME ' updated. "
break
else
print_error "Failed to set password. Check log file for details."
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 "
if confirm "Add an SSH public key from your local machine now?" ; then
while true; do
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 "
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
break
else
print_error "Invalid SSH key format. It should start with 'ssh-rsa', 'ecdsa-*', or 'ssh-ed25519'."
if ! confirm "Try again?" ; then print_info "Skipping SSH key addition." ; break; fi
fi
done
fi
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 "
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
print_info "Configuring timezone..."
if [ [ $( timedatectl status | grep "Time zone" | awk '{print $3}' ) != " $TIMEZONE " ] ] ; then
timedatectl set-timezone " $TIMEZONE "
print_success " Timezone set to $TIMEZONE . "
log " Timezone set to $TIMEZONE . "
else
print_info " Timezone already set to $TIMEZONE . "
fi
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"
if ! dpkg -l openssh-server | grep -q ^ii; then
print_error "openssh-server package is not installed. Please ensure it is installed."
exit 1
fi
if [ [ $ID = = "ubuntu" ] ] && { systemctl is-enabled ssh.service >/dev/null 2>& 1 || systemctl is-active ssh.service >/dev/null 2>& 1; } ; then
SSH_SERVICE = "ssh.service"
elif systemctl is-enabled sshd.service >/dev/null 2>& 1 || systemctl is-active sshd.service >/dev/null 2>& 1; then
SSH_SERVICE = "sshd.service"
elif ps aux | grep -q "[s]shd" ; then
print_warning "SSH daemon running but no standard service detected. Checking for socket activation..."
if systemctl is-active ssh.socket >/dev/null 2>& 1; then
print_info "Disabling ssh.socket to enable ssh.service..."
systemctl disable --now ssh.socket
fi
SSH_SERVICE = "ssh.service"
if ! systemctl enable --now " $SSH_SERVICE " >/dev/null 2>& 1; then
print_error " Failed to enable and start $SSH_SERVICE . Attempting manual start... "
if ! /usr/sbin/sshd; then
print_error "Failed to start SSH daemon manually. Please check openssh-server installation."
exit 1
fi
print_success "SSH daemon started manually."
fi
else
print_error "No SSH service or daemon detected. Please verify openssh-server installation."
exit 1
fi
print_info " Using SSH service: $SSH_SERVICE "
log " Detected SSH service: $SSH_SERVICE "
systemctl status " $SSH_SERVICE " --no-pager >> " $LOG_FILE " 2>& 1
ps aux | grep "[s]shd" >> " $LOG_FILE " 2>& 1
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 "
if [ [ $LOCAL_KEY_ADDED = = false ] ] && [ [ ! -s " $AUTH_KEYS " ] ] ; then
print_info "No local key provided and no existing keys found. Generating new SSH key..."
mkdir -p " $SSH_DIR "
chmod 700 " $SSH_DIR "
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 } "
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) "
cp /etc/ssh/sshd_config " $SSHD_BACKUP_FILE "
NEW_SSH_CONFIG = $( mktemp)
tee " $NEW_SSH_CONFIG " > /dev/null <<EOF
Port $SSH_PORT
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 16:49:03 +01:00
mv " $NEW_SSH_CONFIG " /etc/ssh/sshd_config.d/99thor-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'
******************************************************************************
AUTHORIZED ACCESS ONLY
═════ all attempts are logged and reviewed ═════
******************************************************************************
EOF
fi
print_info "Testing and restarting SSH service..."
if sshd -t; then
if ! systemctl restart " $SSH_SERVICE " ; then
print_error "SSH service failed to restart! Reverting changes..."
cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config
systemctl restart " $SSH_SERVICE " || /usr/sbin/sshd || true
exit 1
fi
if systemctl is-active --quiet " $SSH_SERVICE " ; then
print_success " SSH service restarted on port $SSH_PORT . "
else
print_error "SSH service failed to start! Reverting changes..."
cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config
systemctl restart " $SSH_SERVICE " || /usr/sbin/sshd || true
exit 1
fi
else
print_error "SSH config test failed! Reverting changes..."
cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config
systemctl restart " $SSH_SERVICE " || /usr/sbin/sshd || true
rm -f " $NEW_SSH_CONFIG "
exit 1
fi
print_info "Verifying root SSH login is disabled..."
if grep -q "^PermitRootLogin no" /etc/ssh/sshd_config.d/99-hardening.conf; then
print_success "Root SSH login is disabled."
else
print_error "Failed to disable root SSH login. Please check /etc/ssh/sshd_config.d/99-hardening.conf."
exit 1
fi
if ssh -p " $SSH_PORT " -o BatchMode = yes -o ConnectTimeout = 5 root@localhost true 2>/dev/null; then
print_error "Root SSH login is still possible! Please check SSH configuration."
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 "
if ! confirm "Was the new SSH connection successful?" ; then
print_error "Aborting. Restoring original SSH configuration."
cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config
systemctl restart " $SSH_SERVICE " || /usr/sbin/sshd || true
exit 1
fi
log "SSH hardening completed."
}
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
if [ [ -n " ${ UFW_PORTS :- } " ] ] ; then
for port in ${ UFW_PORTS //,/ } ; do
if ufw status | grep -qw " $port " ; then
print_info " Rule for $port already exists. "
else
ufw allow " $port " comment " Custom port $port "
print_success " Added rule for $port . "
log " Added UFW rule for $port . "
fi
done
elif [ [ $VERBOSE = = true ] ] ; then
if confirm "Allow HTTP traffic (port 80)?" ; then
if ! ufw status | grep -qw "80/tcp" ; then
ufw allow http comment 'HTTP'
print_success "HTTP traffic allowed."
else
print_info "HTTP rule already exists."
fi
fi
if confirm "Allow HTTPS traffic (port 443)?" ; then
if ! ufw status | grep -qw "443/tcp" ; then
ufw allow https comment 'HTTPS'
print_success "HTTPS traffic allowed."
else
print_info "HTTPS rule already exists."
fi
fi
if confirm "Add additional custom ports (e.g., 8080/tcp, 123/udp)?" ; then
while true; do
read -rp " $( echo -e " ${ CYAN } Enter ports (space-separated, e.g., 8080/tcp 123/udp): ${ NC } " ) " CUSTOM_PORTS
if [ [ -z " $CUSTOM_PORTS " ] ] ; then
print_info "No custom ports entered. Skipping."
break
fi
valid = true
for port in $CUSTOM_PORTS ; do
if ! validate_ufw_port " $port " ; then
print_error " Invalid port format: $port . Use <port>[/tcp|/udp]. "
valid = false
break
fi
done
if [ [ " $valid " = = true ] ] ; then
for port in $CUSTOM_PORTS ; do
if ufw status | grep -qw " $port " ; then
print_info " Rule for $port already exists. "
else
ufw allow " $port " comment " Custom port $port "
print_success " Added rule for $port . "
log " Added UFW rule for $port . "
fi
done
break
else
print_info "Please try again."
fi
done
fi
fi
if [ [ " $INSTALL_TAILSCALE " = = "yes" ] ] ; then
if ! ufw status | grep -qw "41641/udp" ; then
ufw allow 41641/udp comment 'Tailscale'
print_success "Tailscale port 41641/udp allowed."
else
print_info "Tailscale port 41641/udp rule already exists."
fi
fi
if [ [ -n " ${ SMTP_PORT :- } " ] ] ; then
if ! ufw status | grep -qw " $SMTP_PORT /tcp " ; then
ufw allow " $SMTP_PORT " /tcp comment 'SMTP'
print_success " SMTP port $SMTP_PORT /tcp allowed. "
else
print_info " SMTP port $SMTP_PORT /tcp rule already exists. "
fi
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
print_warning " ACTION REQUIRED: Check your VPS provider's edge firewall to allow opened ports (e.g., $SSH_PORT /tcp). "
print_info " - DigitalOcean: Configure Firewall in Control Panel -> Networking -> Firewalls."
print_info " - AWS: Update Security Groups in EC2 Dashboard."
print_info " - GCP: Update Firewall Rules in VPC Network -> Firewall."
print_info " - Oracle: Configure Security Lists in Virtual Cloud Network."
ufw status verbose | tee -a " $LOG_FILE "
iptables -L >> " $LOG_FILE " 2>& 1
log "Firewall configuration completed."
}
2025-06-26 16:49:03 +01:00
configure_fail2ban( ) {
print_section "Fail2Ban Configuration"
if ! dpkg -l fail2ban | grep -q ^ii; then
print_error "fail2ban package is not installed."
exit 1
fi
print_info "Configuring Fail2Ban..."
local jail_config = "/etc/fail2ban/jail.d/custom.conf"
mkdir -p /etc/fail2ban/jail.d
cp /etc/fail2ban/jail.conf " $BACKUP_DIR /jail.conf.backup " 2>/dev/null || true
NEW_JAIL_CONFIG = $( mktemp)
tee " $NEW_JAIL_CONFIG " > /dev/null <<EOF
[ sshd]
enabled = true
port = $SSH_PORT
maxretry = 3
findtime = 600
bantime = 3600
EOF
if [ [ -n " ${ UFW_PORTS :- } " ] ] ; then
for port in ${ UFW_PORTS //,/ } ; do
if [ [ " $port " = ~ ^[ 0-9] +/tcp$ ] ] ; then
port_num = " ${ port %/tcp } "
echo -e " [custom- $port_num ]\nenabled = true\nport = $port_num \nmaxretry = 5\nfindtime = 600\nbantime = 3600 " >> " $NEW_JAIL_CONFIG "
fi
done
fi
if [ [ -f " $jail_config " ] ] && cmp -s " $jail_config " " $NEW_JAIL_CONFIG " ; then
print_info "Fail2Ban configuration already correct. Skipping."
else
mv " $NEW_JAIL_CONFIG " " $jail_config "
chmod 644 " $jail_config "
systemctl restart fail2ban
if systemctl is-active --quiet fail2ban; then
print_success "Fail2Ban configured and running."
fail2ban-client status sshd | tee -a " $LOG_FILE "
else
print_error "Failed to start Fail2Ban service. Check 'journalctl -u fail2ban'."
exit 1
fi
fi
rm -f " $NEW_JAIL_CONFIG "
log "Fail2Ban configuration completed."
}
configure_auto_updates( ) {
print_section "Automatic Updates Configuration"
if [ [ " $AUTO_UPDATES " != "yes" ] ] ; then
print_info "Skipping automatic updates configuration."
SKIPPED_SETTINGS += ( "automatic updates" )
return 0
fi
if ! dpkg -l unattended-upgrades | grep -q ^ii; then
print_error "unattended-upgrades package is not installed."
exit 1
fi
print_info "Configuring unattended-upgrades..."
local config_file = "/etc/apt/apt.conf.d/50unattended-upgrades"
cp " $config_file " " $BACKUP_DIR /50unattended-upgrades.backup " 2>/dev/null || true
if ! grep -q "Unattended-Upgrade::Automatic-Reboot" " $config_file " ; then
echo 'Unattended-Upgrade::Automatic-Reboot "true";' >> " $config_file "
echo 'Unattended-Upgrade::Automatic-Reboot-Time "02:00";' >> " $config_file "
fi
if ! grep -q "Unattended-Upgrade::Mail" " $config_file " ; then
if [ [ -n " ${ SMTP_TO :- } " ] ] ; then
echo " Unattended-Upgrade::Mail \" $SMTP_TO \"; " >> " $config_file "
echo 'Unattended-Upgrade::MailReport "on-change";' >> " $config_file "
fi
fi
systemctl restart unattended-upgrades
if systemctl is-active --quiet unattended-upgrades; then
print_success "Automatic updates configured."
else
print_error "Failed to start unattended-upgrades service."
exit 1
fi
log "Automatic updates configuration completed."
}
configure_monitoring( ) {
print_section "System Monitoring Configuration"
if [ [ -n " ${ SMTP_SERVER :- } " || -n " ${ NTFY_SERVER :- } " ] ] ; then
if [ [ -n " ${ SMTP_SERVER :- } " && -n " ${ SMTP_PORT :- } " && -n " ${ SMTP_USER :- } " && -n " ${ SMTP_PASS :- } " && -n " ${ SMTP_FROM :- } " && -n " ${ SMTP_TO :- } " ] ] ; then
print_info "Configuring Postfix for SMTP2go..."
NEW_POSTFIX_CONFIG = $( mktemp)
tee " $NEW_POSTFIX_CONFIG " > /dev/null <<EOF
relayhost = [ $SMTP_SERVER ] :$SMTP_PORT
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
EOF
if [ [ -f /etc/postfix/main.cf ] ] && grep -q " relayhost = \[ $SMTP_SERVER \]: $SMTP_PORT " /etc/postfix/main.cf; then
print_info "Postfix configuration already correct. Skipping."
else
cat " $NEW_POSTFIX_CONFIG " >> /etc/postfix/main.cf
print_info "Creating Postfix SASL password file..."
if [ [ $VERBOSE = = true && ( -z " $SMTP_USER " || -z " $SMTP_PASS " ) ] ] ; then
[ [ -z " $SMTP_USER " ] ] && prompt_smtp_user
[ [ -z " $SMTP_PASS " ] ] && prompt_smtp_pass
fi
if [ [ -z " $SMTP_USER " || -z " $SMTP_PASS " ] ] ; then
print_error "SMTP credentials missing. Skipping SMTP configuration."
SKIPPED_SETTINGS += ( "SMTP monitoring" )
SMTP_SERVER = ""
rm -f " $NEW_POSTFIX_CONFIG "
return
fi
echo " [ $SMTP_SERVER ]: $SMTP_PORT $SMTP_USER : $SMTP_PASS " > /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
systemctl restart postfix
print_info "Testing SMTP configuration..."
if echo " Test email from $( hostname) " | mail -s "Test Alert" " $SMTP_TO " 2>& 1 | tee -a " $LOG_FILE " ; then
print_success " SMTP test email sent to $SMTP_TO . "
else
print_error "Failed to send test email. Check /var/log/mail.log."
fi
fi
rm -f " $NEW_POSTFIX_CONFIG "
else
print_info "Incomplete SMTP configuration. Skipping SMTP."
SKIPPED_SETTINGS += ( "SMTP monitoring" )
SMTP_SERVER = ""
fi
if [ [ -n " ${ NTFY_SERVER :- } " && -n " ${ NTFY_TOKEN :- } " ] ] ; then
print_info "Configuring ntfy notifications..."
curl -s -o /dev/null -H " Authorization: Bearer $NTFY_TOKEN " " $NTFY_SERVER " || {
print_error "Failed to connect to ntfy server. Check NTFY_SERVER and NTFY_TOKEN."
SKIPPED_SETTINGS += ( "ntfy monitoring" )
NTFY_SERVER = ""
NTFY_TOKEN = ""
}
elif [ [ -n " ${ NTFY_SERVER :- } " && -z " ${ NTFY_TOKEN :- } " && $VERBOSE = = true ] ] ; then
prompt_ntfy_token
if [ [ -n " ${ NTFY_TOKEN :- } " ] ] ; then
print_info "Configuring ntfy notifications..."
curl -s -o /dev/null -H " Authorization: Bearer $NTFY_TOKEN " " $NTFY_SERVER " || {
print_error "Failed to connect to ntfy server. Check NTFY_SERVER and NTFY_TOKEN."
SKIPPED_SETTINGS += ( "ntfy monitoring" )
NTFY_SERVER = ""
NTFY_TOKEN = ""
}
else
print_info "No ntfy token provided. Skipping ntfy."
SKIPPED_SETTINGS += ( "ntfy monitoring" )
NTFY_SERVER = ""
fi
else
print_info "Incomplete ntfy configuration. Skipping ntfy."
SKIPPED_SETTINGS += ( "ntfy monitoring" )
NTFY_SERVER = ""
fi
print_info "Setting up disk space monitoring and rsync backup cron jobs..."
local cron_file = "/etc/cron.d/setup_harden_monitoring"
NEW_CRON_CONFIG = $( mktemp)
tee " $NEW_CRON_CONFIG " > /dev/null <<'EOF'
# Disk space monitoring (alert at 90% usage)
*/15 * * * * root df -h / | awk 'NR==2 {if ($5+0 >= 90) {print "Disk usage critical on ' " $( hostname) " ': " $0}}' | while read -r line; do [ [ -n " ${ SMTP_TO :- } " ] ] && echo " $line " | mail -s "Disk Alert" " ${ SMTP_TO } " ; [ [ -n " ${ NTFY_SERVER :- } " && -n " ${ NTFY_TOKEN :- } " ] ] && curl -s -H " Authorization: Bearer ${ NTFY_TOKEN } " -d " $line " " ${ NTFY_SERVER } " ; done
# Rsync backup to /root/backup (daily at 1am)
0 1 * * * root rsync -a --delete --exclude '/root/backup' / /root/backup && { [ [ -n " ${ SMTP_TO :- } " ] ] && echo " Rsync backup completed on $( hostname) " | mail -s "Backup Complete" " ${ SMTP_TO } " ; [ [ -n " ${ NTFY_SERVER :- } " && -n " ${ NTFY_TOKEN :- } " ] ] && curl -s -H " Authorization: Bearer ${ NTFY_TOKEN } " -d " Rsync backup completed on $( hostname) " " ${ NTFY_SERVER } " ; }
EOF
if [ [ -f " $cron_file " ] ] && cmp -s " $cron_file " " $NEW_CRON_CONFIG " ; then
print_info "Cron jobs already configured. Skipping."
else
mv " $NEW_CRON_CONFIG " " $cron_file "
chmod 644 " $cron_file "
print_success "Disk space and backup cron jobs configured."
fi
rm -f " $NEW_CRON_CONFIG "
else
print_info "Skipping system monitoring configuration (no SMTP or ntfy settings provided)."
SKIPPED_SETTINGS += ( "system monitoring" )
fi
log "System monitoring configuration completed."
}
install_docker( ) {
if [ [ " $INSTALL_DOCKER " != "yes" ] ] ; then
print_info "Skipping Docker installation."
SKIPPED_SETTINGS += ( "Docker installation" )
return 0
fi
print_section "Docker Installation"
if command -v docker >/dev/null 2>& 1; then
print_info "Docker already installed."
if ! groups " $USERNAME " | grep -qw docker; then
print_info " Adding ' $USERNAME ' to docker group... "
usermod -aG docker " $USERNAME "
print_success " User ' $USERNAME ' added to docker group. "
else
print_info " User ' $USERNAME ' already in docker group. "
fi
return 0
fi
print_info "Installing Docker..."
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
chmod +x /tmp/get-docker.sh
if ! grep -q "docker" /tmp/get-docker.sh; then
print_error "Downloaded Docker install script appears invalid."
rm -f /tmp/get-docker.sh
exit 1
fi
if ! sh /tmp/get-docker.sh; then
print_error "Failed to install Docker."
rm -f /tmp/get-docker.sh
exit 1
fi
rm -f /tmp/get-docker.sh
systemctl enable docker
systemctl start docker
print_info " Adding ' $USERNAME ' to docker group... "
usermod -aG docker " $USERNAME "
if groups " $USERNAME " | grep -qw docker; then
print_success " User ' $USERNAME ' added to docker group. "
else
print_error " Failed to add ' $USERNAME ' to docker group. "
exit 1
fi
if ! docker ps >/dev/null 2>& 1; then
print_error "Docker service is not running or accessible. Check 'systemctl status docker'."
exit 1
fi
print_success "Docker installation completed."
log "Docker installation and configuration completed."
}
2025-06-26 16:22:23 +01:00
install_tailscale( ) {
if [ [ " $INSTALL_TAILSCALE " != "yes" ] ] ; then
print_info "Skipping Tailscale installation."
SKIPPED_SETTINGS += ( "Tailscale installation" )
return 0
fi
print_section "Tailscale VPN Installation"
if command -v tailscale >/dev/null 2>& 1; then
print_info "Tailscale already installed."
if [ [ -n " ${ TAILSCALE_LOGIN_SERVER :- } " && -n " ${ TAILSCALE_AUTH_KEY :- } " && -n " ${ TAILSCALE_OPERATOR :- } " ] ] ; then
print_info "Configuring Tailscale with Headscale..."
local ts_cmd = " tailscale up --login-server= $TAILSCALE_LOGIN_SERVER --operator= $TAILSCALE_OPERATOR "
[ [ " $TAILSCALE_ACCEPT_DNS " = = "yes" ] ] && ts_cmd = " $ts_cmd --accept-dns "
[ [ " $TAILSCALE_ACCEPT_ROUTES " = = "yes" ] ] && ts_cmd = " $ts_cmd --accept-routes "
[ [ -n " ${ TAILSCALE_AUTH_KEY :- } " ] ] && ts_cmd = " $ts_cmd --authkey= $TAILSCALE_AUTH_KEY "
ts_cmd = " $ts_cmd --hostname= $SERVER_NAME "
if eval " $ts_cmd " 2>& 1 | tee -a " $LOG_FILE " ; then
2025-06-26 16:49:03 +01:00
if tailscale status | grep -q " $SERVER_NAME " ; then
print_success "Tailscale configured and connected."
tailscale status | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 16:49:03 +01:00
print_error "Tailscale failed to connect. Check 'tailscale status' and log file."
exit 1
2025-06-26 16:22:23 +01:00
fi
else
2025-06-26 16:49:03 +01:00
print_error "Failed to configure Tailscale. Check log file."
exit 1
2025-06-26 16:22:23 +01:00
fi
else
2025-06-26 16:49:03 +01:00
print_info "Tailscale configuration incomplete. Skipping configuration."
SKIPPED_SETTINGS += ( "Tailscale configuration" )
2025-06-26 16:22:23 +01:00
fi
return 0
fi
print_info "Installing Tailscale..."
2025-06-26 16:49:03 +01:00
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | gpg --dearmor -o /usr/share/keyrings/tailscale-archive-keyring.gpg
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list -o /etc/apt/sources.list.d/tailscale.list
if ! apt-get update -qq || ! apt-get install -y -qq tailscale; then
2025-06-26 16:22:23 +01:00
print_error "Failed to install Tailscale."
exit 1
fi
2025-06-26 16:49:03 +01:00
systemctl enable tailscaled
systemctl start tailscaled
2025-06-26 16:22:23 +01:00
if [ [ -n " ${ TAILSCALE_LOGIN_SERVER :- } " && -n " ${ TAILSCALE_AUTH_KEY :- } " && -n " ${ TAILSCALE_OPERATOR :- } " ] ] ; then
print_info "Configuring Tailscale with Headscale..."
local ts_cmd = " tailscale up --login-server= $TAILSCALE_LOGIN_SERVER --operator= $TAILSCALE_OPERATOR "
[ [ " $TAILSCALE_ACCEPT_DNS " = = "yes" ] ] && ts_cmd = " $ts_cmd --accept-dns "
[ [ " $TAILSCALE_ACCEPT_ROUTES " = = "yes" ] ] && ts_cmd = " $ts_cmd --accept-routes "
[ [ -n " ${ TAILSCALE_AUTH_KEY :- } " ] ] && ts_cmd = " $ts_cmd --authkey= $TAILSCALE_AUTH_KEY "
ts_cmd = " $ts_cmd --hostname= $SERVER_NAME "
if eval " $ts_cmd " 2>& 1 | tee -a " $LOG_FILE " ; then
2025-06-26 16:49:03 +01:00
if tailscale status | grep -q " $SERVER_NAME " ; then
print_success "Tailscale configured and connected."
tailscale status | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 16:49:03 +01:00
print_error "Tailscale failed to connect. Check 'tailscale status' and log file."
exit 1
2025-06-26 16:22:23 +01:00
fi
else
2025-06-26 16:49:03 +01:00
print_error "Failed to configure Tailscale. Check log file."
exit 1
2025-06-26 16:22:23 +01:00
fi
else
2025-06-26 16:49:03 +01:00
print_info "Tailscale installed but not configured (missing login server, auth key, or operator)."
SKIPPED_SETTINGS += ( "Tailscale configuration" )
2025-06-26 16:22:23 +01:00
fi
print_success "Tailscale installation completed."
log "Tailscale installation and configuration completed."
}
configure_swap( ) {
print_section "Swap Configuration"
if [ [ " $IS_CONTAINER " = = true ] ] ; then
2025-06-26 16:49:03 +01:00
print_info "Skipping swap configuration in container environment."
2025-06-26 16:22:23 +01:00
SKIPPED_SETTINGS += ( "swap configuration" )
return 0
fi
if [ [ -z " ${ SWAP_SIZE :- } " ] ] ; then
print_info "No swap size specified. Skipping swap configuration."
SKIPPED_SETTINGS += ( "swap configuration" )
return 0
fi
if ! validate_swap_size " $SWAP_SIZE " ; then
print_error " Invalid SWAP_SIZE format: $SWAP_SIZE . Skipping swap configuration. "
SKIPPED_SETTINGS += ( "swap configuration" )
return 0
fi
2025-06-26 16:49:03 +01:00
local swap_size_bytes = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 16:22:23 +01:00
if [ [ -f /swapfile ] ] ; then
2025-06-26 16:49:03 +01:00
current_size = $( ls -l /swapfile | awk '{print $5}' )
if [ [ " $current_size " -eq " $swap_size_bytes " ] ] ; then
print_info " Swap file already exists with correct size ( $SWAP_SIZE ). "
2025-06-26 16:22:23 +01:00
return 0
else
2025-06-26 16:49:03 +01:00
print_info "Removing existing swap file..."
2025-06-26 16:22:23 +01:00
swapoff /swapfile 2>/dev/null || true
rm -f /swapfile
fi
fi
print_info " Creating swap file of size $SWAP_SIZE ... "
2025-06-26 16:49:03 +01:00
if ! fallocate -l " $swap_size_bytes " /swapfile; then
2025-06-26 16:22:23 +01:00
print_error "Failed to create swap file. Check disk space."
exit 1
fi
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
2025-06-26 16:49:03 +01:00
if ! grep -q "^/swapfile" /etc/fstab; then
2025-06-26 16:22:23 +01:00
echo "/swapfile none swap sw 0 0" >> /etc/fstab
fi
sysctl vm.swappiness= 10
2025-06-26 16:49:03 +01:00
echo "vm.swappiness=10" >> /etc/sysctl.conf
if free | grep -q "Swap:" ; then
print_success " Swap configured: $SWAP_SIZE "
free -h | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 16:49:03 +01:00
print_error "Failed to configure swap. Check 'free -h' and log file."
2025-06-26 16:22:23 +01:00
exit 1
fi
log "Swap configuration completed."
}
configure_time( ) {
print_section "Time Synchronization"
if ! dpkg -l chrony | grep -q ^ii; then
print_error "chrony package is not installed."
exit 1
fi
2025-06-26 16:49:03 +01:00
print_info "Configuring time synchronization with chrony..."
2025-06-26 16:22:23 +01:00
systemctl enable chrony
2025-06-26 16:49:03 +01:00
systemctl start chrony
2025-06-26 16:22:23 +01:00
if systemctl is-active --quiet chrony; then
2025-06-26 16:49:03 +01:00
print_success "Time synchronization configured."
chronyc tracking | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 16:49:03 +01:00
print_error "Failed to start chrony service. Check 'journalctl -u chrony'."
2025-06-26 16:22:23 +01:00
exit 1
fi
log "Time synchronization configuration completed."
}
cleanup( ) {
2025-06-26 16:49:03 +01:00
print_section "Cleanup"
print_info "Cleaning up package cache and temporary files..."
apt-get clean
rm -rf /var/lib/apt/lists/*
rm -f /tmp/*.sh
2025-06-26 16:22:23 +01:00
find /var/log -type f -name "*.log" -exec truncate -s 0 { } \;
2025-06-26 16:49:03 +01:00
print_success "Cleanup completed."
log "Cleanup completed."
2025-06-26 16:22:23 +01:00
}
print_summary( ) {
print_section "Setup Summary"
echo -e " ${ GREEN } Setup completed successfully! ${ NC } " | tee -a " $LOG_FILE "
2025-06-26 16:49:03 +01:00
echo -e " \n ${ YELLOW } Configuration Applied: ${ NC } "
echo -e " Username: $USERNAME "
echo -e " Hostname: $SERVER_NAME "
echo -e " SSH Port: $SSH_PORT "
echo -e " Server IP: $SERVER_IP "
echo -e " Timezone: $TIMEZONE "
[ [ -n " ${ SWAP_SIZE :- } " && " $IS_CONTAINER " = = false ] ] && echo -e " Swap Size: $SWAP_SIZE "
[ [ -n " ${ UFW_PORTS :- } " ] ] && echo -e " UFW Ports: $UFW_PORTS "
[ [ " $AUTO_UPDATES " = = "yes" ] ] && echo -e " Auto Updates: Enabled"
[ [ " $INSTALL_DOCKER " = = "yes" ] ] && echo -e " Docker: Installed"
[ [ " $INSTALL_TAILSCALE " = = "yes" ] ] && echo -e " Tailscale: Installed"
[ [ -n " ${ TAILSCALE_LOGIN_SERVER :- } " ] ] && echo -e " Tailscale Server: $TAILSCALE_LOGIN_SERVER "
[ [ -n " ${ SMTP_SERVER :- } " ] ] && echo -e " SMTP Server: $SMTP_SERVER "
[ [ -n " ${ NTFY_SERVER :- } " ] ] && echo -e " ntfy Server: $NTFY_SERVER "
2025-06-26 16:22:23 +01:00
if [ [ ${# PROMPTED_SETTINGS [@] } -gt 0 ] ] ; then
2025-06-26 16:49:03 +01:00
echo -e " \n ${ CYAN } Prompted Settings: ${ NC } ${ PROMPTED_SETTINGS [*] } "
2025-06-26 16:22:23 +01:00
fi
if [ [ ${# SKIPPED_SETTINGS [@] } -gt 0 ] ] ; then
2025-06-26 16:49:03 +01:00
echo -e " \n ${ YELLOW } Skipped Settings: ${ NC } ${ SKIPPED_SETTINGS [*] } "
fi
echo -e " \n ${ YELLOW } Log File: ${ NC } $LOG_FILE "
echo -e " ${ YELLOW } Backups: ${ NC } $BACKUP_DIR "
echo -e " \n ${ YELLOW } Next Steps: ${ NC } "
echo -e " 1. Verify SSH: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP "
echo -e " 2. Check Firewall: sudo ufw status verbose"
echo -e " 3. Check Services: systemctl status ssh fail2ban chrony docker tailscaled postfix"
echo -e " 4. Reboot (recommended): sudo reboot"
echo -e " \n ${ YELLOW } Note: ${ NC } If using a VPS (e.g., DigitalOcean, AWS), ensure the edge firewall allows port $SSH_PORT and other configured ports. "
2025-06-26 16:22:23 +01:00
log "Setup summary completed."
}
main( ) {
print_header
check_system
check_dependencies
collect_config
install_packages
setup_user
configure_system
configure_ssh
configure_firewall
configure_fail2ban
configure_auto_updates
configure_monitoring
install_docker
install_tailscale
configure_swap
configure_time
cleanup
print_summary
}
2025-06-26 16:49:03 +01:00
# Execute main function
2025-06-26 16:22:23 +01:00
main
2025-06-26 16:49:03 +01:00
` ` `