2025-06-26 16:22:23 +01:00
#!/bin/bash
# Debian/Ubuntu Server Setup and Hardening Script
2025-06-26 20:03:08 +01:00
# Version: 4.2 | 2025-06-26
2025-06-26 16:22:23 +01:00
# Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS, 24.10 (experimental)
#
2025-06-26 17:12:59 +01:00
# Purpose: Automates server setup, security hardening, and optional installations (Docker, Tailscale).
# Features: User creation, SSH hardening, UFW, Fail2Ban, auto-updates, monitoring (SMTP/ntfy), swap, time sync.
# Usage: Run as root with optional --quiet or --config <file> flags.
2025-06-26 16:22:23 +01:00
# 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 = ""
2025-06-26 17:12:59 +01:00
UBUNTU_CODENAME = ""
2025-06-26 16:22:23 +01:00
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 ---
2025-06-26 17:12:59 +01:00
# Log messages to file with timestamp
2025-06-26 16:22:23 +01:00
log( ) {
echo " $( date '+%Y-%m-%d %H:%M:%S' ) - $1 " >> " $LOG_FILE "
}
2025-06-26 17:12:59 +01:00
# Print header with script title and version
2025-06-26 16:22:23 +01:00
print_header( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ CYAN } ╔═════════════════════════════════════════════════════════════════╗ ${ NC } "
echo -e " ${ CYAN } ║ ║ ${ NC } "
echo -e " ${ CYAN } ║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║ ${ NC } "
2025-06-26 20:03:08 +01:00
echo -e " ${ CYAN } ║ v4.2 | 2025-06-26 ║ ${ NC } "
2025-06-26 16:22:23 +01:00
echo -e " ${ CYAN } ╚═════════════════════════════════════════════════════════════════╝ ${ NC } "
echo
}
2025-06-26 17:12:59 +01:00
# Print section title
2025-06-26 16:22:23 +01:00
print_section( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " \n ${ BLUE } ▓▓▓ $1 ▓▓▓ ${ NC } " | tee -a " $LOG_FILE "
echo -e " ${ BLUE } $( printf '═%.0s' { 1..65} ) ${ NC } "
}
2025-06-26 17:12:59 +01:00
# Print success message
2025-06-26 16:22:23 +01:00
print_success( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ GREEN } ✓ $1 ${ NC } " | tee -a " $LOG_FILE "
}
2025-06-26 17:12:59 +01:00
# Print error message (always printed, even in quiet mode)
2025-06-26 16:22:23 +01:00
print_error( ) {
echo -e " ${ RED } ✗ $1 ${ NC } " | tee -a " $LOG_FILE "
}
2025-06-26 17:12:59 +01:00
# Print warning message
2025-06-26 16:22:23 +01:00
print_warning( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ YELLOW } ⚠ $1 ${ NC } " | tee -a " $LOG_FILE "
}
2025-06-26 17:12:59 +01:00
# Print info message
2025-06-26 16:22:23 +01:00
print_info( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ PURPLE } ℹ $1 ${ NC } " | tee -a " $LOG_FILE "
}
2025-06-26 17:12:59 +01:00
# Prompt for confirmation with default response
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 ---
2025-06-26 17:12:59 +01:00
# Validate username (lowercase, numbers, hyphens, underscores, max 32 chars)
2025-06-26 16:22:23 +01:00
validate_username( ) {
local username = " $1 "
[ [ " $username " = ~ ^[ a-z_] [ a-z0-9_-] *$ && ${# username } -le 32 ] ]
}
2025-06-26 17:12:59 +01:00
# Validate hostname (alphanumeric, dots, hyphens, max 253 chars)
2025-06-26 16:22:23 +01:00
validate_hostname( ) {
local hostname = " $1 "
[ [ " $hostname " = ~ ^[ a-zA-Z0-9] [ a-zA-Z0-9.-] { 0,253} [ a-zA-Z0-9] $ && ! " $hostname " = ~ \. \. ] ]
}
2025-06-26 17:12:59 +01:00
# Validate port (1024-65535)
2025-06-26 16:22:23 +01:00
validate_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " -ge 1024 && " $port " -le 65535 ] ]
}
2025-06-26 17:12:59 +01:00
# Validate SSH public key format
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) \ ] ]
}
2025-06-26 17:12:59 +01:00
# Validate timezone (check if exists in /usr/share/zoneinfo)
2025-06-26 16:22:23 +01:00
validate_timezone( ) {
local tz = " $1 "
[ [ -e " /usr/share/zoneinfo/ $tz " ] ]
}
2025-06-26 17:12:59 +01:00
# Validate swap size (e.g., 2G, 512M)
2025-06-26 16:22:23 +01:00
validate_swap_size( ) {
local size = " $1 "
[ [ " $size " = ~ ^[ 0-9] +[ MG] $ ] ] && [ [ " ${ size %[MG] } " -ge 1 ] ]
}
2025-06-26 17:12:59 +01:00
# Validate UFW port format (e.g., 80/tcp, 123/udp)
2025-06-26 16:22:23 +01:00
validate_ufw_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +( /tcp| /udp) ?$ ] ]
}
2025-06-26 17:12:59 +01:00
# Validate URL format
2025-06-26 16:22:23 +01:00
validate_url( ) {
local url = " $1 "
[ [ " $url " = ~ ^https?://[ a-zA-Z0-9.-] +( :[ 0-9] +) ?( /.*) ?$ ] ]
}
2025-06-26 17:12:59 +01:00
# Validate email format
2025-06-26 16:22:23 +01:00
validate_email( ) {
local email = " $1 "
[ [ " $email " = ~ ^[ a-zA-Z0-9._%+-] +@[ a-zA-Z0-9.-] +\. [ a-zA-Z] { 2,} $ ] ]
}
2025-06-26 17:12:59 +01:00
# Validate SMTP port (common ports: 25, 2525, 8025, 587, 80, 465, 8465, 443)
2025-06-26 16:22:23 +01:00
validate_smtp_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " = ~ ^( 25| 2525| 8025| 587| 80| 465| 8465| 443) $ ] ]
}
2025-06-26 17:12:59 +01:00
# Validate ntfy token format (starts with tk_)
2025-06-26 16:22:23 +01:00
validate_ntfy_token( ) {
local token = " $1 "
[ [ " $token " = ~ ^tk_[ a-zA-Z0-9_-] +$ || -z " $token " ] ]
}
2025-06-26 17:12:59 +01:00
# Convert swap size (e.g., 2G, 512M) to bytes
2025-06-26 16:22:23 +01:00
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
}
2025-06-26 16:49:03 +01:00
# --- USER PROMPT FUNCTIONS ---
2025-06-26 17:12:59 +01:00
# Prompt for admin username
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for server hostname
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for SSH port
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for timezone
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for swap size
2025-06-26 16:22:23 +01:00
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 }
2025-06-26 17:12:59 +01:00
if validate_swap_size " $SWAP_SIZE " ; then
local swap_size_bytes = $( convert_to_bytes " $SWAP_SIZE " )
local available_space = $( df / | awk 'NR==2 {print $4 * 1024}' ) # Convert to bytes
if [ [ $available_space -lt $swap_size_bytes ] ] ; then
print_warning " Insufficient disk space for $SWAP_SIZE swap. Available: $(( available_space / 1024 / 1024 )) M. "
if ! confirm "Try a smaller swap size?" ; then
SWAP_SIZE = ""
SKIPPED_SETTINGS += ( "swap configuration" )
break
fi
else
break
fi
else
print_error "Invalid size. Use format like '2G' or '512M'."
fi
2025-06-26 16:22:23 +01:00
done
2025-06-26 17:12:59 +01:00
[ [ -n " $SWAP_SIZE " ] ] && PROMPTED_SETTINGS += ( "SWAP_SIZE" )
2025-06-26 16:22:23 +01:00
}
2025-06-26 17:12:59 +01:00
# Prompt for UFW ports (comma-separated)
2025-06-26 16:22:23 +01:00
prompt_ufw_ports( ) {
2025-06-26 17:12:59 +01:00
if confirm "Add custom UFW ports (e.g., 80/tcp,443/tcp)?" ; then
2025-06-26 16:22:23 +01:00
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
}
2025-06-26 17:12:59 +01:00
# Prompt for automatic updates
2025-06-26 16:22:23 +01:00
prompt_auto_updates( ) {
AUTO_UPDATES = "no"
confirm "Enable automatic security updates?" && AUTO_UPDATES = "yes"
PROMPTED_SETTINGS += ( "AUTO_UPDATES" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Docker installation
2025-06-26 16:22:23 +01:00
prompt_install_docker( ) {
INSTALL_DOCKER = "no"
confirm "Install Docker Engine?" && INSTALL_DOCKER = "yes"
PROMPTED_SETTINGS += ( "INSTALL_DOCKER" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Tailscale installation
2025-06-26 16:22:23 +01:00
prompt_install_tailscale( ) {
INSTALL_TAILSCALE = "no"
confirm "Install Tailscale VPN?" && INSTALL_TAILSCALE = "yes"
PROMPTED_SETTINGS += ( "INSTALL_TAILSCALE" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Tailscale login server
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Tailscale auth key
2025-06-26 16:22:23 +01:00
prompt_tailscale_auth_key( ) {
read -rp " $( echo -e " ${ CYAN } Enter Tailscale auth key: ${ NC } " ) " TAILSCALE_AUTH_KEY
PROMPTED_SETTINGS += ( "TAILSCALE_AUTH_KEY" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Tailscale operator
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Tailscale DNS acceptance
2025-06-26 16:22:23 +01:00
prompt_tailscale_accept_dns( ) {
TAILSCALE_ACCEPT_DNS = "yes"
confirm "Accept Tailscale DNS?" "y" || TAILSCALE_ACCEPT_DNS = "no"
PROMPTED_SETTINGS += ( "TAILSCALE_ACCEPT_DNS" )
}
2025-06-26 17:12:59 +01:00
# Prompt for Tailscale routes acceptance
2025-06-26 16:22:23 +01:00
prompt_tailscale_accept_routes( ) {
TAILSCALE_ACCEPT_ROUTES = "yes"
confirm "Accept Tailscale routes?" "y" || TAILSCALE_ACCEPT_ROUTES = "no"
PROMPTED_SETTINGS += ( "TAILSCALE_ACCEPT_ROUTES" )
}
2025-06-26 17:12:59 +01:00
# Prompt for SMTP server
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for SMTP port
2025-06-26 16:22:23 +01:00
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 17:12:59 +01:00
# Prompt for SMTP username
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for SMTP password
2025-06-26 16:49:03 +01:00
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 17:12:59 +01:00
# Prompt for SMTP from email
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for SMTP to email
2025-06-26 16:22:23 +01:00
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" )
}
2025-06-26 17:12:59 +01:00
# Prompt for ntfy server
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
PROMPTED_SETTINGS += ( "NTFY_SERVER" )
}
# Prompt for ntfy token
prompt_ntfy_token( ) {
read -rp " $( echo -e " ${ CYAN } Enter ntfy token: ${ NC } " ) " NTFY_TOKEN
if [ [ -n " $NTFY_TOKEN " && ! $( validate_ntfy_token " $NTFY_TOKEN " ) ] ] ; then
print_error "Invalid ntfy token (must start with tk_)."
NTFY_TOKEN = ""
fi
PROMPTED_SETTINGS += ( "NTFY_TOKEN" )
}
# --- CONFIG FILE LOADING ---
# Load and validate configuration from file
load_config( ) {
local config_file = " $1 "
if [ [ ! -f " $config_file " ] ] ; then
print_error " Config file $config_file not found. "
return 1
fi
print_info " Loaded configuration from $config_file "
log " Loaded configuration from $config_file "
source " $config_file "
# Initialize defaults
USERNAME = " ${ USERNAME :- } "
2025-06-26 20:03:08 +01:00
HOSTNAME = " ${ HOSTNAME :- $( hostname) } " # Fallback to current hostname
SERVER_NAME = " $HOSTNAME " # Set SERVER_NAME immediately
PRETTY_NAME = " ${ PRETTY_NAME :- $HOSTNAME } " # Set PRETTY_NAME with fallback
2025-06-26 17:12:59 +01:00
SSH_PORT = " ${ SSH_PORT :- 5595 } "
TIMEZONE = " ${ TIMEZONE :- Etc /UTC } "
SWAP_SIZE = " ${ SWAP_SIZE :- 2G } "
UFW_PORTS = " ${ UFW_PORTS :- } "
AUTO_UPDATES = " ${ AUTO_UPDATES :- no } "
INSTALL_DOCKER = " ${ INSTALL_DOCKER :- no } "
INSTALL_TAILSCALE = " ${ INSTALL_TAILSCALE :- no } "
TAILSCALE_LOGIN_SERVER = " ${ TAILSCALE_LOGIN_SERVER :- } "
TAILSCALE_AUTH_KEY = " ${ TAILSCALE_AUTH_KEY :- } "
TAILSCALE_OPERATOR = " ${ TAILSCALE_OPERATOR :- ${ USERNAME :- } } "
TAILSCALE_ACCEPT_DNS = " ${ TAILSCALE_ACCEPT_DNS :- yes } "
TAILSCALE_ACCEPT_ROUTES = " ${ TAILSCALE_ACCEPT_ROUTES :- yes } "
SMTP_SERVER = " ${ SMTP_SERVER :- } "
SMTP_PORT = " ${ SMTP_PORT :- } "
SMTP_USER = " ${ SMTP_USER :- } "
SMTP_PASS = " ${ SMTP_PASS :- } "
SMTP_FROM = " ${ SMTP_FROM :- } "
SMTP_TO = " ${ SMTP_TO :- } "
NTFY_SERVER = " ${ NTFY_SERVER :- } "
NTFY_TOKEN = " ${ NTFY_TOKEN :- } "
# Validate required fields
local errors = ( )
if [ [ -z " $USERNAME " ] ] ; then
errors += ( "Missing USERNAME" )
elif ! validate_username " $USERNAME " ; then
errors += ( "Invalid USERNAME" )
fi
if [ [ -z " $HOSTNAME " ] ] ; then
errors += ( "Missing HOSTNAME" )
elif ! validate_hostname " $HOSTNAME " ; then
errors += ( "Invalid HOSTNAME" )
2025-06-26 20:03:08 +01:00
SERVER_NAME = " $( hostname) " # Fallback to current hostname if invalid
HOSTNAME = " $SERVER_NAME "
fi
if [ [ " $HOSTNAME " != *.* ] ] ; then
2025-06-26 20:32:02 +01:00
print_warning " Hostname ' $HOSTNAME ' is not an FQDN. Consider using an FQDN (e.g., $HOSTNAME .mydomain.com) for better compatibility. "
2025-06-26 17:12:59 +01:00
fi
if [ [ -z " $SSH_PORT " ] ] ; then
errors += ( "Missing SSH_PORT" )
elif ! validate_port " $SSH_PORT " ; then
errors += ( "Invalid SSH_PORT" )
fi
if [ [ -z " $TIMEZONE " ] ] ; then
errors += ( "Missing TIMEZONE" )
elif ! validate_timezone " $TIMEZONE " ; then
errors += ( "Invalid TIMEZONE" )
fi
if [ [ -n " $SWAP_SIZE " && ! " $SWAP_SIZE " = ~ ^[ 0-9] +[ MG] $ ] ] ; then
errors += ( "Invalid SWAP_SIZE" )
fi
if [ [ -n " $UFW_PORTS " ] ] ; then
for port in ${ UFW_PORTS //,/ } ; do
if ! validate_ufw_port " $port " ; then
errors += ( " Invalid UFW_PORTS format: $port " )
fi
done
fi
if [ [ -n " $AUTO_UPDATES " && ! " $AUTO_UPDATES " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid AUTO_UPDATES (must be yes/no)" )
fi
if [ [ -n " $INSTALL_DOCKER " && ! " $INSTALL_DOCKER " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid INSTALL_DOCKER (must be yes/no)" )
fi
if [ [ -n " $INSTALL_TAILSCALE " && ! " $INSTALL_TAILSCALE " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid INSTALL_TAILSCALE (must be yes/no)" )
fi
if [ [ " $INSTALL_TAILSCALE " = = "yes" && -n " $TAILSCALE_LOGIN_SERVER " ] ] ; then
if ! validate_url " $TAILSCALE_LOGIN_SERVER " ; then
errors += ( "Invalid TAILSCALE_LOGIN_SERVER" )
fi
if [ [ -z " $TAILSCALE_AUTH_KEY " ] ] ; then
errors += ( "Missing TAILSCALE_AUTH_KEY" )
fi
if [ [ -z " $TAILSCALE_OPERATOR " ] ] ; then
errors += ( "Missing TAILSCALE_OPERATOR" )
elif ! validate_username " $TAILSCALE_OPERATOR " ; then
errors += ( "Invalid TAILSCALE_OPERATOR" )
fi
if [ [ -n " $TAILSCALE_ACCEPT_DNS " && ! " $TAILSCALE_ACCEPT_DNS " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid TAILSCALE_ACCEPT_DNS" )
fi
if [ [ -n " $TAILSCALE_ACCEPT_ROUTES " && ! " $TAILSCALE_ACCEPT_ROUTES " = ~ ^( yes| no) $ ] ] ; then
errors += ( "Invalid TAILSCALE_ACCEPT_ROUTES" )
fi
fi
if [ [ -n " $SMTP_SERVER " ] ] ; then
if [ [ ! " $SMTP_SERVER " = ~ ^[ a-zA-Z0-9.-] +$ ] ] ; then
errors += ( "Invalid SMTP_SERVER" )
fi
if [ [ -z " $SMTP_PORT " ] ] ; then
errors += ( "Missing SMTP_PORT" )
elif ! validate_smtp_port " $SMTP_PORT " ; then
errors += ( "Invalid SMTP_PORT" )
fi
if [ [ -z " $SMTP_USER " ] ] ; then
errors += ( "Missing SMTP_USER" )
fi
if [ [ -z " $SMTP_PASS " ] ] ; then
errors += ( "Missing SMTP_PASS" )
fi
if [ [ -z " $SMTP_FROM " ] ] ; then
errors += ( "Missing SMTP_FROM" )
elif ! validate_email " $SMTP_FROM " ; then
errors += ( "Invalid SMTP_FROM" )
fi
if [ [ -z " $SMTP_TO " ] ] ; then
errors += ( "Missing SMTP_TO" )
elif ! validate_email " $SMTP_TO " ; then
errors += ( "Invalid SMTP_TO" )
fi
fi
if [ [ -n " $NTFY_SERVER " ] ] ; then
if ! validate_url " $NTFY_SERVER " ; then
errors += ( "Invalid NTFY_SERVER" )
fi
if [ [ -z " $NTFY_TOKEN " && " $VERBOSE " = = false ] ] ; then
errors += ( "Missing NTFY_TOKEN (skipping ntfy in quiet mode)" )
SKIPPED_SETTINGS += ( "ntfy" )
NTFY_SERVER = ""
NTFY_TOKEN = ""
elif [ [ -n " $NTFY_TOKEN " && ! " $NTFY_TOKEN " = ~ ^tk_[ a-zA-Z0-9_-] +$ ] ] ; then
errors += ( "Invalid NTFY_TOKEN" )
fi
fi
2025-06-26 16:22:23 +01:00
2025-06-26 17:12:59 +01:00
if [ [ ${# errors [@] } -gt 0 ] ] ; then
[ [ $VERBOSE = = true ] ] && for error in " ${ errors [@] } " ; do
print_error " $error "
done
if [ [ $VERBOSE = = true ] ] ; then
print_info "Prompting for missing/invalid required settings..."
[ [ -z " $USERNAME " || ! $( validate_username " $USERNAME " ) ] ] && prompt_username
[ [ -z " $HOSTNAME " || ! $( validate_hostname " $HOSTNAME " ) ] ] && prompt_hostname
[ [ -z " $SSH_PORT " || ! $( validate_port " $SSH_PORT " ) ] ] && prompt_ssh_port
[ [ -z " $TIMEZONE " || ! $( validate_timezone " $TIMEZONE " ) ] ] && prompt_timezone
[ [ -n " $SWAP_SIZE " && ! $( validate_swap_size " $SWAP_SIZE " ) ] ] && prompt_swap_size
if [ [ -n " $UFW_PORTS " ] ] ; then
local valid_ports = true
for port in ${ UFW_PORTS //,/ } ; do
if ! validate_ufw_port " $port " ; then
valid_ports = false
break
fi
done
[ [ " $valid_ports " = = false ] ] && prompt_ufw_ports
fi
[ [ -n " $AUTO_UPDATES " && ! " $AUTO_UPDATES " = ~ ^( yes| no) $ ] ] && prompt_auto_updates
[ [ -n " $INSTALL_DOCKER " && ! " $INSTALL_DOCKER " = ~ ^( yes| no) $ ] ] && prompt_install_docker
[ [ -n " $INSTALL_TAILSCALE " && ! " $INSTALL_TAILSCALE " = ~ ^( yes| no) $ ] ] && prompt_install_tailscale
if [ [ " $INSTALL_TAILSCALE " = = "yes" && -n " $TAILSCALE_LOGIN_SERVER " ] ] ; then
[ [ ! $( validate_url " $TAILSCALE_LOGIN_SERVER " ) ] ] && prompt_tailscale_login_server
[ [ -z " $TAILSCALE_AUTH_KEY " ] ] && prompt_tailscale_auth_key
[ [ -z " $TAILSCALE_OPERATOR " || ! $( validate_username " $TAILSCALE_OPERATOR " ) ] ] && prompt_tailscale_operator
[ [ -n " $TAILSCALE_ACCEPT_DNS " && ! " $TAILSCALE_ACCEPT_DNS " = ~ ^( yes| no) $ ] ] && prompt_tailscale_accept_dns
[ [ -n " $TAILSCALE_ACCEPT_ROUTES " && ! " $TAILSCALE_ACCEPT_ROUTES " = ~ ^( yes| no) $ ] ] && prompt_tailscale_accept_routes
fi
if [ [ -n " $SMTP_SERVER " ] ] ; then
[ [ ! " $SMTP_SERVER " = ~ ^[ a-zA-Z0-9.-] +$ ] ] && prompt_smtp_server
[ [ -z " $SMTP_PORT " || ! $( validate_smtp_port " $SMTP_PORT " ) ] ] && prompt_smtp_port
[ [ -z " $SMTP_USER " ] ] && prompt_smtp_user
[ [ -z " $SMTP_PASS " ] ] && prompt_smtp_pass
[ [ -z " $SMTP_FROM " || ! $( validate_email " $SMTP_FROM " ) ] ] && prompt_smtp_from
[ [ -z " $SMTP_TO " || ! $( validate_email " $SMTP_TO " ) ] ] && prompt_smtp_to
fi
if [ [ -n " $NTFY_SERVER " ] ] ; then
[ [ ! $( validate_url " $NTFY_SERVER " ) ] ] && prompt_ntfy_server
[ [ -z " $NTFY_TOKEN " || ! $( validate_ntfy_token " $NTFY_TOKEN " ) ] ] && prompt_ntfy_token
fi
2025-06-26 20:03:08 +01:00
# Ensure SERVER_NAME and PRETTY_NAME are set after prompting
SERVER_NAME = " $HOSTNAME "
PRETTY_NAME = " ${ PRETTY_NAME :- $HOSTNAME } "
2025-06-26 17:12:59 +01:00
return 0
else
2025-06-26 20:03:08 +01:00
print_error " Invalid or missing configuration in quiet mode. Using default hostname: $HOSTNAME "
SERVER_NAME = " $HOSTNAME "
PRETTY_NAME = " ${ PRETTY_NAME :- $HOSTNAME } "
return 0
2025-06-26 17:12:59 +01:00
fi
2025-06-26 16:22:23 +01:00
fi
2025-06-26 20:03:08 +01:00
# Ensure SERVER_NAME and PRETTY_NAME are set if validation passes
SERVER_NAME = " $HOSTNAME "
PRETTY_NAME = " ${ PRETTY_NAME :- $HOSTNAME } "
2025-06-26 17:12:59 +01:00
return 0
2025-06-26 16:22:23 +01:00
}
2025-06-26 17:12:59 +01:00
# --- FULL INTERACTIVE CONFIG ---
2025-06-26 16:22:23 +01:00
2025-06-26 17:12:59 +01:00
# Collect all configuration interactively
full_interactive_config( ) {
prompt_username
prompt_hostname
read -rp " $( echo -e " ${ CYAN } Enter a 'pretty' hostname (optional): ${ NC } " ) " PRETTY_NAME
2025-06-26 20:03:08 +01:00
[ [ -z " $PRETTY_NAME " ] ] && PRETTY_NAME = " $HOSTNAME "
SERVER_NAME = " $HOSTNAME " # Ensure SERVER_NAME is set
2025-06-26 17:12:59 +01:00
prompt_ssh_port
prompt_timezone
prompt_swap_size
prompt_ufw_ports
prompt_auto_updates
prompt_install_docker
prompt_install_tailscale
if [ [ " $INSTALL_TAILSCALE " = = "yes" ] ] ; then
prompt_tailscale_login_server
if [ [ -n " $TAILSCALE_LOGIN_SERVER " ] ] ; then
prompt_tailscale_auth_key
prompt_tailscale_operator
prompt_tailscale_accept_dns
prompt_tailscale_accept_routes
fi
fi
if confirm "Configure system monitoring with SMTP and/or ntfy?" ; then
prompt_smtp_server
if [ [ -n " $SMTP_SERVER " ] ] ; then
prompt_smtp_port
prompt_smtp_user
prompt_smtp_pass
prompt_smtp_from
prompt_smtp_to
fi
prompt_ntfy_server
if [ [ -n " $NTFY_SERVER " ] ] ; then
prompt_ntfy_token
2025-06-26 16:22:23 +01:00
fi
fi
}
2025-06-26 17:12:59 +01:00
# --- CORE FUNCTIONS ---
# Check system compatibility and prerequisites
2025-06-26 16:22:23 +01:00
check_system( ) {
print_section "System Compatibility Check"
2025-06-26 17:12:59 +01:00
# Verify root privileges
2025-06-26 16:22:23 +01:00
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."
2025-06-26 17:12:59 +01:00
# Detect container environment
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Check OS compatibility
2025-06-26 16:22:23 +01:00
if [ [ -f /etc/os-release ] ] ; then
source /etc/os-release
ID = " $ID "
2025-06-26 17:12:59 +01:00
UBUNTU_CODENAME = " $UBUNTU_CODENAME "
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Check SSH daemon presence
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 17:12:59 +01:00
elif command -v sshd >/dev/null || command -v dropbear >/dev/null; then
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."
2025-06-26 17:12:59 +01:00
SSH_SERVICE = "ssh.service"
2025-06-26 16:22:23 +01:00
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 17:12:59 +01:00
SSH_SERVICE = "sshd.service"
elif ps aux | grep -q "[s]shd\|[d]ropbear" ; then
print_warning "SSH daemon running but no standard service detected. Assuming sshd."
SSH_SERVICE = "sshd.service"
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
2025-06-26 17:12:59 +01:00
else
print_error "No SSH daemon (sshd or dropbear) detected. Please install openssh-server or dropbear."
exit 1
2025-06-26 16:22:23 +01:00
fi
2025-06-26 17:12:59 +01:00
# Verify internet connectivity
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Check log directory permissions
2025-06-26 16:22:23 +01:00
if [ [ ! -w /var/log ] ] ; then
print_error "Failed to write to /var/log. Cannot create log file."
exit 1
fi
2025-06-26 17:12:59 +01:00
# Fix /etc/shadow permissions
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."
}
2025-06-26 17:12:59 +01:00
# Install required dependencies
check_dependencies( ) {
print_section "Checking Dependencies"
local missing_deps = ( )
command -v curl >/dev/null || missing_deps += ( "curl" )
command -v sudo >/dev/null || missing_deps += ( "sudo" )
command -v gpg >/dev/null || missing_deps += ( "gpg" )
command -v postmap >/dev/null || missing_deps += ( "postfix" )
[ [ -n " ${ SMTP_SERVER :- } " ] ] && ! command -v mail >/dev/null && missing_deps += ( "mailutils" )
[ [ -n " ${ SMTP_SERVER :- } " ] ] && ! command -v swaks >/dev/null && missing_deps += ( "swaks" )
if [ [ ${# missing_deps [@] } -gt 0 ] ] ; then
print_info " Installing missing dependencies: ${ missing_deps [*] } "
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."
}
# Collect configuration from file or interactively
2025-06-26 16:22:23 +01:00
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 "
}
2025-06-26 17:12:59 +01:00
# Install essential packages
2025-06-26 16:22:23 +01:00
install_packages( ) {
print_section "Package Installation"
print_info "Updating package lists and upgrading system..."
if ! apt-get update -qq || ! DEBIAN_FRONTEND = noninteractive apt-get upgrade -y -qq; then
print_error "Failed to update 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."
}
2025-06-26 17:12:59 +01:00
# Set up user account and SSH keys
2025-06-26 16:22:23 +01:00
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."
}
2025-06-26 17:12:59 +01:00
# Configure system settings (timezone, hostname, locales)
2025-06-26 16:22:23 +01:00
configure_system( ) {
print_section "System Configuration"
mkdir -p " $BACKUP_DIR " && chmod 700 " $BACKUP_DIR "
2025-06-26 17:12:59 +01:00
# Backup critical files with restricted permissions
cp /etc/hosts " $BACKUP_DIR /hosts.backup " && chmod 600 " $BACKUP_DIR /hosts.backup "
cp /etc/fstab " $BACKUP_DIR /fstab.backup " && chmod 600 " $BACKUP_DIR /fstab.backup "
cp /etc/sysctl.conf " $BACKUP_DIR /sysctl.conf.backup " 2>/dev/null && chmod 600 " $BACKUP_DIR /sysctl.conf.backup " || true
# Configure timezone
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Configure locales if requested
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
2025-06-26 17:12:59 +01:00
# Configure hostname
2025-06-26 16:22:23 +01:00
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."
}
2025-06-26 17:12:59 +01:00
# Harden SSH configuration
2025-06-26 16:22:23 +01:00
configure_ssh( ) {
print_section "SSH Hardening"
2025-06-26 17:12:59 +01:00
# Verify 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-26 17:12:59 +01:00
# Confirm SSH service
if [ [ -n " $SSH_SERVICE " ] ] ; then
print_info " Using SSH service: $SSH_SERVICE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 17:12:59 +01:00
print_error "SSH service not set. Please check openssh-server installation."
2025-06-26 16:22:23 +01:00
exit 1
fi
log " Detected SSH service: $SSH_SERVICE "
systemctl status " $SSH_SERVICE " --no-pager >> " $LOG_FILE " 2>& 1
2025-06-26 17:12:59 +01:00
ps aux | grep "[s]shd\|[d]ropbear" >> " $LOG_FILE " 2>& 1
2025-06-26 16:22:23 +01:00
2025-06-26 17:12:59 +01:00
# Enable and start SSH service if not active
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
2025-06-26 17:12:59 +01:00
# Generate SSH key if none exists
2025-06-26 16:22:23 +01:00
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 17:12:59 +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
2025-06-26 17:12:59 +01:00
# Test SSH key authentication
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Backup SSH configuration
2025-06-26 16:22:23 +01:00
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 17:12:59 +01:00
cp /etc/ssh/sshd_config " $SSHD_BACKUP_FILE " && chmod 600 " $SSHD_BACKUP_FILE "
2025-06-26 16:22:23 +01:00
2025-06-26 17:12:59 +01:00
# Apply hardened SSH configuration
2025-06-26 16:22:23 +01:00
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 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'
******************************************************************************
AUTHORIZED ACCESS ONLY
═════ all attempts are logged and reviewed ═════
******************************************************************************
EOF
fi
2025-06-26 17:12:59 +01:00
# Test and apply SSH configuration
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Verify root SSH login is disabled
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
# Final SSH connection test
2025-06-26 16:22:23 +01:00
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."
}
2025-06-26 17:12:59 +01:00
# Configure UFW firewall
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
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
2025-06-26 17:12:59 +01:00
if confirm "Add additional custom ports (e.g., 8080/tcp,123/udp)?" ; then
2025-06-26 16:22:23 +01:00
while true; do
2025-06-26 17:12:59 +01:00
read -rp " $( echo -e " ${ CYAN } Enter ports (comma-separated, e.g., 8080/tcp,123/udp): ${ NC } " ) " CUSTOM_PORTS
2025-06-26 16:22:23 +01:00
if [ [ -z " $CUSTOM_PORTS " ] ] ; then
print_info "No custom ports entered. Skipping."
break
fi
valid = true
2025-06-26 17:12:59 +01:00
for port in ${ CUSTOM_PORTS //,/ } ; do
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
for port in ${ CUSTOM_PORTS //,/ } ; do
2025-06-26 16:22:23 +01:00
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
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 17:12:59 +01:00
# Configure Fail2Ban for intrusion prevention
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
2025-06-26 17:12:59 +01:00
cp /etc/fail2ban/jail.conf " $BACKUP_DIR /jail.conf.backup " 2>/dev/null && chmod 600 " $BACKUP_DIR /jail.conf.backup " || true
2025-06-26 16:49:03 +01:00
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."
}
2025-06-26 17:12:59 +01:00
# Configure automatic security updates
2025-06-26 16:49:03 +01:00
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"
2025-06-26 17:12:59 +01:00
cp " $config_file " " $BACKUP_DIR /50unattended-upgrades.backup " 2>/dev/null && chmod 600 " $BACKUP_DIR /50unattended-upgrades.backup " || true
2025-06-26 16:49:03 +01:00
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."
}
2025-06-26 17:12:59 +01:00
# Configure system monitoring with SMTP and/or ntfy
2025-06-26 16:49:03 +01:00
configure_monitoring( ) {
print_section "System Monitoring Configuration"
2025-06-26 17:12:59 +01:00
# Ensure backup directory exists with restricted permissions
mkdir -p " $BACKUP_DIR " && chmod 700 " $BACKUP_DIR "
if [ [ -n " ${ SMTP_SERVER :- } " && -n " ${ SMTP_PORT :- } " && -n " ${ SMTP_USER :- } " && -n " ${ SMTP_PASS :- } " && -n " ${ SMTP_FROM :- } " && -n " ${ SMTP_TO :- } " ] ] ; then
print_info "Configuring Postfix for SMTP monitoring..."
# Backup Postfix configuration
cp /etc/postfix/main.cf " $BACKUP_DIR /main.cf.backup " 2>/dev/null && chmod 600 " $BACKUP_DIR /main.cf.backup " || true
cp /etc/postfix/sasl_passwd " $BACKUP_DIR /sasl_passwd.backup " 2>/dev/null && chmod 600 " $BACKUP_DIR /sasl_passwd.backup " || true
2025-06-26 20:03:08 +01:00
cp /etc/aliases " $BACKUP_DIR /aliases.backup " 2>/dev/null && chmod 600 " $BACKUP_DIR /aliases.backup " || true
2025-06-26 17:12:59 +01:00
# Configure Postfix
postconf -e \
"smtp_sasl_auth_enable = yes" \
"smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \
"smtp_sasl_security_options = noanonymous" \
"smtp_tls_security_level = encrypt" \
"smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt" \
" relayhost = [ $SMTP_SERVER ]: $SMTP_PORT " \
"mynetworks = 127.0.0.0/8" \
"inet_interfaces = loopback-only"
# Set up SASL credentials
echo " [ $SMTP_SERVER ]: $SMTP_PORT $SMTP_USER : $SMTP_PASS " > /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd.db
# Set up sender canonical map
echo " /.*/ $SMTP_FROM " > /etc/postfix/sender_canonical
chmod 644 /etc/postfix/sender_canonical
postmap /etc/postfix/sender_canonical
postconf -e "sender_canonical_maps = hash:/etc/postfix/sender_canonical"
2025-06-26 20:03:08 +01:00
# Configure /etc/aliases to suppress root alias warning
if ! grep -q "^root:" /etc/aliases; then
echo " root: $SMTP_TO " >> /etc/aliases
newaliases
print_success " Configured /etc/aliases with root alias to $SMTP_TO . "
else
print_info "/etc/aliases already configured."
fi
2025-06-26 17:12:59 +01:00
# Reload Postfix
if ! systemctl reload postfix; then
print_error "Failed to reload Postfix. Check 'journalctl -u postfix'."
exit 1
2025-06-26 16:49:03 +01:00
fi
2025-06-26 17:12:59 +01:00
print_success "Postfix configured for SMTP monitoring."
# Test SMTP configuration
print_info " Sending test email to $SMTP_TO ... "
if command -v swaks >/dev/null; then
if swaks --to " $SMTP_TO " --from " $SMTP_FROM " --server " $SMTP_SERVER " --port " $SMTP_PORT " --auth-user " $SMTP_USER " --auth-password " $SMTP_PASS " --tls --silent 2 --body " Test email from $( hostname) at $( date) " --subject "Test Alert" >> " $LOG_FILE " 2>& 1; then
print_success " SMTP test email sent to $SMTP_TO . "
2025-06-26 16:49:03 +01:00
else
2025-06-26 17:12:59 +01:00
print_error "Failed to send test email. Check /var/log/mail.log for details."
exit 1
2025-06-26 16:49:03 +01:00
fi
2025-06-26 20:03:08 +01:00
elif echo " Test email from $( hostname) at $( date) " | mail -s "Test Alert" " $SMTP_TO " ; then
2025-06-26 20:32:02 +01:00
sleep 2
if tail -n 50 /var/log/mail.log | grep -qE "status=(sent|delivered|completed)" ; then
print_success " SMTP test email sent to $SMTP_TO . "
else
print_error "Failed to send test email. Check /var/log/mail.log for details."
exit 1
fi
2025-06-26 16:49:03 +01:00
fi
2025-06-26 17:12:59 +01:00
log "SMTP monitoring configured."
else
print_info "Skipping SMTP monitoring configuration."
SKIPPED_SETTINGS += ( "SMTP monitoring" )
fi
if [ [ -n " ${ NTFY_SERVER :- } " && -n " ${ NTFY_TOKEN :- } " ] ] ; then
print_info "Configuring ntfy monitoring..."
if curl -s -H " Authorization: Bearer $NTFY_TOKEN " -d " Test notification from $( hostname) at $( date) " " $NTFY_SERVER " >/dev/null; then
print_success "Test ntfy notification sent."
2025-06-26 16:49:03 +01:00
else
2025-06-26 17:12:59 +01:00
print_error "Failed to send test ntfy notification. Check URL and token."
exit 1
2025-06-26 16:49:03 +01:00
fi
2025-06-26 17:12:59 +01:00
log "ntfy monitoring configured."
else
print_info "Skipping ntfy monitoring configuration."
SKIPPED_SETTINGS += ( "ntfy monitoring" )
fi
# Configure monitoring cron job
print_info "Configuring monitoring cron job..."
mkdir -p /root/backup && chmod 700 /root/backup
NEW_CRON_CONFIG = $( mktemp)
tee " $NEW_CRON_CONFIG " > /dev/null <<'EOF'
# Disk space monitoring (alert on >80% usage)
0 * * * * root df -h | grep '^/dev/' | awk '{if ($5+0 > 80) print "Disk usage alert on " $1 ": " $5 " used";}' | while read -r line; do
[ [ -n " ${ SMTP_TO :- } " ] ] && echo " $line " | mail -s " Disk Usage Alert: $( hostname) " " ${ SMTP_TO :- nobody } " ;
[ [ -n " ${ NTFY_SERVER :- } " && -n " ${ NTFY_TOKEN :- } " ] ] && curl -s -H " Authorization: Bearer ${ NTFY_TOKEN :- } " -d " $line " " ${ NTFY_SERVER :- } " ;
done
# Backup monitoring
0 1 * * * root mkdir -p /root/backup && rsync -a --delete --exclude '/root/backup' / /root/backup && find /root/backup -mtime +7 -delete
EOF
if [ [ -f /etc/cron.d/system-monitoring ] ] && cmp -s /etc/cron.d/system-monitoring " $NEW_CRON_CONFIG " ; then
print_info "Monitoring cron job already configured."
2025-06-26 16:49:03 +01:00
rm -f " $NEW_CRON_CONFIG "
else
2025-06-26 17:12:59 +01:00
mv " $NEW_CRON_CONFIG " /etc/cron.d/system-monitoring
chmod 644 /etc/cron.d/system-monitoring
systemctl restart cron
print_success "Monitoring cron job configured."
2025-06-26 16:49:03 +01:00
fi
2025-06-26 17:12:59 +01:00
log "Monitoring configuration completed."
2025-06-26 20:32:02 +01:00
}
2025-06-26 16:49:03 +01:00
2025-06-26 17:12:59 +01:00
# Install Docker Engine
2025-06-26 16:49:03 +01:00
install_docker( ) {
2025-06-26 17:12:59 +01:00
print_section "Docker Installation"
2025-06-26 16:49:03 +01:00
if [ [ " $INSTALL_DOCKER " != "yes" ] ] ; then
print_info "Skipping Docker installation."
SKIPPED_SETTINGS += ( "Docker installation" )
return 0
fi
2025-06-26 17:12:59 +01:00
if command -v docker >/dev/null; then
print_info "Docker is already installed."
2025-06-26 16:49:03 +01:00
return 0
fi
print_info "Installing Docker..."
2025-06-26 17:12:59 +01:00
if ! curl -fsSL https://get.docker.com | sh; then
2025-06-26 16:49:03 +01:00
print_error "Failed to install Docker."
exit 1
fi
2025-06-26 17:12:59 +01:00
if ! usermod -aG docker " $USERNAME " ; then
print_error " Failed to add $USERNAME to docker group. "
2025-06-26 16:49:03 +01:00
exit 1
fi
2025-06-26 17:12:59 +01:00
print_success " Docker installed and $USERNAME added to docker group. "
log "Docker installation completed."
2025-06-26 16:49:03 +01:00
}
2025-06-26 17:12:59 +01:00
# Install Tailscale VPN
2025-06-26 16:22:23 +01:00
install_tailscale( ) {
2025-06-26 17:12:59 +01:00
print_section "Tailscale Installation"
2025-06-26 16:22:23 +01:00
if [ [ " $INSTALL_TAILSCALE " != "yes" ] ] ; then
print_info "Skipping Tailscale installation."
SKIPPED_SETTINGS += ( "Tailscale installation" )
return 0
fi
2025-06-26 17:12:59 +01:00
if command -v tailscale >/dev/null; then
print_info "Tailscale is already installed."
2025-06-26 16:22:23 +01:00
return 0
fi
2025-06-26 17:12:59 +01:00
print_info "Installing Tailscale using official install script..."
if ! curl -fsSL https://tailscale.com/install.sh | sh; then
2025-06-26 16:22:23 +01:00
print_error "Failed to install Tailscale."
exit 1
fi
2025-06-26 17:12:59 +01:00
print_success "Tailscale installed."
2025-06-26 16:22:23 +01:00
if [ [ -n " ${ TAILSCALE_LOGIN_SERVER :- } " && -n " ${ TAILSCALE_AUTH_KEY :- } " && -n " ${ TAILSCALE_OPERATOR :- } " ] ] ; then
2025-06-26 17:12:59 +01:00
print_info "Configuring Tailscale..."
local up_args = " --authkey= $TAILSCALE_AUTH_KEY --operator= $TAILSCALE_OPERATOR "
[ [ -n " $TAILSCALE_LOGIN_SERVER " ] ] && up_args = " $up_args --login-server= $TAILSCALE_LOGIN_SERVER "
[ [ " $TAILSCALE_ACCEPT_DNS " = = "yes" ] ] && up_args = " $up_args --accept-dns=true " || up_args = " $up_args --accept-dns=false "
[ [ " $TAILSCALE_ACCEPT_ROUTES " = = "yes" ] ] && up_args = " $up_args --accept-routes=true " || up_args = " $up_args --accept-routes=false "
2025-06-26 20:03:08 +01:00
if tailscale up $up_args && tailscale status >/dev/null 2>& 1; then
2025-06-26 20:32:02 +01:00
print_success "Tailscale configured and started."
else
print_error "Failed to configure Tailscale. Check 'tailscale status'."
exit 1
fi
2025-06-26 16:22:23 +01:00
else
2025-06-26 17:12:59 +01:00
print_warning "Tailscale installed but not configured. Run 'sudo tailscale up' manually."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 17:12:59 +01:00
log "Tailscale installation completed."
2025-06-26 16:22:23 +01:00
}
2025-06-26 17:12:59 +01:00
# Configure swap file
2025-06-26 16:22:23 +01:00
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
2025-06-26 17:12:59 +01:00
print_info "No swap size specified. Skipping."
2025-06-26 16:22:23 +01:00
SKIPPED_SETTINGS += ( "swap configuration" )
return 0
fi
2025-06-26 17:12:59 +01:00
if [ [ -f /swapfile ] ] ; then
print_info "Swap file already exists."
2025-06-26 16:22:23 +01:00
return 0
fi
2025-06-26 17:12:59 +01:00
print_info " Creating swap file of size $SWAP_SIZE ... "
2025-06-26 16:49:03 +01:00
local swap_size_bytes = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 17:12:59 +01:00
local available_space = $( df / | awk 'NR==2 {print $4 * 1024}' ) # Convert to bytes
if [ [ $available_space -lt $swap_size_bytes ] ] ; then
print_error " Insufficient disk space for $SWAP_SIZE swap. Available: $(( available_space / 1024 / 1024 )) M. "
exit 1
2025-06-26 16:22:23 +01:00
fi
2025-06-26 17:12:59 +01:00
if ! fallocate -l " $SWAP_SIZE " /swapfile; then
print_error "Failed to create swap file."
2025-06-26 16:22:23 +01:00
exit 1
fi
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
2025-06-26 17:12:59 +01:00
cp /etc/fstab " $BACKUP_DIR /fstab.backup_ $( date +%Y%m%d_%H%M%S) " && chmod 600 " $BACKUP_DIR /fstab.backup_ $( date +%Y%m%d_%H%M%S) "
if ! grep -q "/swapfile" /etc/fstab; then
2025-06-26 16:22:23 +01:00
echo "/swapfile none swap sw 0 0" >> /etc/fstab
fi
2025-06-26 17:12:59 +01:00
print_success "Swap file configured."
2025-06-26 16:22:23 +01:00
log "Swap configuration completed."
}
2025-06-26 17:12:59 +01:00
# Configure time synchronization
2025-06-26 16:22:23 +01:00
configure_time( ) {
print_section "Time Synchronization"
2025-06-26 17:12:59 +01:00
if ! systemctl is-active --quiet chrony; then
systemctl enable chrony
systemctl start chrony
2025-06-26 16:22:23 +01:00
fi
2025-06-26 17:12:59 +01:00
if chronyc tracking >/dev/null 2>& 1; then
print_success "Time synchronization configured with chrony."
2025-06-26 16:22:23 +01:00
else
2025-06-26 17:12:59 +01:00
print_error "Failed to verify chrony status."
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-26 17:12:59 +01:00
# Clean up temporary files and package cache
2025-06-26 16:22:23 +01:00
cleanup( ) {
2025-06-26 16:49:03 +01:00
print_section "Cleanup"
2025-06-26 17:12:59 +01:00
print_info "Cleaning up package cache..."
apt-get autoremove -y -qq
apt-get autoclean -qq
rm -rf /tmp/*
2025-06-26 16:49:03 +01:00
print_success "Cleanup completed."
log "Cleanup completed."
2025-06-26 16:22:23 +01:00
}
2025-06-26 17:12:59 +01:00
# Print final summary and next steps
2025-06-26 16:22:23 +01:00
print_summary( ) {
print_section "Setup Summary"
2025-06-26 17:12:59 +01:00
echo -e " ${ GREEN } Setup completed successfully! ${ NC } "
echo -e " \n ${ YELLOW } System Details: ${ NC } "
echo -e " Hostname: $SERVER_NAME "
echo -e " Pretty Name: $PRETTY_NAME "
echo -e " SSH Port: $SSH_PORT "
echo -e " Admin User: $USERNAME "
echo -e " Server IP: $SERVER_IP "
echo -e " Timezone: $TIMEZONE "
if [ [ -n " ${ SWAP_SIZE :- } " && " $IS_CONTAINER " = = false ] ] ; then
echo -e " Swap Size: $SWAP_SIZE "
fi
if [ [ -n " ${ UFW_PORTS :- } " ] ] ; then
echo -e " Custom Ports: ${ UFW_PORTS //,/ } "
fi
if [ [ " $AUTO_UPDATES " = = "yes" ] ] ; then
echo -e " Auto Updates: Enabled"
fi
if [ [ " $INSTALL_DOCKER " = = "yes" ] ] ; then
echo -e " Docker: Installed"
fi
if [ [ " $INSTALL_TAILSCALE " = = "yes" ] ] ; then
echo -e " Tailscale: Installed"
if [ [ -n " ${ TAILSCALE_LOGIN_SERVER :- } " ] ] ; then
echo -e " Tailscale LS: $TAILSCALE_LOGIN_SERVER "
fi
fi
if [ [ -n " ${ SMTP_SERVER :- } " ] ] ; then
echo -e " SMTP Alerts: Configured ( $SMTP_TO ) "
fi
if [ [ -n " ${ NTFY_SERVER :- } " ] ] ; then
echo -e " ntfy Alerts: Configured ( $NTFY_SERVER ) "
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 "
2025-06-26 17:12:59 +01:00
echo -e " \n ${ YELLOW } Backups: ${ NC } $BACKUP_DIR "
2025-06-26 16:49:03 +01:00
echo -e " \n ${ YELLOW } Next Steps: ${ NC } "
2025-06-26 17:12:59 +01:00
echo -e " - Verify SSH access: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP "
echo -e " - Check UFW status: sudo ufw status"
echo -e " - Check Fail2Ban: sudo fail2ban-client status sshd"
if [ [ " $INSTALL_DOCKER " = = "yes" ] ] ; then
echo -e " - Test Docker: sudo -u $USERNAME docker run hello-world "
fi
if [ [ " $INSTALL_TAILSCALE " = = "yes" ] ] ; then
echo -e " - Check Tailscale: sudo tailscale status"
fi
echo -e " - Review logs: less $LOG_FILE "
echo -e " \n ${ GREEN } Thank you for using the Debian/Ubuntu Setup and Hardening Script! ${ NC } "
log "Setup summary printed."
2025-06-26 16:22:23 +01:00
}
2025-06-26 17:12:59 +01:00
# Main function to orchestrate setup
2025-06-26 16:22:23 +01:00
main( ) {
print_header
2025-06-26 17:12:59 +01:00
mkdir -p " $( dirname " $LOG_FILE " ) "
touch " $LOG_FILE "
chmod 600 " $LOG_FILE "
log "Script started."
2025-06-26 16:22:23 +01:00
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 17:12:59 +01:00
log "Script completed successfully."
2025-06-26 16:22:23 +01:00
}
2025-06-26 16:49:03 +01:00
# Execute main function
2025-06-26 20:11:59 +01:00
main " $@ "