2025-06-26 16:22:23 +01:00
#!/bin/bash
2025-06-26 21:49:51 +01:00
# Debian 12 and Ubuntu Server Hardening Interactive Script
2025-06-26 22:19:35 +01:00
# Version: 3.9 | 2025-06-26
2025-06-26 21:49:51 +01:00
# Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS
2025-06-26 16:22:23 +01:00
#
2025-06-26 21:49:51 +01:00
# Description:
# This script provisions and hardens a fresh Debian 12 or Ubuntu server with essential security
# configurations, user management, SSH hardening, firewall setup, and optional features
# like Docker and Tailscale. It is designed to be idempotent, safe, and suitable for
# production environments.
#
# Prerequisites:
# - Run as root on a fresh Debian 12 or Ubuntu server (e.g., sudo ./setup_harden_debian_ubuntu.sh).
# - Internet connectivity is required for package installation.
#
# Usage:
2025-06-26 22:19:35 +01:00
# Download: wget https://raw.githubusercontent.com/buildplan/setup_harden_server/refs/heads/main/setup_harden_debian_ubuntu.sh
2025-06-26 21:49:51 +01:00
# Make it executable: chmod +x setup_harden_debian_ubuntu.sh
# Run it: sudo ./setup_harden_debian_ubuntu.sh [--quiet]
#
# Options:
# --quiet: Suppress non-critical output for automation.
#
# Notes:
# - The script creates a log file in /var/log/setup_harden_debian_ubuntu_*.log.
# - Critical configurations are backed up before modification. Backup files are at /root/setup_harden_backup_*.
# - A new admin user is created with a mandatory password or SSH key for authentication.
# - Root SSH login is disabled; all access is via the new user with sudo privileges.
# - The user will be prompted to select a timezone, swap size, and custom firewall ports.
# - A reboot is recommended at the end to apply all changes.
# - Test the script in a VM before production use.
#
# Troubleshooting:
# - Check the log file for errors if the script fails.
# - If SSH access is lost, use the server console to restore /etc/ssh/sshd_config.backup_*.
# - Ensure sufficient disk space (>2GB) for swap file creation.
2025-06-26 16:22:23 +01:00
set -euo pipefail # Exit on error, undefined vars, pipe failures
# --- GLOBAL VARIABLES & CONFIGURATION ---
# Colors for output
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
PURPLE = '\033[0;35m'
CYAN = '\033[0;36m'
NC = '\033[0m' # No Color
# Script variables
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
LOG_FILE = " /var/log/setup_harden_debian_ubuntu_ $( date +%Y%m%d_%H%M%S) .log "
VERBOSE = true
BACKUP_DIR = " /root/setup_harden_backup_ $( date +%Y%m%d_%H%M%S) "
IS_CONTAINER = false
SSHD_BACKUP_FILE = ""
LOCAL_KEY_ADDED = false
SSH_SERVICE = ""
2025-06-26 21:49:51 +01:00
ID = "" # This will be populated from /etc/os-release
2025-06-26 16:22:23 +01:00
# --- PARSE ARGUMENTS ---
while [ [ $# -gt 0 ] ] ; do
case $1 in
--quiet) VERBOSE = false; shift ; ;
*) shift ; ;
esac
done
# --- LOGGING & PRINT FUNCTIONS ---
log( ) {
echo " $( date '+%Y-%m-%d %H:%M:%S' ) - $1 " >> " $LOG_FILE "
}
print_header( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ CYAN } ╔═════════════════════════════════════════════════════════════════╗ ${ NC } "
echo -e " ${ CYAN } ║ ║ ${ NC } "
2025-06-26 22:19:35 +01:00
echo -e " ${ CYAN } ║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║ ${ NC } "
echo -e " ${ CYAN } ║ v3.9 | 2025-06-26 ║ ${ NC } "
2025-06-26 16:22:23 +01:00
echo -e " ${ CYAN } ╚═════════════════════════════════════════════════════════════════╝ ${ NC } "
echo
}
print_section( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " \n ${ BLUE } ▓▓▓ $1 ▓▓▓ ${ NC } " | tee -a " $LOG_FILE "
echo -e " ${ BLUE } $( printf '═%.0s' { 1..65} ) ${ NC } "
}
print_success( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ GREEN } ✓ $1 ${ NC } " | tee -a " $LOG_FILE "
}
print_error( ) {
echo -e " ${ RED } ✗ $1 ${ NC } " | tee -a " $LOG_FILE "
}
print_warning( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ YELLOW } ⚠ $1 ${ NC } " | tee -a " $LOG_FILE "
}
print_info( ) {
[ [ $VERBOSE = = false ] ] && return
echo -e " ${ PURPLE } ℹ $1 ${ NC } " | tee -a " $LOG_FILE "
}
2025-06-26 21:49:51 +01:00
# --- USER INTERACTION ---
2025-06-26 16:22:23 +01:00
confirm( ) {
local prompt = " $1 "
local default = " ${ 2 :- n } "
local response
[ [ $VERBOSE = = false ] ] && return 0
if [ [ $default = = "y" ] ] ; then
prompt = " $prompt [Y/n]: "
else
prompt = " $prompt [y/N]: "
fi
while true; do
read -rp " $( echo -e " ${ CYAN } $prompt ${ NC } " ) " response
response = ${ response ,, }
if [ [ -z $response ] ] ; then
response = $default
fi
case $response in
y| yes) return 0 ; ;
n| no) return 1 ; ;
*) echo -e " ${ RED } Please answer yes or no. ${ NC } " ; ;
esac
done
}
# --- VALIDATION FUNCTIONS ---
validate_username( ) {
local username = " $1 "
[ [ " $username " = ~ ^[ a-z_] [ a-z0-9_-] *$ && ${# username } -le 32 ] ]
}
validate_hostname( ) {
local hostname = " $1 "
[ [ " $hostname " = ~ ^[ a-zA-Z0-9] [ a-zA-Z0-9.-] { 0,253} [ a-zA-Z0-9] $ && ! " $hostname " = ~ \. \. ] ]
}
validate_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " -ge 1024 && " $port " -le 65535 ] ]
}
validate_ssh_key( ) {
local key = " $1 "
[ [ -n " $key " && " $key " = ~ ^( ssh-rsa| ecdsa-sha2-nistp256| ecdsa-sha2-nistp384| ecdsa-sha2-nistp521| ssh-ed25519) \ ] ]
}
validate_timezone( ) {
local tz = " $1 "
[ [ -e " /usr/share/zoneinfo/ $tz " ] ]
}
validate_swap_size( ) {
2025-06-26 22:19:35 +01:00
local size_upper = " ${ 1 ^^ } " # Convert to uppercase for case-insensitivity
[ [ " $size_upper " = ~ ^[ 0-9] +[ MG] $ && " ${ size_upper %[MG] } " -ge 1 ] ]
2025-06-26 16:22:23 +01:00
}
validate_ufw_port( ) {
local port = " $1 "
2025-06-26 21:49:51 +01:00
# Matches port (e.g., 8080) or port/protocol (e.g., 8080/tcp, 123/udp)
2025-06-26 16:22:23 +01:00
[ [ " $port " = ~ ^[ 0-9] +( /tcp| /udp) ?$ ] ]
}
convert_to_bytes( ) {
2025-06-26 22:19:35 +01:00
local size_upper = " ${ 1 ^^ } " # Convert to uppercase for case-insensitivity
local unit = " ${ size_upper : -1 } "
local value = " ${ size_upper %[MG] } "
2025-06-26 16:22:23 +01:00
if [ [ " $unit " = = "G" ] ] ; then
echo $(( value * 1024 * 1024 * 1024 ))
elif [ [ " $unit " = = "M" ] ] ; then
echo $(( value * 1024 * 1024 ))
else
echo 0
fi
}
2025-06-26 21:49:51 +01:00
# --- CORE FUNCTIONS ---
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
check_dependencies( ) {
print_section "Checking Dependencies"
local missing_deps = ( )
command -v curl >/dev/null || missing_deps += ( "curl" )
command -v sudo >/dev/null || missing_deps += ( "sudo" )
command -v gpg >/dev/null || missing_deps += ( "gpg" )
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
if [ [ ${# missing_deps [@] } -gt 0 ] ] ; then
print_info " Installing missing dependencies: ${ missing_deps [*] } "
if ! apt-get update -qq || ! apt-get install -y -qq " ${ missing_deps [@] } " ; then
print_error " Failed to install dependencies: ${ missing_deps [*] } "
exit 1
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
print_success "Dependencies installed."
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
print_success "All essential dependencies are installed."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
log "Dependency check completed."
2025-06-26 16:22:23 +01:00
}
check_system( ) {
print_section "System Compatibility Check"
if [ [ $( id -u) -ne 0 ] ] ; then
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
if [ [ $ID = = "debian" && $VERSION_ID = = "12" ] ] || \
2025-06-26 21:49:51 +01:00
[ [ $ID = = "ubuntu" && $VERSION_ID = ~ ^( 20.04| 22.04| 24.04) $ ] ] ; then
2025-06-26 16:22:23 +01:00
print_success " Compatible OS detected: $PRETTY_NAME "
else
print_warning " Script not tested on $PRETTY_NAME . This is for Debian 12 or Ubuntu 20.04/22.04/24.04 LTS. "
if ! confirm "Continue anyway?" ; then exit 1; fi
fi
else
print_error "This does not appear to be a Debian or Ubuntu system."
exit 1
fi
2025-06-26 21:49:51 +01:00
# Preliminary SSH service check
2025-06-26 16:22:23 +01:00
if ! dpkg -l openssh-server | grep -q ^ii; then
print_warning "openssh-server not installed. It will be installed in the next step."
2025-06-26 21:49:51 +01:00
else
2025-06-26 16:22:23 +01:00
if systemctl is-enabled ssh.service >/dev/null 2>& 1 || systemctl is-active ssh.service >/dev/null 2>& 1; then
print_info "Preliminary check: ssh.service detected."
elif systemctl is-enabled sshd.service >/dev/null 2>& 1 || systemctl is-active sshd.service >/dev/null 2>& 1; then
print_info "Preliminary check: sshd.service detected."
2025-06-26 21:49:51 +01:00
elif ps aux | grep -q "[s]shd" ; then
print_warning "Preliminary check: SSH daemon running but no standard service detected."
2025-06-26 16:22:23 +01:00
else
print_warning "No SSH service or daemon detected. Ensure SSH is working after package installation."
fi
fi
if curl -s --head https://deb.debian.org >/dev/null || curl -s --head https://archive.ubuntu.com >/dev/null; then
print_success "Internet connectivity confirmed."
else
print_error "No internet connectivity. Please check your network."
exit 1
fi
if [ [ ! -w /var/log ] ] ; then
print_error "Failed to write to /var/log. Cannot create log file."
exit 1
fi
2025-06-26 21:49:51 +01:00
# Check /etc/shadow permissions
if [ [ ! -w /etc/shadow ] ] ; then
print_error "/etc/shadow is not writable. Check permissions (should be 640, root:shadow)."
exit 1
fi
2025-06-26 22:19:35 +01:00
local SHADOW_PERMS
2025-06-26 16:22:23 +01:00
SHADOW_PERMS = $( stat -c %a /etc/shadow)
if [ [ " $SHADOW_PERMS " != "640" ] ] ; then
print_info "Fixing /etc/shadow permissions to 640..."
chmod 640 /etc/shadow
chown root:shadow /etc/shadow
log "Fixed /etc/shadow permissions to 640."
fi
log "System compatibility check completed."
}
collect_config( ) {
print_section "Configuration Setup"
2025-06-26 21:49:51 +01:00
while true; do
read -rp " $( echo -e " ${ CYAN } Enter username for new admin user: ${ NC } " ) " USERNAME
if validate_username " $USERNAME " ; then
if id " $USERNAME " & >/dev/null; then
print_warning " User ' $USERNAME ' already exists. "
if confirm "Use this existing user?" ; then USER_EXISTS = true; break; fi
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
USER_EXISTS = false; break
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
else
print_error "Invalid username. Use lowercase letters, numbers, hyphens, underscores (max 32 chars)."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
done
while true; do
read -rp " $( echo -e " ${ CYAN } Enter server hostname: ${ NC } " ) " SERVER_NAME
if validate_hostname " $SERVER_NAME " ; then break; else print_error "Invalid hostname." ; fi
done
read -rp " $( echo -e " ${ CYAN } Enter a 'pretty' hostname (optional): ${ NC } " ) " PRETTY_NAME
[ [ -z " $PRETTY_NAME " ] ] && PRETTY_NAME = " $SERVER_NAME "
while true; do
read -rp " $( echo -e " ${ CYAN } Enter custom SSH port (1024-65535) [2222]: ${ NC } " ) " SSH_PORT
SSH_PORT = ${ SSH_PORT :- 2222 }
if validate_port " $SSH_PORT " ; then break; else print_error "Invalid port number." ; fi
done
2025-06-26 16:22:23 +01:00
SERVER_IP = $( curl -s https://ifconfig.me 2>/dev/null || echo "unknown" )
print_info " Detected server IP: $SERVER_IP "
echo -e " \n ${ YELLOW } Configuration Summary: ${ NC } "
2025-06-26 22:19:35 +01:00
echo -e " Username: $USERNAME "
echo -e " Hostname: $SERVER_NAME "
echo -e " SSH Port: $SSH_PORT "
echo -e " Server IP: $SERVER_IP "
2025-06-26 21:49:51 +01:00
if ! confirm "\nContinue with this configuration?" "y" ; then print_info "Exiting." ; exit 0; fi
2025-06-26 16:22:23 +01:00
log " Configuration collected: USER= $USERNAME , HOST= $SERVER_NAME , PORT= $SSH_PORT "
}
install_packages( ) {
print_section "Package Installation"
print_info "Updating package lists and upgrading system..."
if ! apt-get update -qq || ! DEBIAN_FRONTEND = noninteractive apt-get upgrade -y -qq; then
print_error "Failed to update or upgrade system packages."
exit 1
fi
print_info "Installing essential packages..."
if ! apt-get install -y -qq \
ufw fail2ban unattended-upgrades chrony \
rsync wget vim htop iotop nethogs ncdu tree \
rsyslog cron jq gawk coreutils perl skopeo git \
2025-06-26 21:49:51 +01:00
openssh-client openssh-server; then
2025-06-26 16:22:23 +01:00
print_error "Failed to install one or more essential packages."
exit 1
fi
print_success "Essential packages installed."
log "Package installation completed."
}
setup_user( ) {
print_section "User Management"
2025-06-26 22:19:35 +01:00
local USER_HOME SSH_DIR AUTH_KEYS PASS1 PASS2 SSH_PUBLIC_KEY
2025-06-26 16:22:23 +01:00
if [ [ $USER_EXISTS = = false ] ] ; then
print_info " Creating user ' $USERNAME '... "
if ! adduser --disabled-password --gecos "" " $USERNAME " ; then
print_error " Failed to create user ' $USERNAME '. "
exit 1
fi
if ! id " $USERNAME " & >/dev/null; then
print_error " User ' $USERNAME ' creation verification failed. "
exit 1
fi
2025-06-26 22:19:35 +01:00
print_info " Set a password for ' $USERNAME ' (required for sudo, or press Enter twice to skip for key-only access): "
2025-06-26 16:22:23 +01:00
while true; do
read -sp " $( echo -e " ${ CYAN } New password: ${ NC } " ) " PASS1
echo
read -sp " $( echo -e " ${ CYAN } Retype new password: ${ NC } " ) " PASS2
echo
if [ [ -z " $PASS1 " && -z " $PASS2 " ] ] ; then
print_warning "Password skipped. Relying on SSH key authentication."
log " Password setting skipped for ' $USERNAME '. "
break
elif [ [ " $PASS1 " = = " $PASS2 " ] ] ; then
2025-06-26 22:19:35 +01:00
# **SECURITY FIX**: Do not tee chpasswd output to log file.
if echo " $USERNAME : $PASS1 " | chpasswd >/dev/null 2>& 1; then
2025-06-26 16:22:23 +01:00
print_success " Password for ' $USERNAME ' updated. "
break
else
2025-06-26 22:19:35 +01:00
print_error "Failed to set password. This could be a permissions issue."
2025-06-26 16:22:23 +01:00
print_info "Try again or press Enter twice to skip."
log " Failed to set password for ' $USERNAME '. "
fi
else
print_error "Passwords do not match. Please try again."
fi
done
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
AUTH_KEYS = " $SSH_DIR /authorized_keys "
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 "
2025-06-26 22:19:35 +01:00
# De-duplicate keys
2025-06-26 16:22:23 +01:00
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 "
2025-06-26 21:49:51 +01:00
cp /etc/hosts " $BACKUP_DIR /hosts.backup "
cp /etc/fstab " $BACKUP_DIR /fstab.backup "
cp /etc/sysctl.conf " $BACKUP_DIR /sysctl.conf.backup " 2>/dev/null || true
2025-06-26 16:22:23 +01:00
print_info "Configuring timezone..."
2025-06-26 21:49:51 +01:00
while true; do
read -rp " $( echo -e " ${ CYAN } Enter desired timezone (e.g., Etc/UTC, America/New_York) [Etc/UTC]: ${ NC } " ) " TIMEZONE
TIMEZONE = ${ TIMEZONE :- Etc /UTC }
if validate_timezone " $TIMEZONE " ; then
if [ [ $( timedatectl status | grep "Time zone" | awk '{print $3}' ) != " $TIMEZONE " ] ] ; then
timedatectl set-timezone " $TIMEZONE "
print_success " Timezone set to $TIMEZONE . "
log " Timezone set to $TIMEZONE . "
else
print_info " Timezone already set to $TIMEZONE . "
fi
break
else
print_error "Invalid timezone. View list with 'timedatectl list-timezones'."
fi
done
2025-06-26 16:22:23 +01:00
if confirm "Configure system locales interactively?" ; then
dpkg-reconfigure locales
else
print_info "Skipping locale configuration."
fi
print_info "Configuring hostname..."
if [ [ $( hostnamectl --static) != " $SERVER_NAME " ] ] ; then
hostnamectl set-hostname " $SERVER_NAME "
hostnamectl set-hostname " $PRETTY_NAME " --pretty
if grep -q "^127.0.1.1" /etc/hosts; then
sed -i " s/^127.0.1.1.*/127.0.1.1\t $SERVER_NAME / " /etc/hosts
else
echo " 127.0.1.1 $SERVER_NAME " >> /etc/hosts
fi
print_success " Hostname configured: $SERVER_NAME "
else
print_info " Hostname already set to $SERVER_NAME . "
fi
log "System configuration completed."
}
configure_ssh( ) {
print_section "SSH Hardening"
2025-06-26 22:19:35 +01:00
local CURRENT_SSH_PORT USER_HOME SSH_DIR SSH_KEY AUTH_KEYS NEW_SSH_CONFIG
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
# Ensure openssh-server is installed
2025-06-26 16:22:23 +01:00
if ! dpkg -l openssh-server | grep -q ^ii; then
print_error "openssh-server package is not installed. Please ensure it is installed."
exit 1
fi
2025-06-26 21:49:51 +01:00
# Detect SSH service name and handle socket activation
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
2025-06-26 16:22:23 +01:00
else
2025-06-26 22:19:35 +01:00
print_error "No SSH service or daemon detected. Please verify openssh-server installation and daemon status."
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
print_info " Using SSH service: $SSH_SERVICE "
2025-06-26 16:22:23 +01:00
log " Detected SSH service: $SSH_SERVICE "
2025-06-26 21:49:51 +01:00
# Ensure SSH service is enabled and running
2025-06-26 16:22:23 +01:00
if ! systemctl is-enabled " $SSH_SERVICE " >/dev/null 2>& 1; then
if ! systemctl enable " $SSH_SERVICE " >/dev/null 2>& 1; then
print_error " Failed to enable $SSH_SERVICE . Please check service status. "
exit 1
fi
print_success " SSH service enabled: $SSH_SERVICE "
fi
if ! systemctl is-active " $SSH_SERVICE " >/dev/null 2>& 1; then
if ! systemctl start " $SSH_SERVICE " >/dev/null 2>& 1; then
print_error " Failed to start $SSH_SERVICE . Attempting manual start... "
if ! /usr/sbin/sshd; then
print_error "Failed to start SSH daemon manually."
exit 1
fi
print_success "SSH daemon started manually."
fi
fi
CURRENT_SSH_PORT = $( ss -tuln | grep -E " :(22|.* $SSH_SERVICE .*) " | awk '{print $5}' | cut -d':' -f2 | head -n1 || echo "22" )
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
SSH_KEY = " $SSH_DIR /id_ed25519 "
AUTH_KEYS = " $SSH_DIR /authorized_keys "
2025-06-26 21:49:51 +01:00
if [ [ $LOCAL_KEY_ADDED = = false ] ] && [ [ ! -s " $AUTH_KEYS " ] ] ; then
2025-06-26 16:22:23 +01:00
print_info "No local key provided and no existing keys found. Generating new SSH key..."
mkdir -p " $SSH_DIR "
chmod 700 " $SSH_DIR "
sudo -u " $USERNAME " ssh-keygen -t ed25519 -f " $SSH_KEY " -N "" -q
cat " $SSH_KEY .pub " >> " $AUTH_KEYS "
chmod 600 " $AUTH_KEYS "
chown -R " $USERNAME : $USERNAME " " $SSH_DIR "
print_success "SSH key generated."
echo -e " ${ YELLOW } Public key for remote access: ${ NC } "
cat " $SSH_KEY .pub " | tee -a " $LOG_FILE "
echo -e " ${ YELLOW } Copy this key to your local ~/.ssh/authorized_keys or use 'ssh-copy-id -p $CURRENT_SSH_PORT $USERNAME @ $SERVER_IP ' from your local machine. ${ NC } "
else
print_info "SSH key(s) already present or added. Skipping key generation."
fi
print_warning "SSH Key Authentication Required for Next Steps!"
echo -e " ${ CYAN } Test SSH access from a SEPARATE terminal now: ssh -p $CURRENT_SSH_PORT $USERNAME @ $SERVER_IP ${ NC } "
2025-06-26 21:49:51 +01:00
2025-06-26 16:22:23 +01:00
if ! confirm "Can you successfully log in using your SSH key?" ; then
print_error "SSH key authentication is mandatory to proceed. Please fix and re-run."
exit 1
fi
print_info "Backing up original SSH config..."
SSHD_BACKUP_FILE = " $BACKUP_DIR /sshd_config.backup_ $( date +%Y%m%d_%H%M%S) "
2025-06-26 21:49:51 +01:00
cp /etc/ssh/sshd_config " $SSHD_BACKUP_FILE "
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
# Check if SSH config needs updating
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 22:19:35 +01:00
mkdir -p /etc/ssh/sshd_config.d
2025-06-26 17:12:59 +01:00
mv " $NEW_SSH_CONFIG " /etc/ssh/sshd_config.d/99-hardening.conf
2025-06-26 16:22:23 +01:00
chmod 644 /etc/ssh/sshd_config.d/99-hardening.conf
tee /etc/issue.net > /dev/null <<'EOF'
******************************************************************************
AUTHORIZED ACCESS ONLY
2025-06-26 22:19:35 +01:00
════ all attempts are logged and reviewed ════
2025-06-26 16:22:23 +01:00
******************************************************************************
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
2025-06-26 22:19:35 +01:00
rm -f /etc/ssh/sshd_config.d/99-hardening.conf
2025-06-26 16:22:23 +01:00
systemctl restart " $SSH_SERVICE " || /usr/sbin/sshd || true
exit 1
fi
2025-06-26 22:19:35 +01:00
# Wait a moment for the service to potentially fail
sleep 2
2025-06-26 16:22:23 +01:00
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
2025-06-26 22:19:35 +01:00
rm -f /etc/ssh/sshd_config.d/99-hardening.conf
2025-06-26 16:22:23 +01:00
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
rm -f " $NEW_SSH_CONFIG "
exit 1
fi
2025-06-26 21:49:51 +01:00
# Verify root SSH is disabled
2025-06-26 16:22:23 +01:00
print_info "Verifying root SSH login is disabled..."
if ssh -p " $SSH_PORT " -o BatchMode = yes -o ConnectTimeout = 5 root@localhost true 2>/dev/null; then
2025-06-26 22:19:35 +01:00
print_error "Root SSH login is still possible! Check SSH configuration."
2025-06-26 16:22:23 +01:00
exit 1
else
print_success "Confirmed: Root SSH login is disabled."
fi
print_warning "CRITICAL: Test new SSH connection in a SEPARATE terminal NOW!"
print_info " Use: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP "
2025-06-26 21:49:51 +01:00
2025-06-26 16:22:23 +01:00
if ! confirm "Was the new SSH connection successful?" ; then
print_error "Aborting. Restoring original SSH configuration."
cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config
2025-06-26 22:19:35 +01:00
rm -f /etc/ssh/sshd_config.d/99-hardening.conf
2025-06-26 16:22:23 +01:00
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
2025-06-26 21:49:51 +01:00
if confirm "Allow HTTP traffic (port 80)?" ; then
if ! ufw status | grep -qw "80/tcp" ; then
ufw allow http comment 'HTTP'
print_success "HTTP traffic allowed."
else
print_info "HTTP rule already exists."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
fi
if confirm "Allow HTTPS traffic (port 443)?" ; then
if ! ufw status | grep -qw "443/tcp" ; then
ufw allow https comment 'HTTPS'
print_success "HTTPS traffic allowed."
else
print_info "HTTPS rule already exists."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
fi
if confirm "Add additional custom ports (e.g., 8080/tcp, 123/udp)?" ; then
while true; do
2025-06-26 22:19:35 +01:00
local CUSTOM_PORTS # Make variable local to the loop
2025-06-26 21:49:51 +01:00
read -rp " $( echo -e " ${ CYAN } Enter ports (space-separated, e.g., 8080/tcp 123/udp): ${ NC } " ) " CUSTOM_PORTS
if [ [ -z " $CUSTOM_PORTS " ] ] ; then
print_info "No custom ports entered. Skipping."
break
fi
2025-06-26 22:19:35 +01:00
local valid = true
2025-06-26 21:49:51 +01:00
for port in $CUSTOM_PORTS ; do
if ! validate_ufw_port " $port " ; then
print_error " Invalid port format: $port . Use <port>[/tcp|/udp]. "
valid = false
2025-06-26 16:22:23 +01:00
break
fi
2025-06-26 21:49:51 +01:00
done
if [ [ " $valid " = = true ] ] ; then
for port in $CUSTOM_PORTS ; do
if ufw status | grep -qw " $port " ; then
print_info " Rule for $port already exists. "
else
ufw allow " $port " comment " Custom port $port "
print_success " Added rule for $port . "
log " Added UFW rule for $port . "
2025-06-26 16:22:23 +01:00
fi
done
2025-06-26 21:49:51 +01:00
break
else
print_info "Please try again."
fi
done
2025-06-26 16:22:23 +01:00
fi
print_info "Enabling firewall..."
if ! ufw --force enable; then
print_error "Failed to enable UFW. Check 'journalctl -u ufw' for details."
exit 1
fi
if ufw status | grep -q "Status: active" ; then
print_success "Firewall is active."
else
print_error "UFW failed to activate. Check 'journalctl -u ufw' for details."
exit 1
fi
print_warning " ACTION REQUIRED: Check your VPS provider's edge firewall to allow opened ports (e.g., $SSH_PORT /tcp). "
ufw status verbose | tee -a " $LOG_FILE "
log "Firewall configuration completed."
}
2025-06-26 16:49:03 +01:00
configure_fail2ban( ) {
print_section "Fail2Ban Configuration"
2025-06-26 22:10:35 +01:00
# Set the SSH port for Fail2Ban to monitor.
2025-06-26 22:19:35 +01:00
local SSH_PORTS_TO_MONITOR = " $SSH_PORT "
local NEW_FAIL2BAN_CONFIG
2025-06-26 22:10:35 +01:00
2025-06-26 21:49:51 +01:00
NEW_FAIL2BAN_CONFIG = $( mktemp)
tee " $NEW_FAIL2BAN_CONFIG " > /dev/null <<EOF
[ DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
backend = auto
2025-06-26 16:49:03 +01:00
[ sshd]
enabled = true
2025-06-26 22:10:35 +01:00
port = $SSH_PORTS_TO_MONITOR
2025-06-26 21:49:51 +01:00
logpath = %( sshd_log) s
backend = %( sshd_backend) s
2025-06-26 16:49:03 +01:00
EOF
2025-06-26 21:49:51 +01:00
if [ [ -f /etc/fail2ban/jail.local ] ] && cmp -s " $NEW_FAIL2BAN_CONFIG " /etc/fail2ban/jail.local; then
2025-06-26 16:49:03 +01:00
print_info "Fail2Ban configuration already correct. Skipping."
2025-06-26 21:49:51 +01:00
rm -f " $NEW_FAIL2BAN_CONFIG "
elif [ [ -f /etc/fail2ban/jail.local ] ] && grep -q "\[sshd\]" /etc/fail2ban/jail.local; then
2025-06-26 22:10:35 +01:00
print_info "Fail2Ban jail.local exists. Updating SSH port..."
sed -i " s/^\(port\s*=\s*\).*/\1 $SSH_PORTS_TO_MONITOR / " /etc/fail2ban/jail.local
2025-06-26 21:49:51 +01:00
rm -f " $NEW_FAIL2BAN_CONFIG "
2025-06-26 16:49:03 +01:00
else
2025-06-26 21:49:51 +01:00
print_info "Creating Fail2Ban local jail configuration..."
mv " $NEW_FAIL2BAN_CONFIG " /etc/fail2ban/jail.local
chmod 644 /etc/fail2ban/jail.local
fi
print_info "Enabling and restarting Fail2Ban..."
systemctl enable fail2ban
systemctl restart fail2ban
sleep 2
if systemctl is-active --quiet fail2ban; then
2025-06-26 22:10:35 +01:00
print_success " Fail2Ban is active and monitoring port(s) $SSH_PORTS_TO_MONITOR . "
2025-06-26 21:49:51 +01:00
fail2ban-client status sshd | tee -a " $LOG_FILE "
2025-06-26 16:49:03 +01:00
else
2025-06-26 21:49:51 +01:00
print_error "Fail2Ban service failed to start."
2025-06-26 16:49:03 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
log "Fail2Ban configuration completed."
2025-06-26 16:49:03 +01:00
}
2025-06-26 21:49:51 +01:00
configure_auto_updates( ) {
print_section "Automatic Security Updates"
if confirm "Enable automatic security updates via unattended-upgrades?" ; then
if ! dpkg -l unattended-upgrades | grep -q ^ii; then
print_error "unattended-upgrades package is not installed."
2025-06-26 17:12:59 +01:00
exit 1
2025-06-26 16:49:03 +01:00
fi
2025-06-26 21:49:51 +01:00
print_info "Configuring unattended upgrades..."
echo "unattended-upgrades unattended-upgrades/enable_auto_updates boolean true" | debconf-set-selections
DEBIAN_FRONTEND = noninteractive dpkg-reconfigure -f noninteractive unattended-upgrades
print_success "Automatic security updates enabled."
2025-06-26 16:49:03 +01:00
else
2025-06-26 21:49:51 +01:00
print_info "Skipping automatic security updates."
2025-06-26 16:49:03 +01:00
fi
2025-06-26 21:49:51 +01:00
log "Automatic updates configuration completed."
2025-06-26 20:32:02 +01:00
}
2025-06-26 16:49:03 +01:00
install_docker( ) {
2025-06-26 21:49:51 +01:00
if ! confirm "Install Docker Engine (Optional)?" ; then
2025-06-26 16:49:03 +01:00
print_info "Skipping Docker installation."
return 0
fi
2025-06-26 21:49:51 +01:00
print_section "Docker Installation"
if command -v docker >/dev/null 2>& 1; then
print_info "Docker already installed."
2025-06-26 16:49:03 +01:00
return 0
fi
2025-06-26 21:49:51 +01:00
print_info "Removing old container runtimes..."
apt-get remove -y -qq docker docker-engine docker.io containerd runc 2>/dev/null || true
print_info "Adding Docker's official GPG key and repository..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/${ ID } /gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo " deb [arch= $( dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ ${ ID } $( . /etc/os-release && echo " $VERSION_CODENAME " ) stable " > /etc/apt/sources.list.d/docker.list
print_info "Installing Docker packages..."
if ! apt-get update -qq || ! apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
print_error "Failed to install Docker packages."
2025-06-26 16:49:03 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
print_info " Adding ' $USERNAME ' to docker group... "
getent group docker >/dev/null || groupadd docker
if ! groups " $USERNAME " | grep -qw docker; then
usermod -aG docker " $USERNAME "
print_success " User ' $USERNAME ' added to docker group. "
else
print_info " User ' $USERNAME ' is already in docker group. "
fi
print_info "Configuring Docker daemon..."
2025-06-26 22:19:35 +01:00
local NEW_DOCKER_CONFIG
2025-06-26 21:49:51 +01:00
NEW_DOCKER_CONFIG = $( mktemp)
2025-06-26 22:19:35 +01:00
# **BUG FIX**: Corrected typo from >¼ to >
tee " $NEW_DOCKER_CONFIG " > /dev/null <<EOF
2025-06-26 21:49:51 +01:00
{
"log-driver" : "json-file" ,
"log-opts" : { "max-size" : "10m" , "max-file" : "3" } ,
"live-restore" : true
}
EOF
2025-06-26 22:19:35 +01:00
mkdir -p /etc/docker
2025-06-26 21:49:51 +01:00
if [ [ -f /etc/docker/daemon.json ] ] && cmp -s " $NEW_DOCKER_CONFIG " /etc/docker/daemon.json; then
print_info "Docker daemon configuration already correct. Skipping."
rm -f " $NEW_DOCKER_CONFIG "
else
mv " $NEW_DOCKER_CONFIG " /etc/docker/daemon.json
chmod 644 /etc/docker/daemon.json
fi
systemctl daemon-reload
systemctl enable --now docker
print_info "Running Docker sanity check..."
if sudo -u " $USERNAME " docker run --rm hello-world 2>& 1 | tee -a " $LOG_FILE " | grep -q "Hello from Docker" ; then
print_success "Docker sanity check passed."
else
print_error "Docker hello-world test failed. Please verify installation."
2025-06-26 16:49:03 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
print_warning " NOTE: ' $USERNAME ' must log out and back in to use Docker without sudo. "
2025-06-26 17:12:59 +01:00
log "Docker installation completed."
2025-06-26 16:49:03 +01:00
}
2025-06-26 16:22:23 +01:00
install_tailscale( ) {
2025-06-26 21:49:51 +01:00
if ! confirm "Install Tailscale VPN (Optional)?" ; then
2025-06-26 16:22:23 +01:00
print_info "Skipping Tailscale installation."
return 0
fi
2025-06-26 21:49:51 +01:00
print_section "Tailscale VPN Installation"
if command -v tailscale >/dev/null 2>& 1; then
print_info "Tailscale already installed."
2025-06-26 16:22:23 +01:00
return 0
fi
2025-06-26 21:49:51 +01:00
print_info "Installing Tailscale..."
curl -fsSL https://tailscale.com/install.sh -o /tmp/tailscale_install.sh
chmod +x /tmp/tailscale_install.sh
2025-06-26 22:19:35 +01:00
# Simple sanity check on the downloaded script
2025-06-26 21:49:51 +01:00
if ! grep -q "tailscale" /tmp/tailscale_install.sh; then
print_error "Downloaded Tailscale install script appears invalid."
rm -f /tmp/tailscale_install.sh
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
if ! /tmp/tailscale_install.sh; then
print_error "Failed to install Tailscale."
rm -f /tmp/tailscale_install.sh
exit 1
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
rm -f /tmp/tailscale_install.sh
print_warning "ACTION REQUIRED: Run 'sudo tailscale up' after script finishes."
print_success "Tailscale installation complete."
2025-06-26 17:12:59 +01:00
log "Tailscale installation completed."
2025-06-26 16:22:23 +01:00
}
configure_swap( ) {
2025-06-26 21:49:51 +01:00
if [ [ $IS_CONTAINER = = true ] ] ; then
print_info "Swap configuration skipped in container."
2025-06-26 16:22:23 +01:00
return 0
fi
2025-06-26 21:49:51 +01:00
print_section "Swap Configuration"
local existing_swap
existing_swap = $( swapon --show --noheadings | awk '{print $1}' || true )
if [ [ -n " $existing_swap " ] ] ; then
local current_size
current_size = $( ls -lh " $existing_swap " | awk '{print $5}' )
print_info " Existing swap file found: $existing_swap ( $current_size ) "
if confirm "Modify existing swap file size?" ; then
2025-06-26 22:19:35 +01:00
local SWAP_SIZE
2025-06-26 21:49:51 +01:00
while true; do
read -rp " $( echo -e " ${ CYAN } Enter new swap size (e.g., 2G, 512M) [current: $current_size ]: ${ NC } " ) " SWAP_SIZE
SWAP_SIZE = ${ SWAP_SIZE :- $current_size }
if validate_swap_size " $SWAP_SIZE " ; then
break
else
print_error "Invalid size. Use format like '2G' or '512M'."
fi
done
2025-06-26 22:19:35 +01:00
local REQUIRED_SPACE
2025-06-26 21:49:51 +01:00
REQUIRED_SPACE = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 22:19:35 +01:00
local AVAILABLE_SPACE
2025-06-26 21:49:51 +01:00
AVAILABLE_SPACE = $( df -k / | tail -n 1 | awk '{print $4}' )
if ( ( AVAILABLE_SPACE < REQUIRED_SPACE / 1024 ) ) ; then
print_error " Insufficient disk space for $SWAP_SIZE swap file. Available: $(( AVAILABLE_SPACE / 1024 )) MB "
exit 1
fi
print_info "Disabling existing swap file..."
swapoff " $existing_swap " || { print_error "Failed to disable swap file." ; exit 1; }
print_info " Resizing swap file to $SWAP_SIZE ... "
if ! fallocate -l " $SWAP_SIZE " " $existing_swap " || ! chmod 600 " $existing_swap " || ! mkswap " $existing_swap " || ! swapon " $existing_swap " ; then
print_error "Failed to resize or enable swap file."
exit 1
fi
print_success " Swap file resized to $SWAP_SIZE . "
else
print_info "Keeping existing swap file."
return 0
fi
else
if ! confirm "Configure a swap file (recommended for < 4GB RAM)?" ; then
print_info "Skipping swap configuration."
return 0
fi
2025-06-26 22:19:35 +01:00
local SWAP_SIZE
2025-06-26 21:49:51 +01:00
while true; do
read -rp " $( echo -e " ${ CYAN } Enter swap file size (e.g., 2G, 512M) [2G]: ${ NC } " ) " SWAP_SIZE
SWAP_SIZE = ${ SWAP_SIZE :- 2G }
if validate_swap_size " $SWAP_SIZE " ; then
break
else
print_error "Invalid size. Use format like '2G' or '512M'."
fi
done
2025-06-26 22:19:35 +01:00
local REQUIRED_SPACE
2025-06-26 21:49:51 +01:00
REQUIRED_SPACE = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 22:19:35 +01:00
local AVAILABLE_SPACE
2025-06-26 21:49:51 +01:00
AVAILABLE_SPACE = $( df -k / | tail -n 1 | awk '{print $4}' )
if ( ( AVAILABLE_SPACE < REQUIRED_SPACE / 1024 ) ) ; then
print_error " Insufficient disk space for $SWAP_SIZE swap file. Available: $(( AVAILABLE_SPACE / 1024 )) MB "
exit 1
fi
print_info " Creating $SWAP_SIZE swap file... "
if ! fallocate -l " $SWAP_SIZE " /swapfile || ! chmod 600 /swapfile || ! mkswap /swapfile || ! swapon /swapfile; then
print_error "Failed to create or enable swap file."
2025-06-26 22:19:35 +01:00
rm -f /swapfile || true
2025-06-26 21:49:51 +01:00
exit 1
fi
if ! grep -q '^/swapfile ' /etc/fstab; then
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
print_success " Swap file created: $SWAP_SIZE "
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
print_info "Optimizing swap settings..."
2025-06-26 22:19:35 +01:00
local NEW_SWAP_CONFIG
2025-06-26 21:49:51 +01:00
NEW_SWAP_CONFIG = $( mktemp)
tee " $NEW_SWAP_CONFIG " > /dev/null <<EOF
vm.swappiness= 10
vm.vfs_cache_pressure= 50
EOF
if [ [ -f /etc/sysctl.d/99-swap.conf ] ] && cmp -s " $NEW_SWAP_CONFIG " /etc/sysctl.d/99-swap.conf; then
print_info "Swap settings already correct. Skipping."
rm -f " $NEW_SWAP_CONFIG "
else
mv " $NEW_SWAP_CONFIG " /etc/sysctl.d/99-swap.conf
chmod 644 /etc/sysctl.d/99-swap.conf
sysctl -p /etc/sysctl.d/99-swap.conf >/dev/null
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
print_success "Swap configured successfully."
swapon --show | tee -a " $LOG_FILE "
free -h | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
log "Swap configuration completed."
}
2025-06-26 21:49:51 +01:00
configure_time_sync( ) {
2025-06-26 16:22:23 +01:00
print_section "Time Synchronization"
2025-06-26 21:49:51 +01:00
print_info "Ensuring chrony is active..."
systemctl enable --now chrony
sleep 2
if systemctl is-active --quiet chrony; then
print_success "Chrony is active for time synchronization."
chronyc tracking | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
print_error "Chrony service failed to start."
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 17:12:59 +01:00
log "Time synchronization completed."
2025-06-26 16:22:23 +01:00
}
2025-06-26 21:49:51 +01:00
final_cleanup( ) {
print_section "Final System Cleanup"
print_info "Running final system update and cleanup..."
if ! apt-get update -qq; then
2025-06-26 22:19:35 +01:00
print_warning "Failed to update package lists during final cleanup."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
if ! apt-get upgrade -y -qq || ! apt-get --purge autoremove -y -qq || ! apt-get autoclean -y -qq; then
2025-06-26 22:19:35 +01:00
print_warning "Final system cleanup failed on one or more commands."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
systemctl daemon-reload
print_success "Final system update and cleanup complete."
log "Final system cleanup completed."
}
generate_summary( ) {
print_section "Setup Complete!"
print_info "Checking critical services..."
for service in " $SSH_SERVICE " fail2ban chrony; do
if systemctl is-active --quiet " $service " ; then
print_success " Service $service is active. "
else
print_error " Service $service is NOT active. "
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
done
if ufw status | grep -q "Status: active" ; then
print_success "Service ufw is active."
else
print_error "Service ufw is NOT active."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
if command -v docker >/dev/null 2>& 1; then
if systemctl is-active --quiet docker; then
print_success "Service docker is active."
else
print_error "Service docker is NOT active."
fi
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
echo -e " \n ${ GREEN } Server setup and hardening script has finished successfully. ${ NC } "
echo
echo -e " ${ YELLOW } Configuration Summary: ${ NC } "
2025-06-26 22:19:35 +01:00
echo -e " Admin User: $USERNAME "
echo -e " Hostname: $SERVER_NAME "
echo -e " SSH Port: $SSH_PORT "
echo -e " Server IP: $SERVER_IP "
2025-06-26 21:49:51 +01:00
echo
echo -e " ${ PURPLE } Log File: ${ LOG_FILE } ${ NC } "
2025-06-26 22:19:35 +01:00
echo -e " ${ PURPLE } Backups: ${ BACKUP_DIR } ${ NC } "
2025-06-26 21:49:51 +01:00
echo
echo -e " ${ CYAN } Post-Reboot Verification Steps: ${ NC } "
2025-06-26 22:19:35 +01:00
echo -e " - SSH access: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP "
echo -e " - Firewall rules: sudo ufw status verbose"
echo -e " - Time sync: chronyc tracking"
echo -e " - Fail2Ban status: sudo fail2ban-client status sshd"
2025-06-26 22:37:53 +01:00
echo -e " - Swap status: sudo swapon --show && free -h"
2025-06-26 21:49:51 +01:00
if command -v docker >/dev/null 2>& 1; then
2025-06-26 22:19:35 +01:00
echo -e " - Docker status: docker ps"
2025-06-26 21:49:51 +01:00
fi
if command -v tailscale >/dev/null 2>& 1; then
2025-06-26 22:37:53 +01:00
echo -e " - Tailscale status: tailscale status"
2025-06-26 21:49:51 +01:00
fi
print_warning "\nA reboot is required to apply all changes cleanly."
if [ [ $VERBOSE = = true ] ] ; then
if confirm "Reboot now?" "y" ; then
print_info "Rebooting now..."
sleep 3
reboot
else
print_warning "Please reboot manually with 'sudo reboot'."
fi
else
print_warning "Quiet mode enabled. Please reboot manually with 'sudo reboot'."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
log "Script finished successfully."
}
handle_error( ) {
local exit_code = $?
local line_no = " $1 "
print_error " An error occurred on line $line_no (exit code: $exit_code ). "
print_info " Log file: $LOG_FILE "
print_info " Backups: $BACKUP_DIR "
exit $exit_code
2025-06-26 16:22:23 +01:00
}
main( ) {
2025-06-26 21:49:51 +01:00
trap 'handle_error $LINENO' ERR
touch " $LOG_FILE " && chmod 600 " $LOG_FILE "
log "Starting Debian/Ubuntu hardening script."
2025-06-26 16:22:23 +01:00
print_header
check_dependencies
2025-06-26 21:49:51 +01:00
check_system
2025-06-26 16:22:23 +01:00
collect_config
install_packages
setup_user
configure_system
configure_ssh
configure_firewall
configure_fail2ban
configure_auto_updates
2025-06-26 21:49:51 +01:00
configure_time_sync
2025-06-26 16:22:23 +01:00
install_docker
install_tailscale
configure_swap
2025-06-26 21:49:51 +01:00
final_cleanup
generate_summary
2025-06-26 16:22:23 +01:00
}
2025-06-26 21:49:51 +01:00
# Run main function
2025-06-26 20:11:59 +01:00
main " $@ "