2025-06-26 16:22:23 +01:00
#!/bin/bash
2025-08-15 14:14:57 +01:00
# Debian and Ubuntu Server Hardening Interactive Script
2025-10-16 14:55:40 +01:00
# Version: 0.70 | 2025-10-20
2025-06-28 11:43:10 +01:00
# Changelog:
2025-10-14 11:29:40 +01:00
# - v0.70: Option to remove cloud VPS provider packages (like cloud-init).
# New operational modes: --cleanup-preview, --cleanup-only, --skip-cleanup.
# Add help and usage instructions with --help flag.
2025-10-13 11:20:46 +01:00
# - v0.69: Ensure .ssh directory ownership is set for new user.
2025-09-07 17:27:29 +01:00
# - v0.68: Enable UFW IPv6 support if available
2025-09-07 10:48:30 +01:00
# - v0.67: Do not log taiscale auth key in log file
2025-08-26 11:40:41 +01:00
# - v0.66: While configuring and in the summary, display both IPv6 and IPv4.
2025-08-19 20:02:03 +01:00
# - v0.65: If reconfigure locales - appy newly configured locale to the current environment.
2025-08-15 14:14:57 +01:00
# - v0.64: Tested at Debian 13 to confirm it works as expected
2025-08-10 23:30:33 +01:00
# - v0.63: Added ssh install in key packages
2025-08-06 15:03:31 +01:00
# - v0.62: Added fix for fail2ban by creating empty ufw log file
2025-08-03 23:05:44 +01:00
# - v0.61: Display Lynis suggestions in summary, hide tailscale auth key, cleanup temp files
2025-07-15 19:20:26 +01:00
# - v0.60: CI for shellcheck
2025-07-15 13:51:34 +01:00
# - v0.59: Add a new optional function that applies a set of recommended sysctl security settings to harden the kernel.
2025-07-15 15:13:58 +01:00
# Script can now check for update and can run self-update.
2025-07-07 21:08:21 +01:00
# - v0.58: improved fail2ban to parse ufw logs
2025-07-07 20:20:57 +01:00
# - v0.57: Fix for silent failure at test_backup()
2025-10-14 11:56:50 +01:00
# Option to choose which directories to back up.
2025-07-04 14:16:27 +01:00
# - v0.56: Make tailscale config optional
2025-07-02 16:35:44 +01:00
# - v0.55: Improving setup_user() - ssh-keygen replaced the option to skip ssh key
2025-07-02 15:01:58 +01:00
# - v0.54: Fix for rollback_ssh_changes() - more reliable on newer Ubuntu
2025-10-14 11:56:50 +01:00
# Better error message if script is executed by non-root or without sudo
2025-07-01 15:27:04 +01:00
# - v0.53: Fix for test_backup() - was failing if run as non root sudo user
2025-06-30 21:59:07 +01:00
# - v0.52: Roll-back SSH config on failure to configure SSH port, confirmed SSH config support for Ubuntu 24.10
2025-06-30 11:37:08 +01:00
# - v0.51: corrected repo links
2025-06-30 10:38:28 +01:00
# - v0.50: versioning format change and repo name change
2025-06-29 21:29:44 +01:00
# - v4.3: Add SHA256 integrity verification
2025-06-29 13:10:41 +01:00
# - v4.2: Added Security Audit Tools (Integrating Lynis and Optionally Debsecan) & option to do Backup Testing
2025-10-14 11:56:50 +01:00
# Fixed debsecan compatibility (Debian-only), added global BACKUP_LOG, added backup testing
2025-06-28 23:12:54 +01:00
# - v4.1: Added tailscale config to connect to tailscale or headscale server
2025-06-28 18:48:26 +01:00
# - v4.0: Added automated backup config. Mainly for Hetzner Storage Box but can be used for any rsync/SSH enabled remote solution.
# - v3.*: Improvements to script flow and fixed bugs which were found in tests at Oracle Cloud
2025-06-26 16:22:23 +01:00
#
2025-06-26 21:49:51 +01:00
# Description:
# This script provisions and hardens a fresh Debian 12 or Ubuntu server with essential security
# configurations, user management, SSH hardening, firewall setup, and optional features
2025-06-28 18:48:26 +01:00
# like Docker and Tailscale and automated backups to Hetzner storage box or any rsync location.
# It is designed to be idempotent, safe.
2025-06-30 11:37:08 +01:00
# README at GitHub: https://github.com/buildplan/du_setup/blob/main/README.md
2025-06-26 21:49:51 +01:00
#
# Prerequisites:
2025-07-02 13:49:43 +01:00
# - Run as root on a fresh Debian 12 or Ubuntu server (e.g., sudo ./du_setup.sh or run as root -E ./du_setup.sh).
2025-06-26 21:49:51 +01:00
# - Internet connectivity is required for package installation.
#
# Usage:
2025-06-30 11:37:08 +01:00
# Download: wget https://raw.githubusercontent.com/buildplan/du_setup/refs/heads/main/du_setup.sh
2025-06-30 10:35:43 +01:00
# Make it executable: chmod +x du_setup.sh
2025-07-02 13:49:43 +01:00
# Run it: sudo -E ./du_setup.sh [--quiet]
2025-06-26 21:49:51 +01:00
#
# Options:
2025-06-28 18:48:26 +01:00
# --quiet: Suppress non-critical output for automation. (Not recommended always best to review all the options)
2025-06-26 21:49:51 +01:00
#
# Notes:
2025-06-30 10:35:43 +01:00
# - The script creates a log file in /var/log/du_setup_*.log.
2025-06-26 21:49:51 +01:00
# - Critical configurations are backed up before modification. Backup files are at /root/setup_harden_backup_*.
# - A new admin user is created with a mandatory password or SSH key for authentication.
# - Root SSH login is disabled; all access is via the new user with sudo privileges.
# - The user will be prompted to select a timezone, swap size, and custom firewall ports.
# - A reboot is recommended at the end to apply all changes.
# - Test the script in a VM before production use.
#
# Troubleshooting:
# - Check the log file for errors if the script fails.
# - If SSH access is lost, use the server console to restore /etc/ssh/sshd_config.backup_*.
# - Ensure sufficient disk space (>2GB) for swap file creation.
2025-06-26 16:22:23 +01:00
set -euo pipefail # Exit on error, undefined vars, pipe failures
2025-07-15 15:13:58 +01:00
# --- Update Configuration ---
2025-10-13 22:52:07 +01:00
CURRENT_VERSION = "0.70"
2025-07-15 15:13:58 +01:00
SCRIPT_URL = "https://raw.githubusercontent.com/buildplan/du_setup/refs/heads/main/du_setup.sh"
CHECKSUM_URL = " ${ SCRIPT_URL } .sha256 "
2025-06-26 16:22:23 +01:00
# --- GLOBAL VARIABLES & CONFIGURATION ---
2025-07-02 20:22:10 +01:00
# --- Colors for output ---
if command -v tput >/dev/null 2>& 1 && tput setaf 1 >/dev/null 2>& 1; then
RED = $( tput setaf 1)
GREEN = $( tput setaf 2)
YELLOW = " $( tput bold) $( tput setaf 3) "
BLUE = $( tput setaf 4)
PURPLE = $( tput setaf 5)
CYAN = $( tput setaf 6)
BOLD = $( tput bold)
NC = $( tput sgr0)
else
2025-10-14 12:32:29 +01:00
RED = '\e[0;31m'
GREEN = '\e[0;32m'
YELLOW = '\e[1;33m'
BLUE = '\e[0;34m'
PURPLE = '\e[0;35m'
CYAN = '\e[0;36m'
NC = '\e[0m'
BOLD = '\e[1m'
2025-07-02 20:22:10 +01:00
fi
2025-06-26 16:22:23 +01:00
# Script variables
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
2025-06-30 10:35:43 +01:00
LOG_FILE = " /var/log/du_setup_ $( date +%Y%m%d_%H%M%S) .log "
2025-06-29 18:38:12 +01:00
BACKUP_LOG = "/var/log/backup_rsync.log"
2025-07-15 13:51:34 +01:00
REPORT_FILE = " /var/log/du_setup_report_ $( date +%Y%m%d_%H%M%S) .txt "
2025-06-26 16:22:23 +01:00
VERBOSE = true
BACKUP_DIR = " /root/setup_harden_backup_ $( date +%Y%m%d_%H%M%S) "
2025-10-15 17:27:09 +01:00
ORIGINAL_ARGS = " $* "
2025-10-13 22:52:07 +01:00
CLEANUP_PREVIEW = false # If true, show what would be cleaned up without making changes
CLEANUP_ONLY = false # If true, only perform cleanup tasks
SKIP_CLEANUP = false # If true, skip cleanup tasks
DETECTED_VIRT_TYPE = ""
DETECTED_MANUFACTURER = ""
DETECTED_PRODUCT = ""
IS_CLOUD_PROVIDER = false
2025-06-26 16:22:23 +01:00
IS_CONTAINER = false
2025-10-15 18:03:22 +01:00
2025-06-26 16:22:23 +01:00
SSHD_BACKUP_FILE = ""
LOCAL_KEY_ADDED = false
SSH_SERVICE = ""
2025-06-26 21:49:51 +01:00
ID = "" # This will be populated from /etc/os-release
2025-06-29 20:08:19 +01:00
FAILED_SERVICES = ( )
2025-06-26 16:22:23 +01:00
2025-10-15 18:03:22 +01:00
# --- --help ---
2025-10-13 22:52:07 +01:00
show_usage( ) {
2025-10-14 10:49:45 +01:00
printf "\n"
2025-10-14 12:32:29 +01:00
printf "%s%s%s\n" " $CYAN " "Debian/Ubuntu Server Setup & Hardening Script" " $NC "
2025-10-14 10:49:45 +01:00
2025-10-14 12:32:29 +01:00
printf "\n%sUsage:%s\n" " $BOLD " " $NC "
2025-10-15 18:03:22 +01:00
printf " sudo -E %s [OPTIONS]\n" " $( basename " $0 " ) "
2025-10-14 10:49:45 +01:00
2025-10-14 12:32:29 +01:00
printf "\n%sDescription:%s\n" " $BOLD " " $NC "
2025-10-14 10:49:45 +01:00
printf " This script provisions a fresh Debian or Ubuntu server with secure base configurations.\n"
printf " It handles updates, firewall, SSH hardening, user creation, and optional tools.\n"
2025-10-14 11:29:40 +01:00
2025-10-14 12:32:29 +01:00
printf "\n%sOperational Modes:%s\n" " $BOLD " " $NC "
2025-10-14 10:49:45 +01:00
printf " %-22s %s\n" "--cleanup-preview" "Show which provider packages/users would be cleaned without making changes."
printf " %-22s %s\n" "--cleanup-only" "Run only the provider cleanup function (for existing servers)."
2025-10-14 11:29:40 +01:00
2025-10-14 12:32:29 +01:00
printf "\n%sModifiers:%s\n" " $BOLD " " $NC "
2025-10-14 10:49:45 +01:00
printf " %-22s %s\n" "--skip-cleanup" "Skip provider cleanup entirely during a full setup run."
printf " %-22s %s\n" "--quiet" "Suppress verbose output (intended for automation)."
printf " %-22s %s\n" "-h, --help" "Display this help message and exit."
2025-10-14 12:32:29 +01:00
printf "\n%sUsage Examples:%s\n" " $BOLD " " $NC "
2025-10-14 10:49:45 +01:00
printf " # Run the full interactive setup\n"
2025-10-15 18:03:22 +01:00
printf " %ssudo -E ./%s%s\n\n" " $YELLOW " " $( basename " $0 " ) " " $NC "
2025-10-14 10:49:45 +01:00
printf " # Preview provider cleanup actions without applying them\n"
2025-10-15 18:03:22 +01:00
printf " %ssudo -E ./%s --cleanup-preview%s\n\n" " $YELLOW " " $( basename " $0 " ) " " $NC "
2025-10-14 10:49:45 +01:00
printf " # Run a full setup but skip the provider cleanup step\n"
2025-10-15 18:03:22 +01:00
printf " %ssudo -E ./%s --skip-cleanup%s\n\n" " $YELLOW " " $( basename " $0 " ) " " $NC "
printf " # Run in quiet mode for automation\n"
printf " %ssudo -E ./%s --quiet%s\n" " $YELLOW " " $( basename " $0 " ) " " $NC "
2025-10-14 10:49:45 +01:00
2025-10-14 12:32:29 +01:00
printf "\n%sImportant Notes:%s\n" " $BOLD " " $NC "
2025-10-15 18:03:22 +01:00
printf " - The -E flag preserves your environment variables (recommended)\n"
2025-10-14 12:32:29 +01:00
printf " - Logs are saved to %s/var/log/du_setup_*.log%s\n" " $BOLD " " $NC "
printf " - Backups of modified configs are in %s/root/setup_harden_backup_*%s\n" " $BOLD " " $NC "
2025-10-14 10:49:45 +01:00
printf " - For full documentation, see the project repository:\n"
2025-10-14 12:32:29 +01:00
printf " %s%s%s\n" " $CYAN " "https://github.com/buildplan/du-setup" " $NC "
2025-10-14 10:49:45 +01:00
printf "\n"
2025-10-13 22:52:07 +01:00
exit 0
}
2025-10-13 23:06:01 +01:00
# --- PARSE ARGUMENTS ---
while [ [ $# -gt 0 ] ] ; do
case $1 in
--quiet) VERBOSE = false; shift ; ;
--cleanup-preview) CLEANUP_PREVIEW = true; shift ; ;
--cleanup-only) CLEANUP_ONLY = true; shift ; ;
--skip-cleanup) SKIP_CLEANUP = true; shift ; ;
-h| --help) show_usage ; ;
*) shift ; ;
esac
done
2025-10-15 17:27:09 +01:00
# --- Root Check ---
if [ [ $EUID -ne 0 ] ] ; then
printf "\n"
printf "%s✗ You are running as user '%s'. This script must be run as root.%s\n" " $RED " " $( whoami) " " $NC "
printf "\n"
printf "This script makes system-level changes including:\n"
printf " - Package installation/removal\n"
printf " - Firewall configuration\n"
printf " - SSH hardening\n"
printf " - User account management\n"
printf "\n"
printf "Choose one of the following methods to run this script:\n"
printf "\n"
printf "%s%sRun with sudo (-E preserves environment):%s\n" " $BOLD " " $GREEN " " $NC "
if [ [ -n " $ORIGINAL_ARGS " ] ] ; then
printf " %ssudo -E %s %s%s\n" " $CYAN " " $0 " " $ORIGINAL_ARGS " " $NC "
else
printf " %ssudo -E %s%s\n" " $CYAN " " $0 " " $NC "
fi
printf "\n"
printf "%s%sAlternative methods:%s\n" " $BOLD " " $YELLOW " " $NC "
printf " %ssudo su -%s # Switch to root\n" " $CYAN " " $NC "
if [ [ -n " $ORIGINAL_ARGS " ] ] ; then
printf " And run: %s%s %s%s\n" " $CYAN " " $0 " " $ORIGINAL_ARGS " " $NC "
else
printf " And run: %s%s%s\n" " $CYAN " " $0 " " $NC "
fi
printf "\n"
exit 1
fi
2025-06-26 16:22:23 +01:00
# --- LOGGING & PRINT FUNCTIONS ---
log( ) {
echo " $( date '+%Y-%m-%d %H:%M:%S' ) - $1 " >> " $LOG_FILE "
}
print_header( ) {
[ [ $VERBOSE = = false ] ] && return
2025-10-16 14:55:40 +01:00
printf '\n'
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ CYAN } ╔═════════════════════════════════════════════════════════════════╗ ${ NC } "
printf '%s\n' " ${ CYAN } ║ ║ ${ NC } "
printf '%s\n' " ${ CYAN } ║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║ ${ NC } "
2025-10-16 14:55:40 +01:00
printf '%s\n' " ${ CYAN } ║ v0.70 | 2025-10-20 ║ ${ NC } "
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ CYAN } ║ ║ ${ NC } "
printf '%s\n' " ${ CYAN } ╚═════════════════════════════════════════════════════════════════╝ ${ NC } "
printf '\n'
2025-06-26 16:22:23 +01:00
}
print_section( ) {
[ [ $VERBOSE = = false ] ] && return
2025-10-16 10:08:03 +01:00
printf '\n%s\n' " ${ BLUE } ▓▓▓ $1 ▓▓▓ ${ NC } " | tee -a " $LOG_FILE "
printf '%s\n' " ${ BLUE } $( printf '═%.0s' { 1..65} ) ${ NC } "
2025-06-26 16:22:23 +01:00
}
print_success( ) {
[ [ $VERBOSE = = false ] ] && return
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ GREEN } ✓ $1 ${ NC } " | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
}
print_error( ) {
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ RED } ✗ $1 ${ NC } " | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
}
print_warning( ) {
[ [ $VERBOSE = = false ] ] && return
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ YELLOW } ⚠ $1 ${ NC } " | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
}
print_info( ) {
[ [ $VERBOSE = = false ] ] && return
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ PURPLE } ℹ $1 ${ NC } " | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
}
2025-10-13 22:52:07 +01:00
# --- CLEANUP HELPER FUNCTIONS ---
execute_check( ) {
" $@ "
}
execute_command( ) {
local cmd_string = " $* "
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
2025-10-16 10:08:03 +01:00
printf '%s Would execute: %s\n' " ${ CYAN } [PREVIEW] ${ NC } " " ${ BOLD } $cmd_string ${ NC } " | tee -a " $LOG_FILE "
2025-10-13 22:52:07 +01:00
return 0
else
" $@ "
return $?
fi
}
# --- ENVIRONMENT DETECTION (Cloud VPS or Trusted VM) ---
detect_environment( ) {
local VIRT_TYPE = ""
local MANUFACTURER = ""
local PRODUCT = ""
local IS_CLOUD_VPS = false
2025-10-14 01:00:59 +01:00
2025-10-13 23:06:01 +01:00
# systemd-detect-virt
2025-10-13 22:52:07 +01:00
if command -v systemd-detect-virt & >/dev/null; then
VIRT_TYPE = $( systemd-detect-virt 2>/dev/null || echo "none" )
fi
2025-10-14 01:00:59 +01:00
2025-10-13 23:06:01 +01:00
# dmidecode for hardware info
2025-10-13 22:52:07 +01:00
if command -v dmidecode & >/dev/null && [ [ $( id -u) -eq 0 ] ] ; then
MANUFACTURER = $( dmidecode -s system-manufacturer 2>/dev/null | tr '[:upper:]' '[:lower:]' || echo "unknown" )
PRODUCT = $( dmidecode -s system-product-name 2>/dev/null | tr '[:upper:]' '[:lower:]' || echo "unknown" )
fi
2025-10-14 01:00:59 +01:00
2025-10-13 23:06:01 +01:00
# Check /sys/class/dmi/id/ (fallback, doesn't require dmidecode)
2025-10-13 22:52:07 +01:00
if [ [ -z " $MANUFACTURER " || " $MANUFACTURER " = = "unknown" ] ] ; then
if [ [ -r /sys/class/dmi/id/sys_vendor ] ] ; then
2025-10-13 23:21:15 +01:00
MANUFACTURER = $( tr '[:upper:]' '[:lower:]' < /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "unknown" )
2025-10-13 22:52:07 +01:00
fi
fi
2025-10-13 23:21:15 +01:00
2025-10-13 22:52:07 +01:00
if [ [ -z " $PRODUCT " || " $PRODUCT " = = "unknown" ] ] ; then
if [ [ -r /sys/class/dmi/id/product_name ] ] ; then
2025-10-13 23:21:15 +01:00
PRODUCT = $( tr '[:upper:]' '[:lower:]' < /sys/class/dmi/id/product_name 2>/dev/null || echo "unknown" )
2025-10-13 22:52:07 +01:00
fi
fi
2025-10-14 01:00:59 +01:00
if command -v dmidecode & >/dev/null && [ [ $( id -u) -eq 0 ] ] ; then
DETECTED_BIOS_VENDOR = $( dmidecode -s bios-vendor 2>/dev/null | tr '[:upper:]' '[:lower:]' || echo "unknown" )
elif [ [ -r /sys/class/dmi/id/bios_vendor ] ] ; then
DETECTED_BIOS_VENDOR = $( tr '[:upper:]' '[:lower:]' < /sys/class/dmi/id/bios_vendor 2>/dev/null || echo "unknown" )
fi
2025-10-13 22:52:07 +01:00
# Cloud provider detection patterns
local CLOUD_PATTERNS = (
# VPS/Cloud Providers
"digitalocean"
"linode"
"vultr"
"hetzner"
"ovh"
"scaleway"
"contabo"
2025-10-14 22:48:17 +01:00
"netcup"
"ionos"
"hostinger"
"racknerd"
2025-10-13 22:52:07 +01:00
"upcloud"
"dreamhost"
"kimsufi"
"online.net"
"equinix metal"
"lightsail"
"scaleway"
# Major Cloud Platforms
"amazon"
"amazon ec2"
"aws"
"google"
"gce"
"google compute engine"
"microsoft"
"azure"
"oracle cloud"
"alibaba"
"tencent"
"rackspace"
# Virtualization indicating cloud VPS
"droplet"
"linodekvm"
"kvm"
"openstack"
)
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Check if manufacturer or product matches cloud patterns
for pattern in " ${ CLOUD_PATTERNS [@] } " ; do
if [ [ " $MANUFACTURER " = = *" $pattern " * ] ] || [ [ " $PRODUCT " = = *" $pattern " * ] ] ; then
IS_CLOUD_VPS = true
break
fi
done
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Additional checks based on virtualization type
case " $VIRT_TYPE " in
kvm| qemu)
2025-10-14 01:00:59 +01:00
if [ [ -z " $IS_CLOUD_VPS " ] ] || [ [ " $IS_CLOUD_VPS " = = "false" ] ] ; then
if [ [ -d /etc/cloud/cloud.cfg.d ] ] && grep -qE "(Hetzner|DigitalOcean|Vultr|OVH)" /etc/cloud/cloud.cfg.d/* 2>/dev/null; then
2025-10-13 22:52:07 +01:00
IS_CLOUD_VPS = true
fi
fi
; ;
vmware)
IS_CLOUD_VPS = false
; ;
oracle| virtualbox)
IS_CLOUD_VPS = false
; ;
xen)
IS_CLOUD_VPS = true
; ;
hyperv| microsoft)
if [ [ " $MANUFACTURER " = = *"microsoft" * ] ] && [ [ " $PRODUCT " = = *"virtual machine" * ] ] ; then
IS_CLOUD_VPS = false
fi
; ;
none)
IS_CLOUD_VPS = false
; ;
esac
2025-10-14 01:00:59 +01:00
# Determine environment type based on detection
if [ [ " $VIRT_TYPE " = = "none" ] ] ; then
ENVIRONMENT_TYPE = "bare-metal"
elif [ [ " $IS_CLOUD_VPS " = = "true" ] ] ; then
ENVIRONMENT_TYPE = "commercial-cloud"
elif [ [ " $VIRT_TYPE " = ~ ^( kvm| qemu) $ ] ] ; then
if [ [ " $MANUFACTURER " = = "qemu" && " $PRODUCT " = ~ ^( standard pc| pc-| pc ) ] ] ; then
ENVIRONMENT_TYPE = "uncertain-kvm"
else
ENVIRONMENT_TYPE = "commercial-cloud"
fi
elif [ [ " $VIRT_TYPE " = ~ ^( vmware| virtualbox| oracle) $ ] ] ; then
ENVIRONMENT_TYPE = "personal-vm"
elif [ [ " $VIRT_TYPE " = = "xen" ] ] ; then
ENVIRONMENT_TYPE = "uncertain-xen"
else
ENVIRONMENT_TYPE = "unknown"
fi
2025-10-14 21:10:23 +01:00
DETECTED_PROVIDER_NAME = ""
2025-10-14 01:00:59 +01:00
case " $ENVIRONMENT_TYPE " in
commercial-cloud)
if [ [ " $MANUFACTURER " = ~ digitalocean ] ] ; then
DETECTED_PROVIDER_NAME = "DigitalOcean"
elif [ [ " $MANUFACTURER " = ~ hetzner ] ] ; then
DETECTED_PROVIDER_NAME = "Hetzner Cloud"
elif [ [ " $MANUFACTURER " = ~ vultr ] ] ; then
DETECTED_PROVIDER_NAME = "Vultr"
elif [ [ " $MANUFACTURER " = ~ linode || " $PRODUCT " = ~ akamai ] ] ; then
DETECTED_PROVIDER_NAME = "Linode/Akamai"
elif [ [ " $MANUFACTURER " = ~ ovh ] ] ; then
DETECTED_PROVIDER_NAME = "OVH"
elif [ [ " $MANUFACTURER " = ~ amazon || " $PRODUCT " = ~ "ec2" ] ] ; then
DETECTED_PROVIDER_NAME = "Amazon Web Services (AWS)"
elif [ [ " $MANUFACTURER " = ~ google ] ] ; then
DETECTED_PROVIDER_NAME = "Google Cloud Platform"
elif [ [ " $MANUFACTURER " = ~ microsoft ] ] ; then
DETECTED_PROVIDER_NAME = "Microsoft Azure"
else
DETECTED_PROVIDER_NAME = "Cloud VPS Provider"
fi
; ;
personal-vm)
if [ [ " $VIRT_TYPE " = = "virtualbox" || " $MANUFACTURER " = ~ innotek ] ] ; then
DETECTED_PROVIDER_NAME = "VirtualBox"
elif [ [ " $VIRT_TYPE " = = "vmware" ] ] ; then
DETECTED_PROVIDER_NAME = "VMware"
else
DETECTED_PROVIDER_NAME = "Personal VM"
fi
; ;
uncertain-kvm)
DETECTED_PROVIDER_NAME = "KVM/QEMU Hypervisor"
; ;
esac
2025-10-13 22:52:07 +01:00
# Export results as global variables
2025-10-14 01:00:59 +01:00
export ENVIRONMENT_TYPE
2025-10-13 22:52:07 +01:00
DETECTED_VIRT_TYPE = " $VIRT_TYPE "
DETECTED_MANUFACTURER = " $MANUFACTURER "
DETECTED_PRODUCT = " $PRODUCT "
2025-10-14 01:00:59 +01:00
DETECTED_BIOS_VENDOR = " ${ DETECTED_BIOS_VENDOR :- unknown } "
2025-10-13 22:52:07 +01:00
IS_CLOUD_PROVIDER = " $IS_CLOUD_VPS "
2025-10-14 01:00:59 +01:00
log " Environment detection: VIRT= $VIRT_TYPE , MANUFACTURER= $MANUFACTURER , PRODUCT= $PRODUCT , IS_CLOUD= $IS_CLOUD_VPS , TYPE= $ENVIRONMENT_TYPE "
2025-10-13 22:52:07 +01:00
}
cleanup_provider_packages( ) {
print_section "Provider Package Cleanup (Optional)"
2025-10-14 01:00:59 +01:00
2025-10-14 21:59:08 +01:00
# --quiet mode check
if [ [ " $VERBOSE " = = "false" ] ] ; then
print_warning "Provider cleanup cannot be run in --quiet mode due to its interactive nature. Skipping."
log "Provider cleanup skipped due to --quiet mode."
return 0
fi
2025-10-14 02:05:56 +01:00
# Validate required variables
2025-10-14 01:51:55 +01:00
if [ [ -z " ${ LOG_FILE :- } " ] ] ; then
2025-10-14 01:00:59 +01:00
LOG_FILE = " /var/log/du_setup_ $( date +%Y%m%d_%H%M%S) .log "
echo " Warning: LOG_FILE not set, using: $LOG_FILE "
fi
2025-10-14 01:51:55 +01:00
if [ [ -z " ${ USERNAME :- } " ] ] ; then
2025-10-14 02:04:38 +01:00
USERNAME = " ${ SUDO_USER :- root } "
log " USERNAME defaulted to ' $USERNAME ' for cleanup-only mode "
2025-10-14 01:00:59 +01:00
fi
2025-10-14 01:51:55 +01:00
if [ [ -z " ${ BACKUP_DIR :- } " ] ] ; then
2025-10-14 01:00:59 +01:00
BACKUP_DIR = " /root/setup_harden_backup_ $( date +%Y%m%d_%H%M%S) "
mkdir -p " $BACKUP_DIR "
log " Created backup directory: $BACKUP_DIR "
fi
# Ensure cleanup mode variables are set
CLEANUP_PREVIEW = " ${ CLEANUP_PREVIEW :- false } "
CLEANUP_ONLY = " ${ CLEANUP_ONLY :- false } "
VERBOSE = " ${ VERBOSE :- true } "
2025-10-13 22:52:07 +01:00
# Detect environment first
detect_environment
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Display environment information
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ CYAN } === Environment Detection === ${ NC } "
printf 'Virtualization Type: %s\n' " ${ DETECTED_VIRT_TYPE :- unknown } "
printf 'System Manufacturer: %s\n' " ${ DETECTED_MANUFACTURER :- unknown } "
printf 'Product Name: %s\n' " ${ DETECTED_PRODUCT :- unknown } "
printf 'Environment Type: %s\n' " ${ ENVIRONMENT_TYPE :- unknown } "
2025-10-14 01:00:59 +01:00
if [ [ -n " ${ DETECTED_BIOS_VENDOR } " && " ${ DETECTED_BIOS_VENDOR } " != "unknown" ] ] ; then
2025-10-16 10:08:03 +01:00
printf 'BIOS Vendor: %s\n' " ${ DETECTED_BIOS_VENDOR } "
2025-10-14 01:00:59 +01:00
fi
if [ [ -n " ${ DETECTED_PROVIDER_NAME } " ] ] ; then
2025-10-16 10:08:03 +01:00
printf 'Detected Provider: %s\n' " ${ DETECTED_PROVIDER_NAME } "
2025-10-14 01:00:59 +01:00
fi
2025-10-16 10:08:03 +01:00
printf '\n'
2025-10-14 01:00:59 +01:00
# Determine recommendation based on three-way detection
2025-10-13 22:52:07 +01:00
local CLEANUP_RECOMMENDED = false
2025-10-14 01:00:59 +01:00
local DEFAULT_ANSWER = "n"
2025-10-13 22:52:07 +01:00
local RECOMMENDATION_TEXT = ""
2025-10-14 01:00:59 +01:00
local ENVIRONMENT_CONFIDENCE = " ${ ENVIRONMENT_CONFIDENCE :- low } "
case " $ENVIRONMENT_TYPE " in
commercial-cloud)
CLEANUP_RECOMMENDED = true
DEFAULT_ANSWER = "y"
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ YELLOW } ☁ Commercial Cloud VPS Detected ${ NC } "
2025-10-14 01:00:59 +01:00
if [ [ -n " ${ DETECTED_PROVIDER_NAME } " ] ] ; then
2025-10-16 10:08:03 +01:00
printf 'Provider: %s\n' " ${ CYAN } ${ DETECTED_PROVIDER_NAME } ${ NC } "
2025-10-14 01:00:59 +01:00
fi
2025-10-16 10:08:03 +01:00
printf 'This is a commercial VPS from an external provider.\n'
2025-10-14 01:00:59 +01:00
RECOMMENDATION_TEXT = " Provider cleanup is ${ BOLD } RECOMMENDED ${ NC } for security. "
2025-10-16 10:08:03 +01:00
printf '%s\n' " $RECOMMENDATION_TEXT "
printf 'Providers may install monitoring agents, pre-configured users, and management tools.\n'
2025-10-14 01:00:59 +01:00
; ;
uncertain-kvm)
CLEANUP_RECOMMENDED = false
DEFAULT_ANSWER = "n"
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ YELLOW } ⚠ KVM/QEMU Virtualization Detected (Uncertain) ${ NC } "
printf 'This environment could be:\n'
printf ' %s A commercial cloud provider VPS (Hetzner, Vultr, OVH, smaller providers)\n' " ${ CYAN } • ${ NC } "
printf ' %s A personal VM on Proxmox, KVM, or QEMU\n' " ${ CYAN } • ${ NC } "
printf ' %s A VPS from a regional/unlisted provider\n' " ${ CYAN } • ${ NC } "
printf '\n'
2025-10-14 01:00:59 +01:00
RECOMMENDATION_TEXT = " Cleanup is ${ BOLD } OPTIONAL ${ NC } - review packages carefully before proceeding. "
2025-10-16 10:08:03 +01:00
printf '%s\n' " $RECOMMENDATION_TEXT "
printf 'If this is a commercial VPS, cleanup is recommended.\n'
printf 'If you control the hypervisor (Proxmox/KVM), cleanup is optional.\n'
2025-10-14 01:00:59 +01:00
; ;
personal-vm)
CLEANUP_RECOMMENDED = false
DEFAULT_ANSWER = "n"
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ CYAN } ℹ Personal/Private Virtualization Detected${ NC } "
2025-10-14 01:00:59 +01:00
if [ [ -n " ${ DETECTED_PROVIDER_NAME } " ] ] ; then
2025-10-16 10:08:03 +01:00
printf 'Platform: %s\n' " ${ CYAN } ${ DETECTED_PROVIDER_NAME } ${ NC } "
2025-10-14 01:00:59 +01:00
fi
2025-10-16 10:08:03 +01:00
printf 'This appears to be a personal VM (VirtualBox, VMware Workstation, etc.)\n'
2025-10-14 01:00:59 +01:00
RECOMMENDATION_TEXT = " Provider cleanup is ${ BOLD } NOT RECOMMENDED ${ NC } for trusted environments. "
2025-10-16 10:08:03 +01:00
printf '%s\n' " $RECOMMENDATION_TEXT "
printf 'If you control the hypervisor/host, you likely don' \' 't need cleanup.\n'
2025-10-14 01:00:59 +01:00
; ;
bare-metal)
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ GREEN } ✓ Bare Metal Server Detected ${ NC } "
printf 'This appears to be a physical (bare metal) server.\n'
2025-10-14 01:00:59 +01:00
RECOMMENDATION_TEXT = " Provider cleanup is ${ BOLD } NOT NEEDED ${ NC } for bare metal. "
2025-10-16 10:08:03 +01:00
printf '%s\n' " $RECOMMENDATION_TEXT "
printf 'No virtualization layer detected - skipping cleanup.\n'
2025-10-14 01:00:59 +01:00
log "Provider package cleanup skipped: bare metal server detected."
return 0
; ;
uncertain-xen| unknown| *)
CLEANUP_RECOMMENDED = false
DEFAULT_ANSWER = "n"
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ YELLOW } ⚠ Virtualization Environment: Uncertain ${ NC } "
printf 'Could not definitively identify the hosting provider or environment.\n'
2025-10-14 01:00:59 +01:00
RECOMMENDATION_TEXT = " Cleanup is ${ BOLD } OPTIONAL ${ NC } - proceed with caution. "
2025-10-16 10:08:03 +01:00
printf '%s\n' " $RECOMMENDATION_TEXT "
printf 'Review packages carefully before removing anything.\n'
2025-10-14 01:00:59 +01:00
; ;
esac
2025-10-16 10:08:03 +01:00
printf '\n'
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Decision point based on environment and flags
if [ [ " $CLEANUP_PREVIEW " = = "false" ] ] && [ [ " $CLEANUP_ONLY " = = "false" ] ] ; then
2025-10-14 01:00:59 +01:00
local PROMPT_TEXT = ""
if [ [ " $ENVIRONMENT_TYPE " = = "commercial-cloud" ] ] ; then
PROMPT_TEXT = "Run provider package cleanup? (Recommended for cloud VPS)"
elif [ [ " $ENVIRONMENT_TYPE " = = "uncertain-kvm" ] ] ; then
PROMPT_TEXT = "Run provider package cleanup? (Verify your environment first)"
2025-10-13 22:52:07 +01:00
else
2025-10-14 01:00:59 +01:00
PROMPT_TEXT = "Run provider package cleanup? (Not recommended for trusted environments)"
fi
if ! confirm " $PROMPT_TEXT " " $DEFAULT_ANSWER " ; then
print_info "Skipping provider package cleanup."
log " Provider package cleanup skipped by user (environment: $ENVIRONMENT_TYPE ). "
return 0
fi
# Extra warning for non-cloud environments
if [ [ " $CLEANUP_RECOMMENDED " = = "false" ] ] && [ [ " $ENVIRONMENT_TYPE " != "uncertain-kvm" ] ] ; then
2025-10-13 22:52:07 +01:00
echo
2025-10-14 01:00:59 +01:00
print_warning "⚠ You chose to run cleanup on a trusted/personal environment."
print_warning "This may remove useful tools or break functionality."
echo
if ! confirm "Are you sure you want to continue?" "n" ; then
print_info "Cleanup cancelled."
log "User cancelled cleanup after warning."
2025-10-13 22:52:07 +01:00
return 0
fi
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_warning "=== PREVIEW MODE ENABLED ==="
print_info "No changes will be made. This is a simulation only."
echo
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "false" ] ] ; then
print_warning "RECOMMENDED: Create a snapshot/backup via provider dashboard before cleanup."
if ! confirm "Have you created a backup snapshot?" "n" ; then
print_info "Please create a backup first. Exiting cleanup."
log "User declined to proceed without backup snapshot."
return 0
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
print_warning "This will identify packages and configurations installed by your VPS provider."
if [ [ " $CLEANUP_PREVIEW " = = "false" ] ] ; then
print_warning "Removing critical packages can break system functionality."
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
local PROVIDER_PACKAGES = ( )
local PROVIDER_SERVICES = ( )
local PROVIDER_USERS = ( )
local ROOT_SSH_KEYS = ( )
2025-10-14 13:30:14 +01:00
# List of common provider and virtualization packages
2025-10-13 22:52:07 +01:00
local COMMON_PROVIDER_PKGS = (
"qemu-guest-agent"
"virtio-utils"
"virt-what"
"cloud-init"
"cloud-guest-utils"
"cloud-initramfs-growroot"
"cloud-utils"
"open-vm-tools"
"xe-guest-utilities"
"xen-tools"
"hyperv-daemons"
2025-10-14 01:00:59 +01:00
"oracle-cloud-agent"
2025-10-13 22:52:07 +01:00
"aws-systems-manager-agent"
"amazon-ssm-agent"
"google-compute-engine"
"google-osconfig-agent"
"walinuxagent"
"hetzner-needrestart"
"digitalocean-agent"
"do-agent"
"linode-agent"
"vultr-monitoring"
"scaleway-ecosystem"
"ovh-rtm"
"openstack-guest-utils"
"openstack-nova-agent"
2025-10-14 01:00:59 +01:00
)
2025-10-13 22:52:07 +01:00
# Common provider-created default users
local COMMON_PROVIDER_USERS = (
"ubuntu"
"debian"
"admin"
"cloud-user"
"ec2-user"
2025-10-16 09:43:21 +01:00
"linuxuser"
2025-10-13 22:52:07 +01:00
)
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
print_info "Scanning for provider-installed packages..."
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
for pkg in " ${ COMMON_PROVIDER_PKGS [@] } " ; do
if execute_check dpkg -l " $pkg " 2>/dev/null | grep -q '^ii' ; then
PROVIDER_PACKAGES += ( " $pkg " )
fi
done
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Detect associated services
print_info "Scanning for provider-related services..."
for pkg in " ${ PROVIDER_PACKAGES [@] } " ; do
local service_name = " ${ pkg } .service "
if execute_check systemctl list-unit-files " $service_name " 2>/dev/null | grep -q " $service_name " ; then
if execute_check systemctl is-enabled " $service_name " 2>/dev/null | grep -qE 'enabled|static' ; then
PROVIDER_SERVICES += ( " $service_name " )
fi
fi
done
2025-10-14 01:00:59 +01:00
2025-10-14 02:54:19 +01:00
# Check for provider-created users (excluding current admin user and script-managed user)
2025-10-13 22:52:07 +01:00
print_info "Scanning for default provisioning users..."
2025-10-14 02:54:19 +01:00
local MANAGED_USER = ""
if [ [ -f /root/.du_setup_managed_user ] ] ; then
2025-10-14 09:39:39 +01:00
MANAGED_USER = $( tr -d '[:space:]' < /root/.du_setup_managed_user 2>/dev/null)
2025-10-14 02:54:19 +01:00
log " Script-managed user detected: $MANAGED_USER (will be excluded from cleanup) "
fi
2025-10-13 22:52:07 +01:00
for user in " ${ COMMON_PROVIDER_USERS [@] } " ; do
2025-10-14 02:54:19 +01:00
if execute_check id " $user " & >/dev/null && \
[ [ " $user " != " $USERNAME " ] ] && \
[ [ " $user " != " $MANAGED_USER " ] ] ; then
2025-10-13 22:52:07 +01:00
PROVIDER_USERS += ( " $user " )
fi
done
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Audit root SSH keys
print_info "Auditing /root/.ssh/authorized_keys for unexpected keys..."
if [ [ -f /root/.ssh/authorized_keys ] ] ; then
local key_count
2025-10-14 21:10:23 +01:00
key_count = $( ( grep -cE '^ssh-(rsa|ed25519|ecdsa)' /root/.ssh/authorized_keys 2>/dev/null || echo 0) | tr -dc '0-9' )
if [ " $key_count " -gt 0 ] ; then
2025-10-13 22:52:07 +01:00
print_warning " Found $key_count SSH key(s) in /root/.ssh/authorized_keys "
ROOT_SSH_KEYS = ( "present" )
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
# Summary of findings
echo
print_info "=== Scan Results ==="
echo " Packages found: ${# PROVIDER_PACKAGES [@] } "
echo " Services found: ${# PROVIDER_SERVICES [@] } "
echo " Default users found: ${# PROVIDER_USERS [@] } "
echo " Root SSH keys: ${# ROOT_SSH_KEYS [@] } "
echo
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ ${# PROVIDER_PACKAGES [@] } -eq 0 && ${# PROVIDER_USERS [@] } -eq 0 && ${# ROOT_SSH_KEYS [@] } -eq 0 ] ] ; then
print_success "No common provider packages or users detected."
return 0
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info "=== PREVIEW: Showing what would be done ==="
echo
fi
2025-10-14 01:00:59 +01:00
2025-10-14 13:30:14 +01:00
# Audit and optionally clean up root SSH keys
2025-10-13 22:52:07 +01:00
if [ [ ${# ROOT_SSH_KEYS [@] } -gt 0 ] ] ; then
print_section "Root SSH Key Audit"
print_warning "SSH keys in /root/.ssh/authorized_keys can allow provider or previous admins access."
echo
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ YELLOW } Current keys in /root/.ssh/authorized_keys: ${ NC } "
2025-10-13 22:52:07 +01:00
awk '{print NR". "$0}' /root/.ssh/authorized_keys 2>/dev/null | head -20
echo
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info "[PREVIEW] Would offer to review and edit /root/.ssh/authorized_keys"
print_info " [PREVIEW] Would backup to $BACKUP_DIR /root_authorized_keys.backup.<timestamp> "
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
else
if confirm "Review and potentially remove root SSH keys?" "n" ; then
2025-10-14 09:42:04 +01:00
local backup_file
2025-10-14 10:20:38 +01:00
backup_file = " $BACKUP_DIR /root_authorized_keys.backup. $( date +%Y%m%d_%H%M%S) "
2025-10-14 01:00:59 +01:00
cp /root/.ssh/authorized_keys " $backup_file "
2025-10-13 22:52:07 +01:00
log " Backed up /root/.ssh/authorized_keys to $backup_file "
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
print_warning "IMPORTANT: Do NOT delete ALL keys or you'll be locked out!"
print_info "Opening /root/.ssh/authorized_keys for manual review..."
read -rp "Press Enter to continue..."
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
" ${ EDITOR :- nano } " /root/.ssh/authorized_keys
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ ! -s /root/.ssh/authorized_keys ] ] ; then
print_error "WARNING: authorized_keys is empty! This could lock you out."
2025-10-14 01:00:59 +01:00
if [ [ -f " $backup_file " ] ] && confirm "Restore from backup?" "y" ; then
cp " $backup_file " /root/.ssh/authorized_keys
2025-10-13 22:52:07 +01:00
print_info "Restored backup."
log "Restored /root/.ssh/authorized_keys from backup due to empty file."
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
local new_key_count
new_key_count = $( grep -cE '^ssh-(rsa|ed25519|ecdsa)' /root/.ssh/authorized_keys 2>/dev/null || echo 0)
print_info " Keys remaining: $new_key_count "
log " Root SSH keys audit completed. Keys remaining: $new_key_count "
else
print_info "Skipping root SSH key audit."
fi
fi
echo
fi
2025-10-14 01:00:59 +01:00
2025-10-14 13:30:14 +01:00
# Special handling for cloud-init due to its complexity
2025-10-13 22:52:07 +01:00
if [ [ " ${ PROVIDER_PACKAGES [*] } " = ~ " cloud-init " ] ] ; then
print_section "Cloud-Init Management"
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } ℹ cloud-init${ NC } "
printf ' Purpose: Initial VM provisioning (SSH keys, hostname, network)\n'
printf ' %s\n' " ${ YELLOW } Official recommendation: DISABLE rather than remove ${ NC } "
printf ' Benefits of disabling vs removing:\n'
printf ' - Can be re-enabled if needed for reprovisioning\n'
printf ' - Safer than package removal\n'
printf ' - No dependency issues\n'
printf '\n'
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] || confirm "Disable cloud-init (recommended over removal)?" "y" ; then
print_info "Disabling cloud-init..."
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if ! [ [ -f /etc/cloud/cloud-init.disabled ] ] ; then
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info "[PREVIEW] Would create /etc/cloud/cloud-init.disabled"
else
execute_command touch /etc/cloud/cloud-init.disabled
print_success "Created /etc/cloud/cloud-init.disabled"
log "Created /etc/cloud/cloud-init.disabled"
fi
else
print_info "/etc/cloud/cloud-init.disabled already exists."
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
local cloud_services = (
"cloud-init.service"
"cloud-init-local.service"
"cloud-config.service"
"cloud-final.service"
)
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
for service in " ${ cloud_services [@] } " ; do
if execute_check systemctl is-enabled " $service " & >/dev/null; then
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info " [PREVIEW] Would stop and disable $service "
else
execute_command systemctl stop " $service " 2>/dev/null || true
execute_command systemctl disable " $service " 2>/dev/null || true
print_success " Disabled $service "
log " Disabled $service "
fi
fi
done
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "false" ] ] ; then
print_success "cloud-init disabled successfully."
print_info "To re-enable: sudo rm /etc/cloud/cloud-init.disabled && systemctl enable cloud-init.service"
fi
2025-10-14 10:16:36 +01:00
local filtered_packages = ( )
for pkg in " ${ PROVIDER_PACKAGES [@] } " ; do
if [ [ " $pkg " != "cloud-init" && -n " $pkg " ] ] ; then
filtered_packages += ( " $pkg " )
fi
done
PROVIDER_PACKAGES = ( " ${ filtered_packages [@] } " )
2025-10-13 22:52:07 +01:00
else
print_info "Keeping cloud-init enabled."
fi
echo
fi
2025-10-14 01:00:59 +01:00
2025-10-14 13:30:14 +01:00
# Remove identified provider packages
2025-10-13 22:52:07 +01:00
if [ [ ${# PROVIDER_PACKAGES [@] } -gt 0 ] ] ; then
print_section "Provider Package Removal"
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
for pkg in " ${ PROVIDER_PACKAGES [@] } " ; do
[ [ -z " $pkg " ] ] && continue
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
case " $pkg " in
qemu-guest-agent)
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ RED } ⚠ $pkg ${ NC } "
printf ' Purpose: VM-host communication for snapshots and graceful shutdowns\n'
printf ' %s\n' " ${ RED } CRITICAL RISKS if removed: ${ NC } "
printf ' - Snapshot backups will FAIL or be inconsistent\n'
printf ' - Console access may break\n'
printf ' - Graceful shutdowns replaced with forced stops\n'
printf ' - Provider backup systems will malfunction\n'
printf ' %s\n' " ${ RED } STRONGLY RECOMMENDED to keep ${ NC } "
2025-10-13 22:52:07 +01:00
; ;
*-agent| *-monitoring)
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ YELLOW } ⚠ $pkg ${ NC } "
printf ' Purpose: Provider monitoring/management\n'
printf ' Risks if removed:\n'
printf ' - Provider dashboard metrics will disappear\n'
printf ' - May affect support troubleshooting\n'
printf ' %s\n' " ${ YELLOW } Remove only if you don't need provider monitoring ${ NC } "
2025-10-13 22:52:07 +01:00
; ;
*)
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } ℹ $pkg ${ NC } "
printf ' Purpose: Provider-specific tooling\n'
printf ' %s\n' " ${ YELLOW } Review before removing ${ NC } "
2025-10-13 22:52:07 +01:00
; ;
esac
2025-10-16 11:29:53 +01:00
printf '\n'
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] || confirm " Remove $pkg ? " "n" ; then
if [ [ " $pkg " = = "qemu-guest-agent" && " $CLEANUP_PREVIEW " = = "false" ] ] ; then
print_error "FINAL WARNING: Removing qemu-guest-agent will break backups and console access!"
if ! confirm "Are you ABSOLUTELY SURE?" "n" ; then
print_info " Keeping $pkg (wise choice). "
continue
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
local service_name = " ${ pkg } .service "
if execute_check systemctl is-active " $service_name " & >/dev/null; then
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info " [PREVIEW] Would stop and disable $service_name "
else
print_info " Stopping $service_name ... "
execute_command systemctl stop " $service_name " 2>/dev/null || true
execute_command systemctl disable " $service_name " 2>/dev/null || true
log " Stopped and disabled $service_name "
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info " [PREVIEW] Would remove package: $pkg (with --purge flag) "
log " [PREVIEW] Would remove provider package: $pkg "
else
print_info " Removing $pkg ... "
if execute_command apt-get remove --purge -y " $pkg " 2>& 1 | tee -a " $LOG_FILE " ; then
print_success " $pkg removed. "
log " Removed provider package: $pkg "
else
print_error " Failed to remove $pkg . Check logs. "
log " Failed to remove: $pkg "
fi
fi
else
print_info " Keeping $pkg . "
fi
done
echo
fi
2025-10-14 01:00:59 +01:00
2025-10-14 13:30:14 +01:00
# Check and remove default users
2025-10-13 22:52:07 +01:00
if [ [ ${# PROVIDER_USERS [@] } -gt 0 ] ] ; then
print_section "Provider User Cleanup"
print_warning "Default users created during provisioning can be security risks."
echo
2025-10-14 02:28:05 +01:00
2025-10-13 22:52:07 +01:00
for user in " ${ PROVIDER_USERS [@] } " ; do
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ YELLOW } Found user: $user ${ NC } "
2025-10-14 01:00:59 +01:00
2025-10-14 03:31:29 +01:00
local proc_count
2025-10-14 21:10:23 +01:00
proc_count = $( ( ps -u " $user " --no-headers 2>/dev/null || true ) | wc -l)
2025-10-14 03:31:29 +01:00
if [ [ $proc_count -gt 0 ] ] ; then
print_warning " User $user has $proc_count running process(es). "
2025-10-14 02:25:16 +01:00
fi
if [ [ -d " /home/ $user " ] ] && [ [ -f " /home/ $user /.ssh/authorized_keys " ] ] ; then
local key_count = 0
2025-10-14 21:10:23 +01:00
key_count = $( ( grep -cE '^ssh-(rsa|ed25519|ecdsa)' " /home/ $user /.ssh/authorized_keys " 2>/dev/null || echo 0) | tr -dc '0-9' )
if [ " $key_count " -gt 0 ] ; then
2025-10-13 22:52:07 +01:00
print_warning " User $user has $key_count SSH key(s) configured. "
fi
fi
2025-10-14 02:28:05 +01:00
2025-10-14 02:25:16 +01:00
if id -nG " $user " 2>/dev/null | grep -qwE '(sudo|admin)' ; then
2025-10-13 22:52:07 +01:00
print_warning " User $user has sudo/admin privileges! "
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
echo
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] || confirm " Remove user $user and their home directory? " "n" ; then
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info " [PREVIEW] Would terminate processes owned by $user "
print_info " [PREVIEW] Would remove user $user with home directory "
if [ [ -f " /etc/sudoers.d/ $user " ] ] ; then
print_info " [PREVIEW] Would remove /etc/sudoers.d/ $user "
fi
log " [PREVIEW] Would remove provider user: $user "
else
if [ [ $proc_count -gt 1 ] ] ; then
print_info " Terminating processes owned by $user ... "
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
execute_command pkill -u " $user " 2>/dev/null || true
sleep 2
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if ps -u " $user " & >/dev/null; then
print_warning "Some processes didn't terminate gracefully. Force killing..."
execute_command pkill -9 -u " $user " 2>/dev/null || true
sleep 1
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if ps -u " $user " & >/dev/null; then
print_error " Unable to kill all processes for $user . Manual intervention needed. "
log " Failed to terminate all processes for user: $user "
continue
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
print_info " Removing user $user ... "
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
local user_removed = false
if command -v deluser & >/dev/null; then
if execute_command deluser --remove-home " $user " 2>& 1 | tee -a " $LOG_FILE " ; then
user_removed = true
fi
else
if execute_command userdel -r " $user " 2>& 1 | tee -a " $LOG_FILE " ; then
user_removed = true
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $user_removed " = = "true" ] ] ; then
print_success " User $user removed. "
log " Removed provider user: $user "
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ -f " /etc/sudoers.d/ $user " ] ] ; then
execute_command rm -f " /etc/sudoers.d/ $user "
print_info " Removed sudo configuration for $user . "
fi
else
print_error " Failed to remove user $user . Check logs. "
log " Failed to remove user: $user "
fi
fi
else
print_info " Keeping user $user . "
fi
done
echo
fi
2025-10-14 01:00:59 +01:00
2025-10-14 13:30:14 +01:00
# Final cleanup step
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] || confirm "Remove residual configuration files and unused dependencies?" "y" ; then
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info "[PREVIEW] Would run: apt-get autoremove --purge -y"
print_info "[PREVIEW] Would run: apt-get autoclean -y"
else
print_info "Cleaning up..."
execute_command apt-get autoremove --purge -y 2>& 1 | tee -a " $LOG_FILE " || true
execute_command apt-get autoclean -y 2>& 1 | tee -a " $LOG_FILE " || true
print_success "Cleanup complete."
log "Ran apt autoremove and autoclean."
fi
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
log "Provider package cleanup completed."
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
echo
print_success "=== PREVIEW COMPLETED ==="
print_info "No changes were made to the system."
print_info "Run without --cleanup-preview flag to execute these actions."
else
print_success "Cleanup function completed successfully."
fi
}
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
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } $prompt ${ NC } " ) " response
2025-06-26 16:22:23 +01:00
response = ${ response ,, }
if [ [ -z $response ] ] ; then
response = $default
fi
case $response in
y| yes) return 0 ; ;
n| no) return 1 ; ;
2025-10-16 11:29:53 +01:00
*) printf '%s\n' " ${ RED } Please answer yes or no. ${ NC } " ; ;
2025-06-26 16:22:23 +01:00
esac
done
}
# --- VALIDATION FUNCTIONS ---
validate_username( ) {
local username = " $1 "
[ [ " $username " = ~ ^[ a-z_] [ a-z0-9_-] *$ && ${# username } -le 32 ] ]
}
validate_hostname( ) {
local hostname = " $1 "
[ [ " $hostname " = ~ ^[ a-zA-Z0-9] [ a-zA-Z0-9.-] { 0,253} [ a-zA-Z0-9] $ && ! " $hostname " = ~ \. \. ] ]
}
validate_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " -ge 1024 && " $port " -le 65535 ] ]
}
2025-06-28 12:24:23 +01:00
validate_backup_port( ) {
local port = " $1 "
[ [ " $port " = ~ ^[ 0-9] +$ && " $port " -ge 1 && " $port " -le 65535 ] ]
}
2025-06-26 16:22:23 +01:00
validate_ssh_key( ) {
local key = " $1 "
[ [ -n " $key " && " $key " = ~ ^( ssh-rsa| ecdsa-sha2-nistp256| ecdsa-sha2-nistp384| ecdsa-sha2-nistp521| ssh-ed25519) \ ] ]
}
validate_timezone( ) {
local tz = " $1 "
[ [ -e " /usr/share/zoneinfo/ $tz " ] ]
}
validate_swap_size( ) {
2025-06-26 22:19:35 +01:00
local size_upper = " ${ 1 ^^ } " # Convert to uppercase for case-insensitivity
[ [ " $size_upper " = ~ ^[ 0-9] +[ MG] $ && " ${ size_upper %[MG] } " -ge 1 ] ]
2025-06-26 16:22:23 +01:00
}
validate_ufw_port( ) {
local port = " $1 "
2025-06-26 21:49:51 +01:00
# Matches port (e.g., 8080) or port/protocol (e.g., 8080/tcp, 123/udp)
2025-06-26 16:22:23 +01:00
[ [ " $port " = ~ ^[ 0-9] +( /tcp| /udp) ?$ ] ]
}
convert_to_bytes( ) {
2025-06-26 22:19:35 +01:00
local size_upper = " ${ 1 ^^ } " # Convert to uppercase for case-insensitivity
local unit = " ${ size_upper : -1 } "
local value = " ${ size_upper %[MG] } "
2025-06-26 16:22:23 +01:00
if [ [ " $unit " = = "G" ] ] ; then
echo $(( value * 1024 * 1024 * 1024 ))
elif [ [ " $unit " = = "M" ] ] ; then
echo $(( value * 1024 * 1024 ))
else
echo 0
fi
}
2025-07-15 15:13:58 +01:00
# --- script update check ---
run_update_check( ) {
print_section "Checking for Script Updates"
local latest_version
# Fetch the latest script from GitHub and parse the version number from it.
if ! latest_version = $( curl -sL " $SCRIPT_URL " | grep '^CURRENT_VERSION=' | head -n 1 | awk -F'"' '{print $2}' ) ; then
print_warning "Could not check for updates. Please check your internet connection."
log " Update check failed: Could not fetch script from $SCRIPT_URL "
return
fi
if [ [ -z " $latest_version " ] ] ; then
print_warning "Failed to find the version number in the remote script."
log "Update check failed: Could not parse version string from remote script."
return
fi
local lower_version
lower_version = $( printf '%s\n' " $CURRENT_VERSION " " $latest_version " | sort -V | head -n 1)
if [ [ " $lower_version " = = " $CURRENT_VERSION " && " $CURRENT_VERSION " != " $latest_version " ] ] ; then
print_success " A new version ( $latest_version ) is available! "
if ! confirm " Would you like to update to version $latest_version now? " ; then
return
fi
local temp_dir
if ! temp_dir = $( mktemp -d) ; then
print_error "Failed to create temporary directory. Update aborted."
exit 1
fi
trap 'rm -rf -- "$temp_dir"' EXIT
local temp_script = " $temp_dir /du_setup.sh "
local temp_checksum = " $temp_dir /checksum.sha256 "
print_info "Downloading new script version..."
if ! curl -sL " $SCRIPT_URL " -o " $temp_script " ; then
print_error "Failed to download the new script. Update aborted."
exit 1
fi
print_info "Downloading checksum..."
if ! curl -sL " $CHECKSUM_URL " -o " $temp_checksum " ; then
print_error "Failed to download the checksum file. Update aborted."
exit 1
fi
print_info "Verifying checksum..."
if ! ( cd " $temp_dir " && sha256sum -c "checksum.sha256" --quiet) ; then
print_error "Checksum verification failed! The downloaded file may be corrupt. Update aborted."
exit 1
fi
print_success "Checksum verified successfully."
print_info "Checking script syntax..."
if ! bash -n " $temp_script " ; then
print_error "Downloaded file has a syntax error. Update aborted to prevent issues."
exit 1
fi
print_success "Syntax check passed."
if ! mv " $temp_script " " $0 " ; then
print_error "Failed to replace the old script file. You may need to run 'mv' manually."
exit 1
fi
chmod +x " $0 "
trap - EXIT
rm -rf -- " $temp_dir "
print_success "Update successful. Please run the script again to use the new version."
exit 0
else
print_info " You are running the latest version ( $CURRENT_VERSION ). "
log " No new version found. Current: $CURRENT_VERSION , Latest: $latest_version "
fi
}
2025-06-26 21:49:51 +01:00
# --- CORE FUNCTIONS ---
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
check_dependencies( ) {
print_section "Checking Dependencies"
local missing_deps = ( )
command -v curl >/dev/null || missing_deps += ( "curl" )
command -v sudo >/dev/null || missing_deps += ( "sudo" )
command -v gpg >/dev/null || missing_deps += ( "gpg" )
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
if [ [ ${# missing_deps [@] } -gt 0 ] ] ; then
print_info " Installing missing dependencies: ${ missing_deps [*] } "
if ! apt-get update -qq || ! apt-get install -y -qq " ${ missing_deps [@] } " ; then
print_error " Failed to install dependencies: ${ missing_deps [*] } "
exit 1
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
print_success "Dependencies installed."
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
print_success "All essential dependencies are installed."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
log "Dependency check completed."
2025-06-26 16:22:23 +01:00
}
check_system( ) {
print_section "System Compatibility Check"
if [ [ $( id -u) -ne 0 ] ] ; then
2025-06-30 10:35:43 +01:00
print_error "This script must be run as root (e.g., sudo ./du_setup.sh)."
2025-06-26 16:22:23 +01:00
exit 1
fi
print_success "Running with root privileges."
if [ [ -f /proc/1/cgroup ] ] && grep -qE '(docker|lxc|kubepod)' /proc/1/cgroup; then
IS_CONTAINER = true
print_warning "Container environment detected. Some features (like swap) will be skipped."
fi
if [ [ -f /etc/os-release ] ] ; then
source /etc/os-release
2025-06-27 00:36:26 +01:00
ID = $ID # Populate global ID variable
2025-08-15 14:14:57 +01:00
if [ [ $ID = = "debian" && $VERSION_ID = ~ ^( 12| 13) $ ] ] || \
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
2025-08-15 14:14:57 +01:00
print_warning " Script not tested on $PRETTY_NAME . This is for Debian 12/13 or Ubuntu 20.04/22.04/24.04 LTS. "
2025-06-26 16:22:23 +01:00
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-10-14 13:04:55 +01:00
elif pgrep -q sshd; then
2025-06-26 21:49:51 +01:00
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
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter username for new admin user: ${ NC } " ) " USERNAME
2025-06-26 21:49:51 +01:00
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
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter server hostname: ${ NC } " ) " SERVER_NAME
2025-06-26 21:49:51 +01:00
if validate_hostname " $SERVER_NAME " ; then break; else print_error "Invalid hostname." ; fi
done
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter a 'pretty' hostname (optional): ${ NC } " ) " PRETTY_NAME
2025-06-26 21:49:51 +01:00
[ [ -z " $PRETTY_NAME " ] ] && PRETTY_NAME = " $SERVER_NAME "
while true; do
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter custom SSH port (1024-65535) [2222]: ${ NC } " ) " SSH_PORT
2025-06-26 21:49:51 +01:00
SSH_PORT = ${ SSH_PORT :- 2222 }
if validate_port " $SSH_PORT " ; then break; else print_error "Invalid port number." ; fi
done
2025-08-26 11:40:41 +01:00
SERVER_IP_V4 = $( curl -4 -s https://ifconfig.me 2>/dev/null || echo "unknown" )
SERVER_IP_V6 = $( curl -6 -s https://ifconfig.me 2>/dev/null || echo "not available" )
2025-08-26 16:14:58 +01:00
if [ [ " $SERVER_IP_V4 " != "unknown" ] ] ; then
print_info " Detected server IPv4: $SERVER_IP_V4 "
fi
2025-08-26 11:40:41 +01:00
if [ [ " $SERVER_IP_V6 " != "not available" ] ] ; then
print_info " Detected server IPv6: $SERVER_IP_V6 "
fi
2025-10-16 12:29:34 +01:00
printf '\n%s\n' " ${ YELLOW } Configuration Summary: ${ NC } "
2025-08-26 15:41:00 +01:00
printf " %-15s %s\n" "Username:" " $USERNAME "
printf " %-15s %s\n" "Hostname:" " $SERVER_NAME "
printf " %-15s %s\n" "SSH Port:" " $SSH_PORT "
2025-08-26 16:14:58 +01:00
if [ [ " $SERVER_IP_V4 " != "unknown" ] ] ; then
printf " %-15s %s\n" "Server IPv4:" " $SERVER_IP_V4 "
fi
2025-08-26 11:40:41 +01:00
if [ [ " $SERVER_IP_V6 " != "not available" ] ] ; then
2025-08-26 15:41:00 +01:00
printf " %-15s %s\n" "Server IPv6:" " $SERVER_IP_V6 "
2025-08-26 11:40:41 +01:00
fi
2025-06-26 21:49:51 +01:00
if ! confirm "\nContinue with this configuration?" "y" ; then print_info "Exiting." ; exit 0; fi
2025-08-26 11:40:41 +01:00
log " Configuration collected: USER= $USERNAME , HOST= $SERVER_NAME , PORT= $SSH_PORT , IPV4= $SERVER_IP_V4 , IPV6= $SERVER_IP_V6 "
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 \
2025-06-28 11:43:10 +01:00
rsync wget vim htop iotop nethogs netcat-traditional ncdu \
tree rsyslog cron jq gawk coreutils perl skopeo git \
2025-08-10 23:30:33 +01:00
ssh 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-07-02 17:25:17 +01:00
local USER_HOME SSH_DIR AUTH_KEYS PASS1 PASS2 SSH_PUBLIC_KEY TEMP_KEY_FILE
if [ [ -z " $USERNAME " ] ] ; then
print_error "USERNAME variable is not set. Cannot proceed with user setup."
exit 1
fi
2025-06-26 16:22:23 +01:00
if [ [ $USER_EXISTS = = false ] ] ; then
print_info " Creating user ' $USERNAME '... "
if ! adduser --disabled-password --gecos "" " $USERNAME " ; then
print_error " Failed to create user ' $USERNAME '. "
exit 1
fi
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
2025-10-16 18:09:47 +01:00
read -rsp " $( printf '%s' " ${ CYAN } New password: ${ NC } " ) " PASS1
2025-06-26 16:22:23 +01:00
echo
2025-10-16 18:09:47 +01:00
read -rsp " $( printf '%s' " ${ CYAN } Retype new password: ${ NC } " ) " PASS2
2025-06-26 16:22:23 +01:00
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
if echo " $USERNAME : $PASS1 " | chpasswd >/dev/null 2>& 1; then
2025-06-26 16:22:23 +01:00
print_success " Password for ' $USERNAME ' updated. "
break
else
2025-06-26 22:19:35 +01:00
print_error "Failed to set password. This could be a permissions issue."
2025-06-26 16:22:23 +01:00
print_info "Try again or press Enter twice to skip."
log " Failed to set password for ' $USERNAME '. "
fi
else
print_error "Passwords do not match. Please try again."
fi
done
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
AUTH_KEYS = " $SSH_DIR /authorized_keys "
2025-07-02 16:35:44 +01:00
# Check if home directory is writable
if [ [ ! -w " $USER_HOME " ] ] ; then
print_error " Home directory $USER_HOME is not writable by $USERNAME . "
print_info "Attempting to fix permissions..."
chown " $USERNAME : $USERNAME " " $USER_HOME "
chmod 700 " $USER_HOME "
if [ [ ! -w " $USER_HOME " ] ] ; then
print_error " Failed to make $USER_HOME writable. Check filesystem permissions. "
exit 1
2025-06-27 14:53:54 +01:00
fi
2025-07-02 16:35:44 +01:00
log " Fixed permissions for $USER_HOME . "
fi
2025-06-27 14:53:54 +01:00
2025-07-02 16:35:44 +01:00
if confirm "Add SSH public key(s) from your local machine now?" ; then
while true; do
local SSH_PUBLIC_KEY
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Paste your full SSH public key: ${ NC } " ) " SSH_PUBLIC_KEY
2025-07-02 16:35:44 +01:00
if validate_ssh_key " $SSH_PUBLIC_KEY " ; then
mkdir -p " $SSH_DIR "
chmod 700 " $SSH_DIR "
chown " $USERNAME : $USERNAME " " $SSH_DIR "
echo " $SSH_PUBLIC_KEY " >> " $AUTH_KEYS "
awk '!seen[$0]++' " $AUTH_KEYS " > " $AUTH_KEYS .tmp " && mv " $AUTH_KEYS .tmp " " $AUTH_KEYS "
chmod 600 " $AUTH_KEYS "
chown " $USERNAME : $USERNAME " " $AUTH_KEYS "
print_success "SSH public key added."
log " Added SSH public key for ' $USERNAME '. "
LOCAL_KEY_ADDED = true
else
print_error "Invalid SSH key format. It should start with 'ssh-rsa', 'ecdsa-*', or 'ssh-ed25519'."
fi
if ! confirm "Do you have another SSH public key to add?" "n" ; then
print_info "Finished adding SSH keys."
break
fi
done
else
print_info " No local SSH key provided. Generating a new key pair for ' $USERNAME '. "
log "User opted not to provide a local SSH key. Generating a new one."
2025-07-02 17:25:17 +01:00
if ! command -v ssh-keygen >/dev/null 2>& 1; then
print_error "ssh-keygen not found. Please install openssh-client."
exit 1
fi
if [ [ ! -w /tmp ] ] ; then
print_error "Cannot write to /tmp. Unable to create temporary key file."
exit 1
fi
2025-07-02 16:35:44 +01:00
mkdir -p " $SSH_DIR "
chmod 700 " $SSH_DIR "
chown " $USERNAME : $USERNAME " " $SSH_DIR "
2025-07-02 22:09:11 +01:00
# Generate user key pair for login
if ! sudo -u " $USERNAME " ssh-keygen -t ed25519 -f " $SSH_DIR /id_ed25519_user " -N "" -q; then
print_error " Failed to generate user SSH key for ' $USERNAME '. "
2025-07-02 16:35:44 +01:00
exit 1
2025-06-27 14:53:54 +01:00
fi
2025-07-02 22:09:11 +01:00
cat " $SSH_DIR /id_ed25519_user.pub " >> " $AUTH_KEYS "
2025-07-02 16:35:44 +01:00
chmod 600 " $AUTH_KEYS "
chown " $USERNAME : $USERNAME " " $AUTH_KEYS "
print_success "SSH key generated and added to authorized_keys."
2025-07-02 22:09:11 +01:00
log " Generated and added user SSH key for ' $USERNAME '. "
if ! sudo -u " $USERNAME " ssh-keygen -t ed25519 -f " $SSH_DIR /id_ed25519_server " -N "" -q; then
print_error " Failed to generate server SSH key for ' $USERNAME '. "
exit 1
fi
print_success "Server SSH key generated (not shared)."
log " Generated server SSH key for ' $USERNAME '. "
2025-07-02 16:35:44 +01:00
2025-07-02 17:25:17 +01:00
TEMP_KEY_FILE = " /tmp/ ${ USERNAME } _ssh_key_ $( date +%s) "
trap 'rm -f "$TEMP_KEY_FILE" 2>/dev/null' EXIT
2025-07-02 22:09:11 +01:00
cp " $SSH_DIR /id_ed25519_user " " $TEMP_KEY_FILE "
2025-07-02 16:35:44 +01:00
chmod 600 " $TEMP_KEY_FILE "
chown root:root " $TEMP_KEY_FILE "
2025-10-16 10:08:03 +01:00
printf '\n'
printf '%s\n' " ${ YELLOW } ⚠ SECURITY WARNING: The SSH key pair below is your only chance to access ' $USERNAME ' via SSH. ${ NC } "
printf '%s\n' " ${ YELLOW } ⚠ Anyone with the private key can access your server. Secure it immediately. ${ NC } "
printf '\n'
printf '%s\n' " ${ PURPLE } ℹ ACTION REQUIRED: Save the keys to your local machine:${ NC } "
printf '%s\n' " ${ CYAN } 1. Save the PRIVATE key to ~/.ssh/ ${ USERNAME } _key: ${ NC } "
printf '%s\n' " ${ RED } vvvv PRIVATE KEY BELOW THIS LINE vvvv ${ NC } "
2025-07-02 16:35:44 +01:00
cat " $TEMP_KEY_FILE "
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ RED } ^^^^ PRIVATE KEY ABOVE THIS LINE ^^^^^ ${ NC } "
printf '\n'
printf '%s\n' " ${ CYAN } 2. Save the PUBLIC key to verify or use elsewhere: ${ NC } "
printf '====SSH PUBLIC KEY BELOW THIS LINE====\n'
2025-07-02 22:09:11 +01:00
cat " $SSH_DIR /id_ed25519_user.pub "
2025-10-16 10:08:03 +01:00
printf '====SSH PUBLIC KEY END====\n'
printf '\n'
printf '%s\n' " ${ CYAN } 3. On your local machine, set permissions for the private key: ${ NC } "
printf '%s\n' " ${ CYAN } chmod 600 ~/.ssh/ ${ USERNAME } _key ${ NC } "
printf '%s\n' " ${ CYAN } 4. Connect to the server using: ${ NC } "
2025-08-26 16:26:40 +01:00
if [ [ " $SERVER_IP_V4 " != "unknown" ] ] ; then
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ CYAN } ssh -i ~/.ssh/ ${ USERNAME } _key -p $SSH_PORT $USERNAME @ $SERVER_IP_V4 ${ NC } "
2025-08-26 16:26:40 +01:00
fi
if [ [ " $SERVER_IP_V6 " != "not available" ] ] ; then
2025-10-16 10:08:03 +01:00
printf '%s\n' " ${ CYAN } ssh -i ~/.ssh/ ${ USERNAME } _key -p $SSH_PORT $USERNAME @ $SERVER_IP_V6 ${ NC } "
2025-08-26 16:26:40 +01:00
fi
2025-10-16 10:08:03 +01:00
printf '\n'
printf '%s\n' " ${ PURPLE } ℹ The private key file ($TEMP_KEY_FILE ) will be deleted after this step. ${ NC } "
read -rp " $( printf '%s' " ${ CYAN } Press Enter after you have saved the keys securely... ${ NC } " ) "
2025-07-02 16:35:44 +01:00
print_info "Temporary key file deleted."
LOCAL_KEY_ADDED = true
fi
2025-06-26 16:22:23 +01:00
print_success " User ' $USERNAME ' created. "
2025-10-14 02:54:19 +01:00
echo " $USERNAME " > /root/.du_setup_managed_user
chmod 600 /root/.du_setup_managed_user
log " Marked ' $USERNAME ' as script-managed user (excluded from provider cleanup) "
2025-06-26 16:22:23 +01:00
else
print_info " Using existing user: $USERNAME "
2025-10-14 02:54:19 +01:00
if [ [ ! -f /root/.du_setup_managed_user ] ] ; then
echo " $USERNAME " > /root/.du_setup_managed_user
chmod 600 /root/.du_setup_managed_user
log " Marked existing user ' $USERNAME ' as script-managed "
fi
2025-06-26 16:22:23 +01:00
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
AUTH_KEYS = " $SSH_DIR /authorized_keys "
2025-10-14 13:04:55 +01:00
if [ [ ! -s " $AUTH_KEYS " ] ] || ! grep -qE '^(ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519) ' " $AUTH_KEYS " 2>/dev/null; then
2025-07-02 16:35:44 +01:00
print_warning " No valid SSH keys found in $AUTH_KEYS for existing user ' $USERNAME '. "
print_info " You must manually add a public key to $AUTH_KEYS to enable SSH access. "
log " No valid SSH keys found for existing user ' $USERNAME '. "
fi
2025-06-26 16:22:23 +01:00
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"
2025-08-15 14:14:57 +01:00
# Warn about /tmp being a RAM-backed filesystem on Debian 13+
print_info "Note: Debian 13 uses tmpfs for /tmp by default (stored in RAM)"
print_info "Large temporary files may consume system memory"
2025-06-26 16:22:23 +01:00
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
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter desired timezone (e.g., Europe/London, America/New_York) [Etc/UTC]: ${ NC } " ) " TIMEZONE
2025-06-26 21:49:51 +01:00
TIMEZONE = ${ TIMEZONE :- Etc /UTC }
if validate_timezone " $TIMEZONE " ; then
if [ [ $( timedatectl status | grep "Time zone" | awk '{print $3}' ) != " $TIMEZONE " ] ] ; then
timedatectl set-timezone " $TIMEZONE "
print_success " Timezone set to $TIMEZONE . "
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
2025-08-19 20:02:03 +01:00
print_info "Applying new locale settings to the current session..."
if [ [ -f /etc/default/locale ] ] ; then
2025-10-14 21:46:39 +01:00
# shellcheck disable=SC1091
2025-08-19 20:02:03 +01:00
. /etc/default/locale
2025-10-14 21:46:39 +01:00
# shellcheck disable=SC2046
2025-08-19 20:02:03 +01:00
export $( grep -v '^#' /etc/default/locale | cut -d= -f1)
print_success "Locale environment updated for this session."
log "Sourced /etc/default/locale to update script's environment."
else
print_warning "Could not find /etc/default/locale to update session environment."
fi
2025-06-26 16:22:23 +01:00
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."
}
2025-06-30 21:10:35 +01:00
cleanup_and_exit( ) {
local exit_code = $?
if [ [ $exit_code -ne 0 && $( type -t rollback_ssh_changes) = = "function" ] ] ; then
print_error " An error occurred. Rolling back SSH changes to port $PREVIOUS_SSH_PORT ... "
rollback_ssh_changes
if [ [ $? -ne 0 ] ] ; then
print_error " Rollback failed. SSH may not be accessible. Please check 'systemctl status $SSH_SERVICE ' and 'journalctl -u $SSH_SERVICE '. "
fi
fi
2025-10-13 16:39:04 +01:00
trap - ERR
2025-06-30 21:10:35 +01:00
exit $exit_code
}
2025-06-26 16:22:23 +01:00
configure_ssh( ) {
2025-06-30 21:10:35 +01:00
trap cleanup_and_exit ERR
2025-06-26 16:22:23 +01:00
print_section "SSH Hardening"
2025-06-30 21:10:35 +01:00
local CURRENT_SSH_PORT USER_HOME SSH_DIR SSH_KEY AUTH_KEYS NEW_SSH_CONFIG PREVIOUS_SSH_PORT
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
2025-06-30 21:10:35 +01:00
print_error "openssh-server package is not installed."
return 1
2025-06-26 16:22:23 +01:00
fi
2025-06-30 21:10:35 +01:00
# Detect SSH service name
2025-06-27 00:54:10 +01:00
if [ [ $ID = = "ubuntu" ] ] && systemctl is-active ssh.socket >/dev/null 2>& 1; then
SSH_SERVICE = "ssh.socket"
print_info " Using SSH socket activation: $SSH_SERVICE "
elif [ [ $ID = = "ubuntu" ] ] && { systemctl is-enabled ssh.service >/dev/null 2>& 1 || systemctl is-active ssh.service >/dev/null 2>& 1; } ; then
2025-06-26 21:49:51 +01:00
SSH_SERVICE = "ssh.service"
elif systemctl is-enabled sshd.service >/dev/null 2>& 1 || systemctl is-active sshd.service >/dev/null 2>& 1; then
SSH_SERVICE = "sshd.service"
2025-06-26 16:22:23 +01:00
else
2025-06-30 21:10:35 +01:00
print_error "No SSH service or daemon detected."
return 1
2025-06-26 16:22:23 +01:00
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-10-13 14:45:15 +01:00
print_info "Backing up original SSH config..."
SSHD_BACKUP_FILE = " $BACKUP_DIR /sshd_config.backup_ $( date +%Y%m%d_%H%M%S) "
cp /etc/ssh/sshd_config " $SSHD_BACKUP_FILE "
2025-06-30 21:10:35 +01:00
# Store the current active port as the previous port
PREVIOUS_SSH_PORT = $( ss -tuln | grep -E " :(22|.* $SSH_SERVICE .*) " | awk '{print $5}' | cut -d':' -f2 | head -n1 || echo "22" )
CURRENT_SSH_PORT = $PREVIOUS_SSH_PORT
2025-06-26 16:22:23 +01:00
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
SSH_DIR = " $USER_HOME /.ssh "
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-30 21:10:35 +01:00
print_info "No local key provided. Generating new SSH key..."
2025-10-13 11:20:46 +01:00
mkdir -p " $SSH_DIR " ; chmod 700 " $SSH_DIR " ; chown " $USERNAME : $USERNAME " " $SSH_DIR "
2025-06-30 21:10:35 +01:00
sudo -u " $USERNAME " ssh-keygen -t ed25519 -f " $SSH_DIR /id_ed25519 " -N "" -q
cat " $SSH_DIR /id_ed25519.pub " >> " $AUTH_KEYS "
2025-10-13 11:20:46 +01:00
# Verify the key was added
if [ [ ! -s " $AUTH_KEYS " ] ] ; then
print_error "Failed to create authorized_keys file."
return 1
fi
2025-06-30 21:10:35 +01:00
chmod 600 " $AUTH_KEYS " ; chown -R " $USERNAME : $USERNAME " " $SSH_DIR "
2025-06-26 16:22:23 +01:00
print_success "SSH key generated."
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ YELLOW } Public key for remote access: ${ NC } " ; cat " $SSH_DIR /id_ed25519.pub "
2025-06-26 16:22:23 +01:00
fi
print_warning "SSH Key Authentication Required for Next Steps!"
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ CYAN } Test SSH access from a SEPARATE terminal now: ${ NC } "
2025-10-13 16:39:04 +01:00
if [ [ -n " $SERVER_IP_V4 " && " $SERVER_IP_V4 " != "unknown" ] ] ; then
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ CYAN } Using IPv4: ssh -p $CURRENT_SSH_PORT $USERNAME @ $SERVER_IP_V4 ${ NC } "
2025-08-26 16:14:58 +01:00
fi
2025-10-13 16:39:04 +01:00
if [ [ -n " $SERVER_IP_V6 " && " $SERVER_IP_V6 " != "not available" ] ] ; then
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ CYAN } Using IPv6: ssh -p $CURRENT_SSH_PORT $USERNAME @ $SERVER_IP_V6 ${ NC } "
2025-08-26 11:40:41 +01:00
fi
2025-06-26 16:22:23 +01:00
if ! confirm "Can you successfully log in using your SSH key?" ; then
2025-06-30 21:10:35 +01:00
print_error "SSH key authentication is mandatory to proceed."
return 1
2025-06-26 16:22:23 +01:00
fi
2025-06-30 21:10:35 +01:00
# Apply port override
2025-06-30 17:23:39 +01:00
if [ [ $ID = = "ubuntu" ] ] && dpkg --compare-versions " $( lsb_release -rs) " ge "24.04" ; then
2025-06-30 21:10:35 +01:00
print_info "Updating SSH port in /etc/ssh/sshd_config for Ubuntu 24.04+..."
if ! grep -q "^Port" /etc/ssh/sshd_config; then echo " Port $SSH_PORT " >> /etc/ssh/sshd_config; else sed -i " s/^Port .*/Port $SSH_PORT / " /etc/ssh/sshd_config; fi
elif [ [ " $SSH_SERVICE " = = "ssh.socket" ] ] ; then
print_info " Configuring SSH socket to listen on port $SSH_PORT ... "
mkdir -p /etc/systemd/system/ssh.socket.d
2025-10-16 12:29:34 +01:00
printf '%s\n' " [Socket]\nListenStream=\nListenStream= $SSH_PORT " > /etc/systemd/system/ssh.socket.d/override.conf
2025-06-30 17:23:39 +01:00
else
2025-06-30 21:10:35 +01:00
print_info " Configuring SSH service to listen on port $SSH_PORT ... "
mkdir -p /etc/systemd/system/${ SSH_SERVICE } .d
2025-10-16 12:29:34 +01:00
printf '%s\n' " [Service]\nExecStart=\nExecStart=/usr/sbin/sshd -D -p $SSH_PORT " > /etc/systemd/system/${ SSH_SERVICE } .d/override.conf
2025-06-27 19:23:28 +01:00
fi
2025-06-28 11:43:10 +01:00
2025-06-30 21:10:35 +01:00
# Apply additional hardening
mkdir -p /etc/ssh/sshd_config.d
tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<EOF
2025-06-26 16:22:23 +01:00
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
X11Forwarding no
PrintMotd no
Banner /etc/issue.net
EOF
2025-06-30 21:10:35 +01:00
tee /etc/issue.net > /dev/null <<'EOF'
2025-06-26 16:22:23 +01:00
******************************************************************************
2025-06-27 15:50:40 +01:00
🔒AUTHORIZED ACCESS ONLY
2025-06-30 19:22:31 +01:00
════ all attempts are logged and reviewed ════
2025-06-26 16:22:23 +01:00
******************************************************************************
EOF
2025-10-13 11:20:46 +01:00
print_info "Testing SSH configuration syntax..."
2025-10-13 14:31:47 +01:00
if ! sshd -t 2>& 1 | tee -a " $LOG_FILE " ; then
2025-10-13 11:20:46 +01:00
print_warning "SSH configuration test detected potential issues (see above)."
print_info "This may be due to existing configuration files on the system."
if ! confirm "Continue despite configuration warnings?" ; then
print_error "Aborting SSH configuration."
2025-10-13 16:39:04 +01:00
rm -f /etc/ssh/sshd_config.d/99-hardening.conf
rm -f /etc/issue.net
rm -f /etc/systemd/system/ssh.socket.d/override.conf
rm -f /etc/systemd/system/ssh.service.d/override.conf
rm -f /etc/systemd/system/sshd.service.d/override.conf
2025-10-13 14:45:15 +01:00
systemctl daemon-reload
2025-10-13 11:20:46 +01:00
return 1
fi
fi
2025-06-27 00:54:10 +01:00
print_info "Reloading systemd and restarting SSH service..."
systemctl daemon-reload
2025-06-30 21:10:35 +01:00
systemctl restart " $SSH_SERVICE "
2025-06-27 00:54:10 +01:00
sleep 5
if ! ss -tuln | grep -q " : $SSH_PORT " ; then
2025-06-30 21:10:35 +01:00
print_error " SSH not listening on port $SSH_PORT after restart! "
return 1
2025-06-26 16:22:23 +01:00
fi
2025-06-27 00:54:10 +01:00
print_success " SSH service restarted on port $SSH_PORT . "
2025-06-26 16:22:23 +01:00
2025-06-26 21:49:51 +01:00
# Verify root SSH is disabled
2025-06-26 16:22:23 +01:00
print_info "Verifying root SSH login is disabled..."
2025-10-13 11:20:46 +01:00
sleep 2
2025-06-30 21:10:35 +01:00
if ssh -p " $SSH_PORT " -o BatchMode = yes -o StrictHostKeyChecking = no -o ConnectTimeout = 5 root@localhost true 2>/dev/null; then
print_error "Root SSH login is still possible! Check configuration."
return 1
2025-06-26 16:22:23 +01:00
else
print_success "Confirmed: Root SSH login is disabled."
fi
print_warning "CRITICAL: Test new SSH connection in a SEPARATE terminal NOW!"
2025-10-15 23:30:30 +01:00
print_warning " ACTION REQUIRED: Check your VPS provider's edge/network firewall to allow $SSH_PORT /tcp. "
2025-10-13 16:39:04 +01:00
if [ [ -n " $SERVER_IP_V4 " && " $SERVER_IP_V4 " != "unknown" ] ] ; then
2025-08-26 16:14:58 +01:00
print_info " Use IPv4: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP_V4 "
fi
2025-10-13 16:39:04 +01:00
if [ [ -n " $SERVER_IP_V6 " && " $SERVER_IP_V6 " != "not available" ] ] ; then
2025-08-26 21:05:00 +01:00
print_info " Use IPv6: ssh -p $SSH_PORT $USERNAME @ $SERVER_IP_V6 "
2025-08-26 11:40:41 +01:00
fi
2025-06-26 21:49:51 +01:00
2025-06-27 00:36:26 +01:00
# Retry loop for SSH connection test
local retry_count = 0
local max_retries = 3
while ( ( retry_count < max_retries ) ) ; do
if confirm "Was the new SSH connection successful?" ; then
2025-06-30 21:10:35 +01:00
print_success "SSH hardening confirmed and finalized."
2025-06-27 00:36:26 +01:00
break
else
( ( retry_count++ ) )
if ( ( retry_count < max_retries ) ) ; then
print_info " Retrying SSH connection test ( $retry_count / $max_retries )... "
sleep 5
else
2025-06-30 21:10:35 +01:00
print_error " All retries failed. Initiating rollback to port $PREVIOUS_SSH_PORT ... "
2025-06-30 17:23:39 +01:00
rollback_ssh_changes
2025-06-30 21:10:35 +01:00
if ! ss -tuln | grep -q " : $PREVIOUS_SSH_PORT " ; then
print_error " Rollback failed. SSH not restored on original port $PREVIOUS_SSH_PORT . "
2025-06-30 20:06:10 +01:00
else
2025-06-30 21:10:35 +01:00
print_success " Rollback successful. SSH restored on original port $PREVIOUS_SSH_PORT . "
2025-06-30 19:22:31 +01:00
fi
2025-06-30 21:10:35 +01:00
return 1
2025-06-27 00:36:26 +01:00
fi
fi
done
2025-06-30 21:10:35 +01:00
trap - ERR
2025-06-26 16:22:23 +01:00
log "SSH hardening completed."
}
2025-06-30 17:23:39 +01:00
rollback_ssh_changes( ) {
2025-06-30 21:10:35 +01:00
print_info " Rolling back SSH configuration changes to port $PREVIOUS_SSH_PORT ... "
2025-07-02 13:48:34 +01:00
# Ensure SSH_SERVICE is set and valid
local SSH_SERVICE = ${ SSH_SERVICE :- "sshd.service" }
local USE_SOCKET = false
# Check if socket activation is used
if systemctl list-units --full -all --no-pager | grep -E "[[:space:]]ssh.socket[[:space:]]" >/dev/null 2>& 1; then
USE_SOCKET = true
SSH_SERVICE = "ssh.socket"
print_info "Detected SSH socket activation: using ssh.socket."
log "Rollback: Using ssh.socket for SSH service."
2025-07-15 19:16:06 +01:00
elif ! systemctl list-units --full -all --no-pager | grep -E " [[:space:]] ${ SSH_SERVICE } [[:space:]] " >/dev/null 2>& 1; then
2025-10-15 22:50:25 +01:00
local initial_service_check = " $SSH_SERVICE "
2025-07-02 13:48:34 +01:00
SSH_SERVICE = "ssh.service" # Fallback for Ubuntu
2025-10-15 22:50:25 +01:00
print_warning " SSH service ' $initial_service_check ' not found, falling back to ' $SSH_SERVICE '. "
2025-07-02 13:48:34 +01:00
log "Rollback warning: Using fallback SSH service ssh.service."
# Verify fallback service exists
if ! systemctl list-units --full -all --no-pager | grep -E "[[:space:]]ssh.service[[:space:]]" >/dev/null 2>& 1; then
print_error "No valid SSH service (sshd.service or ssh.service) found."
log "Rollback failed: No valid SSH service detected."
print_info "Action: Verify SSH service with 'systemctl list-units --full -all | grep ssh' and manually configure /etc/ssh/sshd_config."
return 0
fi
fi
# Remove systemd overrides for both service and socket
2025-10-15 22:32:46 +01:00
if ! rm -rf /etc/systemd/system/ssh.service.d /etc/systemd/system/sshd.service.d /etc/systemd/system/ssh.socket.d 2>/dev/null; then
print_warning "Could not remove one or more systemd override directories."
2025-07-02 13:48:34 +01:00
log "Rollback warning: Failed to remove systemd overrides."
else
2025-10-15 22:32:46 +01:00
log "Removed all potential systemd override directories for SSH."
2025-07-02 13:48:34 +01:00
fi
# Remove custom SSH configuration
if ! rm -f /etc/ssh/sshd_config.d/99-hardening.conf 2>/dev/null; then
print_warning "Failed to remove /etc/ssh/sshd_config.d/99-hardening.conf."
log "Rollback warning: Failed to remove /etc/ssh/sshd_config.d/99-hardening.conf."
else
log "Removed /etc/ssh/sshd_config.d/99-hardening.conf"
fi
# Restore original sshd_config
2025-06-30 19:36:22 +01:00
if [ [ -f " $SSHD_BACKUP_FILE " ] ] ; then
2025-07-02 13:48:34 +01:00
if ! cp " $SSHD_BACKUP_FILE " /etc/ssh/sshd_config 2>/dev/null; then
print_error " Failed to restore sshd_config from $SSHD_BACKUP_FILE . "
log " Rollback failed: Cannot copy $SSHD_BACKUP_FILE to /etc/ssh/sshd_config. "
print_info " Action: Manually restore with 'cp $SSHD_BACKUP_FILE /etc/ssh/sshd_config' and verify with 'sshd -t'. "
return 0
fi
2025-06-30 21:10:35 +01:00
print_info " Restored original sshd_config from $SSHD_BACKUP_FILE . "
2025-07-02 13:48:34 +01:00
log " Restored sshd_config from $SSHD_BACKUP_FILE . "
2025-06-30 19:36:22 +01:00
else
2025-07-02 13:48:34 +01:00
print_error " Backup file not found at $SSHD_BACKUP_FILE . "
log " Rollback failed: $SSHD_BACKUP_FILE not found. "
print_info " Action: Manually configure /etc/ssh/sshd_config to use port $PREVIOUS_SSH_PORT and verify with 'sshd -t'. "
return 0
2025-06-30 19:36:22 +01:00
fi
2025-06-30 21:10:35 +01:00
2025-07-02 13:48:34 +01:00
# Validate restored sshd_config
if ! /usr/sbin/sshd -t >/tmp/sshd_config_test.log 2>& 1; then
print_error "Restored sshd_config is invalid. Check /tmp/sshd_config_test.log for details."
log "Rollback failed: Invalid sshd_config after restoration. See /tmp/sshd_config_test.log."
print_info "Action: Fix /etc/ssh/sshd_config manually and test with 'sshd -t', then restart with 'systemctl restart ssh.service'."
return 0
2025-06-30 17:23:39 +01:00
fi
2025-06-30 21:10:35 +01:00
2025-07-02 13:48:34 +01:00
# Reload systemd
print_info "Reloading systemd..."
if ! systemctl daemon-reload 2>/dev/null; then
print_warning "Failed to reload systemd. Continuing with restart attempt..."
log "Rollback warning: Failed to reload systemd."
fi
# Handle socket activation or direct service restart
if [ [ " $USE_SOCKET " = = true ] ] ; then
# Stop ssh.socket to avoid conflicts
if systemctl is-active --quiet ssh.socket; then
if ! systemctl stop ssh.socket 2>/tmp/ssh_socket_stop.log; then
print_warning "Failed to stop ssh.socket. May affect port binding."
log "Rollback warning: Failed to stop ssh.socket. See /tmp/ssh_socket_stop.log."
else
log "Stopped ssh.socket to ensure correct port binding."
fi
fi
# Restart ssh.service to ensure sshd starts
print_info "Restarting ssh.service..."
if ! systemctl restart ssh.service 2>/tmp/sshd_restart.log; then
print_warning "Failed to restart ssh.service. Attempting manual start..."
log "Rollback warning: Failed to restart ssh.service. See /tmp/sshd_restart.log."
# Ensure no other sshd processes are running
pkill -f "sshd:.*" 2>/dev/null || true
# Manual start in foreground to verify
timeout 5 /usr/sbin/sshd -D -f /etc/ssh/sshd_config >/tmp/sshd_manual_start.log 2>& 1
local TIMEOUT_EXIT = $?
if [ [ $TIMEOUT_EXIT -eq 0 || $TIMEOUT_EXIT -eq 124 ] ] ; then
log " Manual SSH start succeeded (exit code $TIMEOUT_EXIT ). "
# Restart ssh.service to ensure systemd management
if ! systemctl restart ssh.service 2>/tmp/sshd_restart_manual.log; then
print_error "Failed to restart ssh.service after manual start."
log "Rollback failed: Failed to restart ssh.service after manual start. See /tmp/sshd_restart_manual.log."
print_info "Action: Check service status with 'systemctl status ssh.service' and logs with 'journalctl -u ssh.service'."
return 0
fi
else
print_error " Manual SSH start failed (exit code $TIMEOUT_EXIT ). Check /tmp/sshd_manual_start.log. "
log " Rollback failed: Manual SSH start failed (exit code $TIMEOUT_EXIT ). See /tmp/sshd_manual_start.log. "
print_info "Action: Check service status with 'systemctl status ssh.service' and logs with 'journalctl -u ssh.service'."
return 0
fi
fi
# Restart ssh.socket to re-enable socket activation
print_info "Restarting ssh.socket..."
if ! systemctl restart ssh.socket 2>/tmp/ssh_socket_restart.log; then
print_warning "Failed to restart ssh.socket. SSH service may still be running."
log "Rollback warning: Failed to restart ssh.socket. See /tmp/ssh_socket_restart.log."
else
log "Restarted ssh.socket for socket activation."
fi
else
# Direct service restart for non-socket systems
print_info " Restarting $SSH_SERVICE ... "
if ! systemctl restart " $SSH_SERVICE " 2>/tmp/sshd_restart.log; then
print_warning " Failed to restart $SSH_SERVICE . Attempting manual start... "
log " Rollback warning: Failed to restart $SSH_SERVICE . See /tmp/sshd_restart.log. "
# Ensure no other sshd processes are running
pkill -f "sshd:.*" 2>/dev/null || true
# Manual start in foreground to verify
timeout 5 /usr/sbin/sshd -D -f /etc/ssh/sshd_config >/tmp/sshd_manual_start.log 2>& 1
local TIMEOUT_EXIT = $?
if [ [ $TIMEOUT_EXIT -eq 0 || $TIMEOUT_EXIT -eq 124 ] ] ; then
log " Manual SSH start succeeded (exit code $TIMEOUT_EXIT ). "
# Restart service to ensure systemd management
if ! systemctl restart " $SSH_SERVICE " 2>/tmp/sshd_restart_manual.log; then
print_error " Failed to restart $SSH_SERVICE after manual start. "
log " Rollback failed: Failed to restart $SSH_SERVICE after manual start. See /tmp/sshd_restart_manual.log. "
print_info " Action: Check service status with 'systemctl status $SSH_SERVICE ' and logs with 'journalctl -u $SSH_SERVICE '. "
return 0
fi
else
print_error " Manual SSH start failed (exit code $TIMEOUT_EXIT ). Check /tmp/sshd_manual_start.log. "
log " Rollback failed: Manual SSH start failed (exit code $TIMEOUT_EXIT ). See /tmp/sshd_manual_start.log. "
print_info " Action: Check service status with 'systemctl status $SSH_SERVICE ' and logs with 'journalctl -u $SSH_SERVICE '. "
return 0
fi
fi
fi
# Verify rollback with retries
2025-06-30 21:10:35 +01:00
local rollback_verified = false
print_info " Verifying SSH rollback to port $PREVIOUS_SSH_PORT ... "
for ( ( i = 1; i<= 10; i++) ) ; do
2025-07-02 13:48:34 +01:00
if ss -tuln | grep -q " : $PREVIOUS_SSH_PORT " ; then
2025-06-30 21:10:35 +01:00
rollback_verified = true
break
fi
2025-07-02 13:48:34 +01:00
log " Rollback verification attempt $i /10: SSH not listening on port $PREVIOUS_SSH_PORT . "
sleep 3
2025-06-30 21:10:35 +01:00
done
2025-07-02 13:48:34 +01:00
if [ [ $rollback_verified = = true ] ] ; then
print_success " Rollback successful. SSH is now listening on port $PREVIOUS_SSH_PORT . "
log " Rollback successful: SSH listening on port $PREVIOUS_SSH_PORT . "
else
2025-06-30 21:10:35 +01:00
print_error " Rollback failed. SSH service is not listening on port $PREVIOUS_SSH_PORT . "
2025-07-02 13:48:34 +01:00
log " Rollback failed: SSH not listening on port $PREVIOUS_SSH_PORT . See /tmp/sshd_config_test.log, /tmp/sshd_restart.log, /tmp/sshd_manual_start.log, /tmp/ssh_socket_restart.log. "
print_info "Action: Check service status with 'systemctl status ssh.service' or 'systemctl status ssh.socket', logs with 'journalctl -u ssh.service' or 'journalctl -u ssh.socket', and test config with 'sshd -t'."
print_info " Manually verify port with 'ss -tuln | grep : $PREVIOUS_SSH_PORT '. "
print_info "Try starting SSH with 'sudo systemctl start ssh.service'."
2025-06-30 17:23:39 +01:00
fi
2025-07-02 13:48:34 +01:00
return 0
2025-06-30 17:23:39 +01:00
}
2025-06-26 16:22:23 +01:00
configure_firewall( ) {
print_section "Firewall Configuration (UFW)"
if ufw status | grep -q "Status: active" ; then
print_info "UFW already enabled."
else
print_info "Configuring UFW default policies..."
ufw default deny incoming
ufw default allow outgoing
fi
if ! ufw status | grep -qw " $SSH_PORT /tcp " ; then
print_info " Adding SSH rule for port $SSH_PORT ... "
ufw allow " $SSH_PORT " /tcp comment 'Custom SSH'
else
print_info " SSH rule for port $SSH_PORT already exists. "
fi
2025-06-26 21:49:51 +01:00
if confirm "Allow HTTP traffic (port 80)?" ; then
if ! ufw status | grep -qw "80/tcp" ; then
ufw allow http comment 'HTTP'
print_success "HTTP traffic allowed."
else
print_info "HTTP rule already exists."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
fi
if confirm "Allow HTTPS traffic (port 443)?" ; then
if ! ufw status | grep -qw "443/tcp" ; then
ufw allow https comment 'HTTPS'
print_success "HTTPS traffic allowed."
else
print_info "HTTPS rule already exists."
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
fi
2025-06-29 13:40:03 +01:00
if confirm "Allow Tailscale traffic (UDP 41641)?" ; then
if ! ufw status | grep -qw "41641/udp" ; then
ufw allow 41641/udp comment 'Tailscale VPN'
print_success "Tailscale traffic (UDP 41641) allowed."
log "Added UFW rule for Tailscale (41641/udp)."
else
print_info "Tailscale rule (UDP 41641) already exists."
fi
fi
2025-06-26 21:49:51 +01:00
if confirm "Add additional custom ports (e.g., 8080/tcp, 123/udp)?" ; then
while true; do
2025-06-26 22:19:35 +01:00
local CUSTOM_PORTS # Make variable local to the loop
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter ports (space-separated, e.g., 8080/tcp 123/udp): ${ NC } " ) " CUSTOM_PORTS
2025-06-26 21:49:51 +01:00
if [ [ -z " $CUSTOM_PORTS " ] ] ; then
print_info "No custom ports entered. Skipping."
break
fi
2025-06-26 22:19:35 +01:00
local valid = true
2025-06-26 21:49:51 +01:00
for port in $CUSTOM_PORTS ; do
if ! validate_ufw_port " $port " ; then
print_error " Invalid port format: $port . Use <port>[/tcp|/udp]. "
valid = false
2025-06-26 16:22:23 +01:00
break
fi
2025-06-26 21:49:51 +01:00
done
if [ [ " $valid " = = true ] ] ; then
for port in $CUSTOM_PORTS ; do
if ufw status | grep -qw " $port " ; then
print_info " Rule for $port already exists. "
else
2025-06-29 13:40:03 +01:00
local CUSTOM_COMMENT
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter comment for $port (e.g., 'My App Port'): ${ NC } " ) " CUSTOM_COMMENT
2025-06-29 13:40:03 +01:00
if [ [ -z " $CUSTOM_COMMENT " ] ] ; then
CUSTOM_COMMENT = " Custom port $port "
fi
# Sanitize comment to avoid breaking UFW command
CUSTOM_COMMENT = $( echo " $CUSTOM_COMMENT " | tr -d "'\"\\" )
ufw allow " $port " comment " $CUSTOM_COMMENT "
print_success " Added rule for $port with comment ' $CUSTOM_COMMENT '. "
log " Added UFW rule for $port with comment ' $CUSTOM_COMMENT '. "
2025-06-26 16:22:23 +01:00
fi
done
2025-06-26 21:49:51 +01:00
break
else
print_info "Please try again."
fi
done
2025-06-26 16:22:23 +01:00
fi
2025-09-07 17:27:29 +01:00
# --- Enable IPv6 Support if Available ---
if [ [ -f /proc/net/if_inet6 ] ] ; then
print_info "IPv6 detected. Ensuring UFW is configured for IPv6..."
if grep -q '^IPV6=yes' /etc/default/ufw; then
print_info "UFW IPv6 support is already enabled."
else
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
if ! grep -q '^IPV6=yes' /etc/default/ufw; then
echo "IPV6=yes" >> /etc/default/ufw
fi
print_success "Enabled IPv6 support in /etc/default/ufw."
log "Enabled UFW IPv6 support."
fi
else
print_info "No IPv6 detected on this system. Skipping UFW IPv6 configuration."
log "UFW IPv6 configuration skipped as no kernel support was detected."
fi
2025-10-13 11:20:46 +01:00
2025-06-26 16:22:23 +01:00
print_info "Enabling firewall..."
if ! ufw --force enable; then
print_error "Failed to enable UFW. Check 'journalctl -u ufw' for details."
exit 1
fi
if ufw status | grep -q "Status: active" ; then
print_success "Firewall is active."
else
print_error "UFW failed to activate. Check 'journalctl -u ufw' for details."
exit 1
fi
2025-06-29 13:40:03 +01:00
print_warning " ACTION REQUIRED: Check your VPS provider's edge firewall to allow opened ports (e.g., $SSH_PORT /tcp, 41641/udp for Tailscale). "
2025-06-26 16:22:23 +01:00
ufw status verbose | tee -a " $LOG_FILE "
log "Firewall configuration completed."
}
2025-06-28 12:51:55 +01:00
configure_fail2ban( ) {
print_section "Fail2Ban Configuration"
2025-07-07 21:22:47 +01:00
# --- Define Desired Configurations ---
# Define content of config file.
local UFW_PROBES_CONFIG
UFW_PROBES_CONFIG = $( cat <<'EOF'
2025-07-07 21:08:21 +01:00
[ Definition]
# This regex looks for the standard "[UFW BLOCK]" message in /var/log/ufw.log
failregex = \[ UFW BLOCK\] IN = .* OUT = .* SRC = <HOST>
ignoreregex =
EOF
2025-07-07 21:22:47 +01:00
)
2025-06-28 12:51:55 +01:00
2025-07-07 21:22:47 +01:00
local JAIL_LOCAL_CONFIG
JAIL_LOCAL_CONFIG = $( cat <<EOF
2025-06-28 12:51:55 +01:00
[ DEFAULT]
2025-07-07 21:08:21 +01:00
ignoreip = 127.0.0.1/8 ::1
bantime = 1d
2025-06-28 12:51:55 +01:00
findtime = 10m
2025-07-07 21:08:21 +01:00
maxretry = 5
banaction = ufw
2025-06-28 12:51:55 +01:00
[ sshd]
enabled = true
2025-07-07 21:08:21 +01:00
port = $SSH_PORT
# This jail monitors UFW logs for rejected packets (port scans, etc.).
[ ufw-probes]
enabled = true
port = all
filter = ufw-probes
logpath = /var/log/ufw.log
maxretry = 3
2025-06-28 12:51:55 +01:00
EOF
2025-07-07 21:22:47 +01:00
)
local UFW_FILTER_PATH = "/etc/fail2ban/filter.d/ufw-probes.conf"
local JAIL_LOCAL_PATH = "/etc/fail2ban/jail.local"
# --- Idempotency Check ---
# This checks if the on-disk files are already identical to our desired configuration.
if [ [ -f " $UFW_FILTER_PATH " && -f " $JAIL_LOCAL_PATH " ] ] && \
cmp -s " $UFW_FILTER_PATH " <<< " $UFW_PROBES_CONFIG " && \
cmp -s " $JAIL_LOCAL_PATH " <<< " $JAIL_LOCAL_CONFIG " ; then
print_info "Fail2Ban is already configured correctly. Skipping."
log "Fail2Ban configuration is already correct."
return 0
fi
# --- Apply Configuration ---
# If the check above fails, we write the correct configuration files.
print_info "Applying new Fail2Ban configuration..."
mkdir -p /etc/fail2ban/filter.d
echo " $UFW_PROBES_CONFIG " > " $UFW_FILTER_PATH "
echo " $JAIL_LOCAL_CONFIG " > " $JAIL_LOCAL_PATH "
2025-07-07 21:08:21 +01:00
2025-08-06 14:52:12 +01:00
# --- Ensure the log file exists BEFORE restarting the service ---
if [ [ ! -f /var/log/ufw.log ] ] ; then
touch /var/log/ufw.log
print_info "Created empty /var/log/ufw.log to ensure Fail2Ban starts correctly."
fi
2025-07-07 21:08:21 +01:00
# --- Restart and Verify Fail2ban ---
print_info "Enabling and restarting Fail2Ban to apply new rules..."
2025-06-28 12:51:55 +01:00
systemctl enable fail2ban
systemctl restart fail2ban
2025-08-06 14:54:50 +01:00
sleep 2 # Give the service a moment to initialize.
2025-07-07 21:08:21 +01:00
2025-06-28 12:51:55 +01:00
if systemctl is-active --quiet fail2ban; then
2025-07-07 21:08:21 +01:00
print_success "Fail2Ban is active with the new configuration."
# Show the status of the enabled jails for confirmation.
fail2ban-client status | tee -a " $LOG_FILE "
2025-06-28 12:51:55 +01:00
else
2025-07-07 21:08:21 +01:00
print_error "Fail2Ban service failed to start. Check 'journalctl -u fail2ban' for errors."
FAILED_SERVICES += ( "fail2ban" )
2025-06-28 12:51:55 +01:00
fi
log "Fail2Ban configuration completed."
}
configure_auto_updates( ) {
print_section "Automatic Security Updates"
if confirm "Enable automatic security updates via unattended-upgrades?" ; then
if ! dpkg -l unattended-upgrades | grep -q ^ii; then
print_error "unattended-upgrades package is not installed."
exit 1
fi
2025-06-29 19:09:50 +01:00
# Check for existing unattended-upgrades configuration
if [ [ -f /etc/apt/apt.conf.d/50unattended-upgrades ] ] && grep -q "Unattended-Upgrade::Allowed-Origins" /etc/apt/apt.conf.d/50unattended-upgrades; then
print_info "Existing unattended-upgrades configuration found. Verify with 'cat /etc/apt/apt.conf.d/50unattended-upgrades'."
fi
2025-06-28 12:51:55 +01:00
print_info "Configuring unattended upgrades..."
echo "unattended-upgrades unattended-upgrades/enable_auto_updates boolean true" | debconf-set-selections
DEBIAN_FRONTEND = noninteractive dpkg-reconfigure -f noninteractive unattended-upgrades
print_success "Automatic security updates enabled."
else
print_info "Skipping automatic security updates."
fi
log "Automatic updates configuration completed."
}
2025-07-15 13:51:34 +01:00
configure_kernel_hardening( ) {
print_section "Kernel Parameter Hardening (sysctl)"
if ! confirm "Apply recommended kernel security settings (sysctl)?" ; then
print_info "Skipping kernel hardening."
log "Kernel hardening skipped by user."
return 0
fi
local KERNEL_HARDENING_CONFIG
KERNEL_HARDENING_CONFIG = $( mktemp)
# create the config in a temporary file
tee " $KERNEL_HARDENING_CONFIG " > /dev/null <<'EOF'
# Recommended Security Settings managed by du_setup.sh
# For details, see: https://www.kernel.org/doc/Documentation/sysctl/
# --- IPV4 Networking ---
# Protect against IP spoofing
net.ipv4.conf.default.rp_filter= 1
net.ipv4.conf.all.rp_filter= 1
# Block SYN-FLOOD attacks
net.ipv4.tcp_syncookies= 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects= 0
net.ipv4.conf.default.accept_redirects= 0
net.ipv4.conf.all.secure_redirects= 1
net.ipv4.conf.default.secure_redirects= 1
# Ignore source-routed packets
net.ipv4.conf.all.accept_source_route= 0
net.ipv4.conf.default.accept_source_route= 0
# Log martian packets (packets with impossible source addresses)
net.ipv4.conf.all.log_martians= 1
net.ipv4.conf.default.log_martians= 1
# --- IPV6 Networking (if enabled) ---
net.ipv6.conf.all.accept_redirects= 0
net.ipv6.conf.default.accept_redirects= 0
net.ipv6.conf.all.accept_source_route= 0
net.ipv6.conf.default.accept_source_route= 0
# --- Kernel Security ---
# Enable ASLR (Address Space Layout Randomization) for better security
kernel.randomize_va_space= 2
# Restrict access to kernel pointers in /proc to prevent leaks
kernel.kptr_restrict= 2
# Restrict access to dmesg for unprivileged users
kernel.dmesg_restrict= 1
# Restrict ptrace scope to prevent process injection attacks
kernel.yama.ptrace_scope= 1
# --- Filesystem Security ---
# Protect against TOCTOU (Time-of-Check to Time-of-Use) race conditions
fs.protected_hardlinks= 1
fs.protected_symlinks= 1
EOF
local SYSCTL_CONF_FILE = "/etc/sysctl.d/99-du-hardening.conf"
# Idempotency check: only update if the file doesn't exist or has changed
if [ [ -f " $SYSCTL_CONF_FILE " ] ] && cmp -s " $KERNEL_HARDENING_CONFIG " " $SYSCTL_CONF_FILE " ; then
print_info "Kernel security settings are already configured correctly."
rm -f " $KERNEL_HARDENING_CONFIG "
log "Kernel hardening settings already in place."
return 0
fi
print_info " Applying settings to $SYSCTL_CONF_FILE ... "
# Move the new config into place
mv " $KERNEL_HARDENING_CONFIG " " $SYSCTL_CONF_FILE "
chmod 644 " $SYSCTL_CONF_FILE "
print_info "Loading new settings..."
if sysctl -p " $SYSCTL_CONF_FILE " >/dev/null 2>& 1; then
print_success "Kernel security settings applied successfully."
log "Applied kernel hardening settings."
else
print_error "Failed to apply kernel settings. Check for kernel compatibility."
log "sysctl -p failed for kernel hardening config."
fi
}
2025-06-28 12:51:55 +01:00
install_docker( ) {
if ! confirm "Install Docker Engine (Optional)?" ; then
print_info "Skipping Docker installation."
return 0
fi
print_section "Docker Installation"
if command -v docker >/dev/null 2>& 1; then
print_info "Docker already installed."
return 0
fi
print_info "Removing old container runtimes..."
apt-get remove -y -qq docker docker-engine docker.io containerd runc 2>/dev/null || true
print_info "Adding Docker's official GPG key and repository..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/${ ID } /gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo " deb [arch= $( dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ ${ ID } $( . /etc/os-release && echo " $VERSION_CODENAME " ) stable " > /etc/apt/sources.list.d/docker.list
print_info "Installing Docker packages..."
if ! apt-get update -qq || ! apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
print_error "Failed to install Docker packages."
exit 1
fi
print_info " Adding ' $USERNAME ' to docker group... "
getent group docker >/dev/null || groupadd docker
if ! groups " $USERNAME " | grep -qw docker; then
usermod -aG docker " $USERNAME "
print_success " User ' $USERNAME ' added to docker group. "
else
print_info " User ' $USERNAME ' is already in docker group. "
fi
print_info "Configuring Docker daemon..."
local NEW_DOCKER_CONFIG
NEW_DOCKER_CONFIG = $( mktemp)
tee " $NEW_DOCKER_CONFIG " > /dev/null <<EOF
{
"log-driver" : "json-file" ,
"log-opts" : { "max-size" : "10m" , "max-file" : "3" } ,
"live-restore" : true
}
EOF
mkdir -p /etc/docker
if [ [ -f /etc/docker/daemon.json ] ] && cmp -s " $NEW_DOCKER_CONFIG " /etc/docker/daemon.json; then
print_info "Docker daemon configuration already correct. Skipping."
rm -f " $NEW_DOCKER_CONFIG "
else
mv " $NEW_DOCKER_CONFIG " /etc/docker/daemon.json
chmod 644 /etc/docker/daemon.json
fi
systemctl daemon-reload
systemctl enable --now docker
print_info "Running Docker sanity check..."
if sudo -u " $USERNAME " docker run --rm hello-world 2>& 1 | tee -a " $LOG_FILE " | grep -q "Hello from Docker" ; then
print_success "Docker sanity check passed."
else
print_error "Docker hello-world test failed. Please verify installation."
exit 1
fi
print_warning " NOTE: ' $USERNAME ' must log out and back in to use Docker without sudo. "
log "Docker installation completed."
}
install_tailscale( ) {
2025-06-28 22:13:17 +01:00
if ! confirm "Install and configure Tailscale VPN (Optional)?" ; then
2025-06-28 12:51:55 +01:00
print_info "Skipping Tailscale installation."
2025-06-28 22:13:17 +01:00
log "Tailscale installation skipped by user."
2025-06-28 12:51:55 +01:00
return 0
fi
2025-06-28 22:13:17 +01:00
print_section "Tailscale VPN Installation and Configuration"
2025-07-07 20:49:14 +01:00
# Check if Tailscale is already installed and active
2025-06-28 12:51:55 +01:00
if command -v tailscale >/dev/null 2>& 1; then
2025-06-29 20:08:19 +01:00
if systemctl is-active --quiet tailscaled && tailscale ip >/dev/null 2>& 1; then
local TS_IPS TS_IPV4
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
2025-07-04 15:42:46 +01:00
print_success " Service tailscaled is active and connected. Node IPv4 in tailnet: $TS_IPV4 "
2025-06-29 20:08:19 +01:00
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 22:13:17 +01:00
else
2025-07-04 15:42:46 +01:00
print_warning "Service tailscaled is installed but not active or connected."
FAILED_SERVICES += ( "tailscaled" )
TS_COMMAND = $( grep "Tailscale connection failed: tailscale up" " $LOG_FILE " | tail -1 | sed 's/.*Tailscale connection failed: //' )
2025-07-07 20:49:14 +01:00
TS_COMMAND = ${ TS_COMMAND :- "" }
2025-06-28 22:13:17 +01:00
fi
else
print_info "Installing Tailscale..."
2025-07-07 20:49:14 +01:00
# Gracefully handle download failures
if ! curl -fsSL https://tailscale.com/install.sh -o /tmp/tailscale_install.sh; then
print_error "Failed to download the Tailscale installation script."
print_info "After setup completes, please try installing it manually: curl -fsSL https://tailscale.com/install.sh | sh"
rm -f /tmp/tailscale_install.sh # Clean up partial download
return 0 # Exit the function without exiting the main script
2025-06-28 22:13:17 +01:00
fi
2025-07-07 20:49:14 +01:00
# Execute the downloaded script with 'sh'
if ! sh /tmp/tailscale_install.sh; then
print_error "Tailscale installation script failed to execute."
2025-06-28 22:13:17 +01:00
log "Tailscale installation failed."
2025-07-07 20:49:14 +01:00
rm -f /tmp/tailscale_install.sh # Clean up
return 0 # Exit the function gracefully
2025-06-28 22:13:17 +01:00
fi
2025-07-07 20:49:14 +01:00
rm -f /tmp/tailscale_install.sh # Clean up successful install
2025-06-28 22:13:17 +01:00
print_success "Tailscale installation complete."
log "Tailscale installation completed."
fi
2025-06-29 20:08:19 +01:00
if systemctl is-active --quiet tailscaled && tailscale ip >/dev/null 2>& 1; then
local TS_IPS TS_IPV4
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
print_info " Tailscale is already connected. Node IPv4 in tailnet: $TS_IPV4 "
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 12:51:55 +01:00
return 0
fi
2025-07-04 14:16:27 +01:00
if ! confirm "Configure Tailscale now?" ; then
print_info "You can configure Tailscale later by running: sudo tailscale up"
print_info "If you are using a custom Tailscale server, use: sudo tailscale up --login-server=<your_server_url>"
return 0
fi
2025-06-28 22:13:17 +01:00
print_info "Configuring Tailscale connection..."
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } Choose Tailscale connection method: ${ NC } "
printf ' 1) Standard Tailscale (requires pre-auth key from https://login.tailscale.com/admin)\n'
printf ' 2) Custom Tailscale server (requires server URL and pre-auth key)\n'
read -rp " $( printf '%s' " ${ CYAN } Enter choice (1-2) [1]: ${ NC } " ) " TS_CONNECTION
2025-06-28 22:13:17 +01:00
TS_CONNECTION = ${ TS_CONNECTION :- 1 }
local AUTH_KEY LOGIN_SERVER = ""
if [ [ " $TS_CONNECTION " = = "2" ] ] ; then
while true; do
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter Tailscale server URL (e.g., https://ts.mydomain.cloud): ${ NC } " ) " LOGIN_SERVER
2025-06-28 22:13:17 +01:00
if [ [ " $LOGIN_SERVER " = ~ ^https://[ a-zA-Z0-9.-] +( :[ 0-9] +) ?$ ] ] ; then break; else print_error "Invalid URL. Must start with https://. Try again." ; fi
done
2025-06-28 12:51:55 +01:00
fi
2025-06-28 22:13:17 +01:00
while true; do
2025-10-16 11:29:53 +01:00
read -rsp " $( printf '%s' " ${ CYAN } Enter Tailscale pre-auth key: ${ NC } " ) " AUTH_KEY
printf '\n'
2025-06-28 22:13:17 +01:00
if [ [ " $TS_CONNECTION " = = "1" && " $AUTH_KEY " = ~ ^tskey-auth- ] ] ; then break
elif [ [ " $TS_CONNECTION " = = "2" && -n " $AUTH_KEY " ] ] ; then
print_warning " Ensure the pre-auth key is valid for your custom Tailscale server ( $LOGIN_SERVER ). "
break
else
print_error "Invalid key format. For standard connection, key must start with 'tskey-auth-'. For custom server, key cannot be empty."
fi
done
local TS_COMMAND = "tailscale up"
if [ [ " $TS_CONNECTION " = = "2" ] ] ; then
TS_COMMAND = " $TS_COMMAND --login-server= $LOGIN_SERVER "
fi
TS_COMMAND = " $TS_COMMAND --auth-key= $AUTH_KEY --operator= $USERNAME "
2025-09-07 10:48:30 +01:00
TS_COMMAND_SAFE = $( echo " $TS_COMMAND " | sed -E 's/--auth-key=[^[:space:]]+/--auth-key=REDACTED/g' )
print_info " Connecting to Tailscale with: $TS_COMMAND_SAFE "
2025-06-28 22:13:17 +01:00
if ! $TS_COMMAND ; then
print_warning "Failed to connect to Tailscale. Possible issues: invalid pre-auth key, network restrictions, or server unavailability."
print_info "Please run the following command manually after resolving the issue:"
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } $TS_COMMAND_SAFE ${ NC } "
2025-09-07 10:48:30 +01:00
log " Tailscale connection failed: $TS_COMMAND_SAFE "
2025-06-28 22:13:17 +01:00
else
2025-06-28 23:12:54 +01:00
# Verify connection status with retries
local RETRIES = 3
local DELAY = 5
local CONNECTED = false
2025-06-29 20:08:19 +01:00
local TS_IPS TS_IPV4
2025-06-28 23:12:54 +01:00
for ( ( i = 1; i<= RETRIES; i++) ) ; do
2025-06-29 20:08:19 +01:00
if tailscale ip >/dev/null 2>& 1; then
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
if [ [ -n " $TS_IPV4 " && " $TS_IPV4 " != "Unknown" ] ] ; then
CONNECTED = true
break
fi
2025-06-28 23:12:54 +01:00
fi
print_info " Waiting for Tailscale to connect ( $i / $RETRIES )... "
sleep $DELAY
done
if $CONNECTED ; then
2025-06-29 20:08:19 +01:00
print_success " Tailscale connected successfully. Node IPv4 in tailnet: $TS_IPV4 "
2025-09-07 10:48:30 +01:00
log " Tailscale connected: $TS_COMMAND_SAFE "
2025-06-28 23:12:54 +01:00
# Store connection details for summary
2025-06-28 23:50:46 +01:00
echo " ${ LOGIN_SERVER :- https : //controlplane.tailscale.com } " > /tmp/tailscale_server
2025-06-29 20:08:19 +01:00
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 23:12:54 +01:00
echo "None" > /tmp/tailscale_flags
2025-06-28 22:44:03 +01:00
else
2025-06-29 20:08:19 +01:00
print_warning "Tailscale connection attempt succeeded, but no IPs assigned."
print_info "Please verify with 'tailscale ip' and run the following command manually if needed:"
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } $TS_COMMAND_SAFE ${ NC } "
2025-09-07 10:48:30 +01:00
log " Tailscale connection not verified: $TS_COMMAND_SAFE "
2025-06-28 23:50:46 +01:00
tailscale status > /tmp/tailscale_status.txt 2>& 1
log "Tailscale status output saved to /tmp/tailscale_status.txt for debugging"
2025-06-28 22:44:03 +01:00
fi
2025-06-28 12:51:55 +01:00
fi
2025-06-28 22:13:17 +01:00
# --- Configure Additional Flags ---
2025-06-28 22:44:03 +01:00
print_info "Select additional Tailscale options to configure (comma-separated, e.g., 1,3):"
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } 1) SSH (--ssh) - WARNING: May restrict server access to Tailscale connections only ${ NC } "
printf '%s\n' " ${ CYAN } 2) Advertise as Exit Node (--advertise-exit-node) ${ NC } "
printf '%s\n' " ${ CYAN } 3) Accept DNS (--accept-dns) ${ NC } "
printf '%s\n' " ${ CYAN } 4) Accept Routes (--accept-routes) ${ NC } "
printf '%s\n' " ${ CYAN } Enter numbers (1-4) or leave blank to skip: ${ NC } "
2025-06-28 22:44:03 +01:00
read -rp " " TS_FLAG_CHOICES
local TS_FLAGS = ""
if [ [ -n " $TS_FLAG_CHOICES " ] ] ; then
if echo " $TS_FLAG_CHOICES " | grep -q "1" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --ssh "
fi
2025-06-28 22:44:03 +01:00
if echo " $TS_FLAG_CHOICES " | grep -q "2" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --advertise-exit-node "
fi
2025-06-28 22:44:03 +01:00
if echo " $TS_FLAG_CHOICES " | grep -q "3" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --accept-dns "
fi
2025-06-28 22:44:03 +01:00
if echo " $TS_FLAG_CHOICES " | grep -q "4" ; then
2025-06-28 22:13:17 +01:00
TS_FLAGS = " $TS_FLAGS --accept-routes "
fi
if [ [ -n " $TS_FLAGS " ] ] ; then
TS_COMMAND = "tailscale up"
if [ [ " $TS_CONNECTION " = = "2" ] ] ; then
TS_COMMAND = " $TS_COMMAND --login-server= $LOGIN_SERVER "
fi
TS_COMMAND = " $TS_COMMAND --auth-key= $AUTH_KEY --operator= $USERNAME $TS_FLAGS "
2025-09-07 10:48:30 +01:00
TS_COMMAND_SAFE = $( echo " $TS_COMMAND " | sed -E 's/--auth-key=[^[:space:]]+/--auth-key=REDACTED/g' )
print_info " Reconfiguring Tailscale with additional options: $TS_COMMAND_SAFE "
2025-06-28 22:13:17 +01:00
if ! $TS_COMMAND ; then
print_warning "Failed to reconfigure Tailscale with additional options."
print_info "Please run the following command manually after resolving the issue:"
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } $TS_COMMAND_SAFE ${ NC } "
2025-09-07 10:48:30 +01:00
log " Tailscale reconfiguration failed: $TS_COMMAND_SAFE "
2025-06-28 22:13:17 +01:00
else
2025-06-28 23:12:54 +01:00
# Verify reconfiguration status with retries
local RETRIES = 3
local DELAY = 5
local CONNECTED = false
2025-06-29 20:08:19 +01:00
local TS_IPS TS_IPV4
2025-06-28 23:12:54 +01:00
for ( ( i = 1; i<= RETRIES; i++) ) ; do
2025-06-29 20:08:19 +01:00
if tailscale ip >/dev/null 2>& 1; then
TS_IPS = $( tailscale ip 2>/dev/null || echo "Unknown" )
TS_IPV4 = $( echo " $TS_IPS " | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "Unknown" )
if [ [ -n " $TS_IPV4 " && " $TS_IPV4 " != "Unknown" ] ] ; then
CONNECTED = true
break
fi
2025-06-28 23:12:54 +01:00
fi
print_info " Waiting for Tailscale to connect ( $i / $RETRIES )... "
sleep $DELAY
done
if $CONNECTED ; then
2025-06-29 20:08:19 +01:00
print_success " Tailscale reconfigured with additional options. Node IPv4 in tailnet: $TS_IPV4 "
2025-09-07 10:48:30 +01:00
log " Tailscale reconfigured: $TS_COMMAND_SAFE "
2025-06-30 13:26:43 +01:00
# Store flags and IPs for summary
echo " $TS_FLAGS " | sed 's/ --/ /g' | sed 's/^ *//' > /tmp/tailscale_flags
2025-06-29 20:08:19 +01:00
echo " $TS_IPS " > /tmp/tailscale_ips.txt
2025-06-28 22:44:03 +01:00
else
2025-06-29 20:08:19 +01:00
print_warning "Tailscale reconfiguration attempt succeeded, but no IPs assigned."
print_info "Please verify with 'tailscale ip' and run the following command manually if needed:"
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } $TS_COMMAND_SAFE ${ NC } "
2025-06-28 22:44:03 +01:00
log " Tailscale reconfiguration not verified: $TS_COMMAND "
2025-06-28 23:50:46 +01:00
tailscale status > /tmp/tailscale_status.txt 2>& 1
log "Tailscale status output saved to /tmp/tailscale_status.txt for debugging"
2025-06-28 22:44:03 +01:00
fi
2025-06-28 22:13:17 +01:00
fi
else
2025-06-28 22:44:03 +01:00
print_info "No valid Tailscale options selected."
log "No valid Tailscale options selected."
2025-06-28 22:13:17 +01:00
fi
2025-06-28 22:44:03 +01:00
else
print_info "No additional Tailscale options selected."
log "No additional Tailscale options applied."
2025-06-28 22:13:17 +01:00
fi
print_success "Tailscale setup complete."
2025-06-29 20:08:19 +01:00
print_info "Verify status: tailscale ip"
2025-06-28 22:13:17 +01:00
log "Tailscale setup completed."
2025-06-28 12:51:55 +01:00
}
2025-06-28 11:43:10 +01:00
setup_backup( ) {
print_section "Backup Configuration (rsync over SSH)"
if ! confirm "Configure rsync-based backups to a remote SSH server?" ; then
print_info "Skipping backup configuration."
2025-06-28 14:51:26 +01:00
log "Backup configuration skipped by user."
2025-06-28 11:43:10 +01:00
return 0
fi
2025-06-28 14:51:26 +01:00
# --- Pre-flight Check ---
if [ [ -z " $USERNAME " ] ] || ! id " $USERNAME " >/dev/null 2>& 1; then
print_error " Cannot configure backup: valid admin user (' $USERNAME ') not found. "
log "Backup configuration failed: USERNAME variable not set or user does not exist."
return 1
2025-06-28 12:41:22 +01:00
fi
2025-06-28 11:43:10 +01:00
local ROOT_SSH_DIR = "/root/.ssh"
local ROOT_SSH_KEY = " $ROOT_SSH_DIR /id_ed25519 "
2025-06-28 14:51:26 +01:00
local BACKUP_SCRIPT_PATH = "/root/run_backup.sh"
local EXCLUDE_FILE_PATH = "/root/rsync_exclude.txt"
local CRON_MARKER = "#-*- managed by setup_harden script -*-"
# --- Generate SSH Key for Root ---
2025-06-28 11:43:10 +01:00
if [ [ ! -f " $ROOT_SSH_KEY " ] ] ; then
2025-06-28 14:51:26 +01:00
print_info "Generating a dedicated SSH key for root's backup job..."
mkdir -p " $ROOT_SSH_DIR " && chmod 700 " $ROOT_SSH_DIR "
ssh-keygen -t ed25519 -f " $ROOT_SSH_KEY " -N "" -q
2025-06-28 11:43:10 +01:00
chown -R root:root " $ROOT_SSH_DIR "
2025-06-28 14:51:26 +01:00
print_success " Root SSH key generated at $ROOT_SSH_KEY "
log "Generated root SSH key for backups."
2025-06-28 11:43:10 +01:00
else
2025-06-28 14:51:26 +01:00
print_info " Existing root SSH key found at $ROOT_SSH_KEY . "
2025-06-28 11:43:10 +01:00
fi
2025-06-28 16:05:32 +01:00
# --- Collect Backup Destination Details with Retry Loops ---
2025-06-28 15:25:37 +01:00
local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH SSH_COPY_ID_FLAGS = ""
2025-06-28 16:05:32 +01:00
while true; do
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${ NC } " ) " BACKUP_DEST
2025-06-28 16:05:32 +01:00
if [ [ " $BACKUP_DEST " = ~ ^[ a-zA-Z0-9._-] +@[ a-zA-Z0-9.-] +$ ] ] ; then break; else print_error "Invalid format. Expected user@host. Please try again." ; fi
done
while true; do
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter destination SSH port (Hetzner uses 23) [22]: ${ NC } " ) " BACKUP_PORT
2025-06-28 16:05:32 +01:00
BACKUP_PORT = ${ BACKUP_PORT :- 22 }
if [ [ " $BACKUP_PORT " = ~ ^[ 0-9] +$ && " $BACKUP_PORT " -ge 1 && " $BACKUP_PORT " -le 65535 ] ] ; then break; else print_error "Invalid port. Must be between 1 and 65535. Please try again." ; fi
done
while true; do
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter remote backup path (e.g., /home/my_backups/): ${ NC } " ) " REMOTE_BACKUP_PATH
2025-06-28 16:05:32 +01:00
if [ [ " $REMOTE_BACKUP_PATH " = ~ ^/[ ^[ :space:] ] */$ ] ] ; then break; else print_error "Invalid path. Must start and end with '/' and contain no spaces. Please try again." ; fi
done
2025-06-28 14:51:26 +01:00
print_info " Backup target set to: ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } on port ${ BACKUP_PORT } "
2025-06-28 16:05:32 +01:00
# --- Hetzner Specific Handling ---
if confirm "Is this backup destination a Hetzner Storage Box (requires special -s flag for key copy)?" ; then
SSH_COPY_ID_FLAGS = "-s"
2025-06-28 15:25:37 +01:00
print_info "Hetzner Storage Box mode enabled. Using '-s' for ssh-copy-id."
fi
2025-06-28 14:51:26 +01:00
# --- Handle SSH Key Copy ---
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ CYAN } Choose how to copy the root SSH key: ${ NC } "
printf ' 1) Automate with password (requires sshpass, password stored briefly in memory)\n'
printf ' 2) Manual copy (recommended)\n'
read -rp " $( printf '%s' " ${ CYAN } Enter choice (1-2) [2]: ${ NC } " ) " KEY_COPY_CHOICE
2025-06-28 14:51:26 +01:00
KEY_COPY_CHOICE = ${ KEY_COPY_CHOICE :- 2 }
if [ [ " $KEY_COPY_CHOICE " = = "1" ] ] ; then
if ! command -v sshpass >/dev/null 2>& 1; then
print_info "Installing sshpass for automated key copying..."
2025-10-14 13:04:55 +01:00
if ! { apt-get update -qq && apt-get install -y -qq sshpass; } ; then
print_warning "Failed to install sshpass. Falling back to manual copy."
KEY_COPY_CHOICE = 2
fi
2025-06-28 13:17:23 +01:00
fi
2025-06-28 14:51:26 +01:00
if [ [ " $KEY_COPY_CHOICE " = = "1" ] ] ; then
2025-10-16 11:29:53 +01:00
read -rsp " $( printf '%s' " ${ CYAN } Enter password for $BACKUP_DEST : ${ NC } " ) " BACKUP_PASSWORD; echo
2025-06-28 16:30:39 +01:00
# Ensure ~/.ssh/ exists on remote for Hetzner
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
ssh -p " $BACKUP_PORT " " $BACKUP_DEST " "mkdir -p ~/.ssh && chmod 700 ~/.ssh" 2>/dev/null || print_warning "Failed to create ~/.ssh on remote server."
fi
if SSHPASS = " $BACKUP_PASSWORD " sshpass -e ssh-copy-id -p " $BACKUP_PORT " -i " $ROOT_SSH_KEY .pub " $SSH_COPY_ID_FLAGS " $BACKUP_DEST " 2>& 1 | tee /tmp/ssh-copy-id.log; then
2025-06-28 14:51:26 +01:00
print_success "SSH key copied successfully."
2025-06-28 13:17:23 +01:00
else
2025-06-28 16:30:39 +01:00
print_error "Automated SSH key copy failed. Error details in /tmp/ssh-copy-id.log."
print_info "Please verify the password and ensure ~/.ssh/authorized_keys is writable on the remote server."
2025-06-28 14:51:26 +01:00
KEY_COPY_CHOICE = 2
2025-06-28 12:11:20 +01:00
fi
fi
2025-06-28 11:43:10 +01:00
fi
2025-06-28 14:51:26 +01:00
if [ [ " $KEY_COPY_CHOICE " = = "2" ] ] ; then
print_warning "ACTION REQUIRED: Copy the root SSH key to the backup destination."
2025-10-16 11:29:53 +01:00
printf 'This will allow the root user to connect without a password for automated backups.\n'
printf '%s' " ${ YELLOW } The root user's public key is: ${ NC } " ; cat " ${ ROOT_SSH_KEY } .pub " ; printf '\n'
printf '%s\n' " ${ YELLOW } Run the following command from this server's terminal to copy the key: ${ NC } "
printf '%s\n' " ${ CYAN } ssh-copy-id -p \" ${ BACKUP_PORT } \" -i \" ${ ROOT_SSH_KEY } .pub\" ${ SSH_COPY_ID_FLAGS } \" ${ BACKUP_DEST } \" ${ NC } " ; printf '\n'
2025-06-28 16:30:39 +01:00
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
print_info " For Hetzner, ensure ~/.ssh/ exists on the remote server: ssh -p \" $BACKUP_PORT \" \" $BACKUP_DEST \" \"mkdir -p ~/.ssh && chmod 700 ~/.ssh\" "
fi
2025-06-28 14:51:26 +01:00
fi
2025-06-28 16:05:32 +01:00
# --- SSH Connection Test ---
2025-06-28 14:51:26 +01:00
if confirm "Test SSH connection to the backup destination (recommended)?" ; then
2025-06-28 16:05:32 +01:00
print_info "Testing SSH connection (timeout: 10 seconds)..."
2025-06-28 16:30:39 +01:00
if [ [ ! -f " $ROOT_SSH_DIR /known_hosts " ] ] || ! grep -q " $BACKUP_DEST " " $ROOT_SSH_DIR /known_hosts " ; then
print_warning "SSH key may not be copied yet. Connection test may fail."
fi
local test_command = " ssh -p \" $BACKUP_PORT \" -o BatchMode=yes -o ConnectTimeout=10 \" $BACKUP_DEST \" true "
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
test_command = " sftp -P \" $BACKUP_PORT \" -o BatchMode=yes -o ConnectTimeout=10 \" $BACKUP_DEST \" <<< 'quit' "
fi
if eval " $test_command " 2>/dev/null; then
2025-06-28 15:25:37 +01:00
print_success "SSH connection to backup destination successful!"
else
2025-06-28 14:51:26 +01:00
print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open."
2025-06-28 16:05:32 +01:00
print_info " - Copy key: ssh-copy-id -p \" $BACKUP_PORT \" -i \" $ROOT_SSH_KEY .pub\" $SSH_COPY_ID_FLAGS \" $BACKUP_DEST \" "
2025-06-28 14:51:26 +01:00
print_info " - Check port: nc -zv $( echo \" $BACKUP_DEST \" | cut -d'@' -f2) \" $BACKUP_PORT \" "
2025-06-28 16:05:32 +01:00
print_info " - Ensure key is in ~/.ssh/authorized_keys on the backup server."
2025-06-28 16:30:39 +01:00
if [ [ -n " $SSH_COPY_ID_FLAGS " ] ] ; then
print_info " - For Hetzner, ensure ~/.ssh/ exists: ssh -p \" $BACKUP_PORT \" \" $BACKUP_DEST \" \"mkdir -p ~/.ssh && chmod 700 ~/.ssh\" "
fi
2025-06-28 11:43:10 +01:00
fi
fi
2025-06-28 16:30:39 +01:00
2025-07-07 20:04:19 +01:00
# --- Collect Backup Source Directories ---
local BACKUP_DIRS_ARRAY = ( )
while true; do
print_info "Enter the full paths of directories to back up, separated by spaces."
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Default is '/home/ ${ USERNAME } /'. Press Enter for default or provide your own: ${ NC } " ) " -a user_input_dirs
2025-07-07 20:04:19 +01:00
if [ ${# user_input_dirs [@] } -eq 0 ] ; then
BACKUP_DIRS_ARRAY = ( " /home/ ${ USERNAME } / " )
break
fi
local all_valid = true
for dir in " ${ user_input_dirs [@] } " ; do
if [ [ ! " $dir " = ~ ^/ ] ] ; then
print_error " Invalid path: ' $dir '. All paths must be absolute (start with '/'). Please try again. "
all_valid = false
break
fi
done
if [ [ " $all_valid " = = true ] ] ; then
BACKUP_DIRS_ARRAY = ( " ${ user_input_dirs [@] } " )
break
fi
done
# Convert array to a space-separated string for the backup script
local BACKUP_DIRS_STRING = " ${ BACKUP_DIRS_ARRAY [*] } "
print_info " Directories to be backed up: $BACKUP_DIRS_STRING "
2025-06-28 14:51:26 +01:00
# --- Create Exclude File ---
print_info " Creating rsync exclude file at $EXCLUDE_FILE_PATH ... "
tee " $EXCLUDE_FILE_PATH " > /dev/null <<'EOF'
# Default Exclusions
2025-06-28 11:43:10 +01:00
.cache/
.docker/
.local/
2025-06-28 14:51:26 +01:00
.npm/
2025-07-07 20:04:19 +01:00
.ssh/
2025-06-28 14:51:26 +01:00
.vscode-server/
*.log
2025-06-28 11:43:10 +01:00
*.tmp
2025-06-28 14:51:26 +01:00
node_modules/
2025-07-07 20:04:19 +01:00
.bashrc
2025-06-28 14:51:26 +01:00
.bash_history
2025-07-07 20:04:19 +01:00
.bash_logout
.cloud-locale-test.skip
.profile
2025-06-28 14:51:26 +01:00
.wget-hsts
2025-06-28 11:43:10 +01:00
EOF
2025-06-28 14:51:26 +01:00
if confirm "Add more directories/files to the exclude list?" ; then
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter items separated by spaces (e.g., Videos/ 'My Documents/'): ${ NC } " ) " -a extra_excludes
2025-06-28 16:05:32 +01:00
for item in " ${ extra_excludes [@] } " ; do echo " $item " >> " $EXCLUDE_FILE_PATH " ; done
2025-06-28 11:43:10 +01:00
fi
2025-06-28 14:51:26 +01:00
chmod 600 " $EXCLUDE_FILE_PATH "
2025-06-28 11:43:10 +01:00
print_success "Rsync exclude file created."
2025-06-28 14:51:26 +01:00
# --- Collect Cron Schedule ---
2025-06-28 16:55:55 +01:00
local CRON_SCHEDULE = "5 3 * * *"
2025-10-16 18:09:47 +01:00
print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help."
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter schedule (default: daily at 3:05 AM) [ ${ CRON_SCHEDULE } ]: ${ NC } " ) " input
2025-06-28 16:55:55 +01:00
CRON_SCHEDULE = " ${ input :- $CRON_SCHEDULE } "
if ! echo " $CRON_SCHEDULE " | grep -qE '^((\*\/)?[0-9,-]+|\*)\s+(((\*\/)?[0-9,-]+|\*)\s+){3}((\*\/)?[0-9,-]+|\*|[0-6])$' ; then
print_error " Invalid cron expression. Using default: ${ CRON_SCHEDULE } "
fi
2025-06-28 14:51:26 +01:00
# --- Collect Notification Details ---
local NOTIFICATION_SETUP = "none" NTFY_URL = "" NTFY_TOKEN = "" DISCORD_WEBHOOK = ""
if confirm "Enable backup status notifications?" ; then
2025-10-16 11:29:53 +01:00
printf '%s' " ${ CYAN } Select notification method: 1) ntfy.sh 2) Discord [1]: ${ NC } " ; read -r n_choice
2025-06-28 14:51:26 +01:00
if [ [ " $n_choice " = = "2" ] ] ; then
NOTIFICATION_SETUP = "discord"
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter Discord Webhook URL: ${ NC } " ) " DISCORD_WEBHOOK
2025-06-28 17:42:48 +01:00
if [ [ ! " $DISCORD_WEBHOOK " = ~ ^https://discord.com/api/webhooks/ ] ] ; then
print_error "Invalid Discord webhook URL."
log "Invalid Discord webhook URL provided."
return 1
fi
2025-06-28 14:51:26 +01:00
else
NOTIFICATION_SETUP = "ntfy"
2025-10-16 11:29:53 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter ntfy URL/topic (e.g., https://ntfy.sh/my-backups): ${ NC } " ) " NTFY_URL
read -rp " $( printf '%s' " ${ CYAN } Enter ntfy Access Token (optional): ${ NC } " ) " NTFY_TOKEN
2025-06-28 17:42:48 +01:00
if [ [ ! " $NTFY_URL " = ~ ^https?:// ] ] ; then
print_error "Invalid ntfy URL."
log "Invalid ntfy URL provided."
return 1
fi
2025-06-28 14:51:26 +01:00
fi
2025-06-28 11:43:10 +01:00
fi
2025-06-28 14:51:26 +01:00
# --- Generate the Backup Script ---
print_info " Generating the backup script at $BACKUP_SCRIPT_PATH ... "
2025-06-28 17:42:48 +01:00
if ! tee " $BACKUP_SCRIPT_PATH " > /dev/null <<EOF
2025-06-28 11:43:10 +01:00
#!/bin/bash
2025-06-28 14:51:26 +01:00
# Generated by server setup script on $(date)
2025-06-28 16:05:32 +01:00
set -Euo pipefail; umask 077
2025-06-28 14:51:26 +01:00
# --- CONFIGURATION ---
2025-07-07 20:04:19 +01:00
BACKUP_DIRS = " ${ BACKUP_DIRS_STRING } "
2025-06-28 14:51:26 +01:00
REMOTE_DEST = " ${ BACKUP_DEST } "
REMOTE_PATH = " ${ REMOTE_BACKUP_PATH } "
SSH_PORT = " ${ BACKUP_PORT } "
EXCLUDE_FILE = " ${ EXCLUDE_FILE_PATH } "
LOG_FILE = "/var/log/backup_rsync.log"
LOCK_FILE = "/tmp/backup_rsync.lock"
HOSTNAME = "\$(hostname -f)"
NOTIFICATION_SETUP = " ${ NOTIFICATION_SETUP } "
NTFY_URL = " ${ NTFY_URL } "
NTFY_TOKEN = " ${ NTFY_TOKEN } "
DISCORD_WEBHOOK = " ${ DISCORD_WEBHOOK } "
2025-06-28 11:43:10 +01:00
EOF
2025-06-28 17:42:48 +01:00
then
print_error " Failed to create backup script at $BACKUP_SCRIPT_PATH . "
log " Failed to create backup script at $BACKUP_SCRIPT_PATH . "
return 1
fi
if ! tee -a " $BACKUP_SCRIPT_PATH " > /dev/null <<'EOF'
2025-06-28 16:05:32 +01:00
# --- BACKUP SCRIPT LOGIC ---
2025-06-28 11:43:10 +01:00
send_notification( ) {
2025-06-28 16:05:32 +01:00
local status = " $1 " message = " $2 " title color
if [ [ " $status " = = "SUCCESS" ] ] ; then title = " ✅ Backup SUCCESS: $HOSTNAME " ; color = 3066993; else title = " ❌ Backup FAILED: $HOSTNAME " ; color = 15158332; fi
2025-06-28 14:51:26 +01:00
if [ [ " $NOTIFICATION_SETUP " = = "ntfy" ] ] ; then
2025-06-28 16:05:32 +01:00
curl -s -H " Title: $title " ${ NTFY_TOKEN : +-H " Authorization: Bearer $NTFY_TOKEN " } -d " $message " " $NTFY_URL " > /dev/null 2>& 1
2025-06-28 14:51:26 +01:00
elif [ [ " $NOTIFICATION_SETUP " = = "discord" ] ] ; then
2025-06-28 16:05:32 +01:00
local escaped_message = $( echo " $message " | sed 's/"/\\"/g' | sed 's/\\/\\\\/g' | sed ':a;N;$!ba;s/\n/\\n/g' )
local json_payload = $( printf '{"embeds": [{"title": "%s", "description": "%s", "color": %d}]}' " $title " " $escaped_message " " $color " )
2025-06-28 14:51:26 +01:00
curl -s -H "Content-Type: application/json" -d " $json_payload " " $DISCORD_WEBHOOK " > /dev/null 2>& 1
2025-06-28 11:43:10 +01:00
fi
}
2025-06-28 16:05:32 +01:00
# --- DEPENDENCY & LOCKING ---
2025-06-28 16:30:39 +01:00
for cmd in rsync flock numfmt awk; do if ! command -v " $cmd " & >/dev/null; then send_notification "FAILURE" " FATAL: ' $cmd ' not found. " ; exit 10; fi ; done
2025-06-28 16:05:32 +01:00
exec 200>" $LOCK_FILE " ; flock -n 200 || { echo "Backup already running." ; exit 1; }
# --- LOG ROTATION ---
touch " $LOG_FILE " ; chmod 600 " $LOG_FILE " ; if [ [ -f " $LOG_FILE " && $( stat -c%s " $LOG_FILE " ) -gt 10485760 ] ] ; then mv " $LOG_FILE " " ${ LOG_FILE } .1 " ; fi
2025-06-28 14:51:26 +01:00
echo " --- Starting Backup at $( date) --- " >> " $LOG_FILE "
2025-06-28 16:05:32 +01:00
# --- RSYNC COMMAND ---
2025-07-07 20:04:19 +01:00
rsync_output = $( rsync -avz --delete --stats --exclude-from= " $EXCLUDE_FILE " -e " ssh -p $SSH_PORT " $BACKUP_DIRS " ${ REMOTE_DEST } : ${ REMOTE_PATH } " 2>& 1)
2025-06-28 16:05:32 +01:00
rsync_exit_code = $? ; echo " $rsync_output " >> " $LOG_FILE "
# --- NOTIFICATION ---
2025-06-28 14:51:26 +01:00
if [ [ $rsync_exit_code -eq 0 ] ] ; then
data_transferred = $( echo " $rsync_output " | grep 'Total transferred file size' | awk '{print $5}' | sed 's/,//g' )
2025-06-28 15:25:37 +01:00
human_readable = $( numfmt --to= iec-i --suffix= B --format= "%.2f" " $data_transferred " 2>/dev/null || echo "0 B" )
2025-06-28 16:05:32 +01:00
message = " Backup completed successfully.\nData Transferred: ${ human_readable } "
2025-06-28 14:51:26 +01:00
send_notification "SUCCESS" " $message "
2025-06-28 11:43:10 +01:00
else
2025-06-28 14:51:26 +01:00
message = " rsync failed with exit code ${ rsync_exit_code } . Check log for details. "
send_notification "FAILURE" " $message "
2025-06-28 11:43:10 +01:00
fi
EOF
2025-06-28 17:42:48 +01:00
then
print_error " Failed to append to backup script at $BACKUP_SCRIPT_PATH . "
log " Failed to append to backup script at $BACKUP_SCRIPT_PATH . "
return 1
fi
if ! chmod 700 " $BACKUP_SCRIPT_PATH " ; then
print_error " Failed to set permissions on $BACKUP_SCRIPT_PATH . "
log " Failed to set permissions on $BACKUP_SCRIPT_PATH . "
return 1
fi
2025-06-28 14:51:26 +01:00
print_success "Backup script created."
2025-06-29 13:10:41 +01:00
# --- Backup test ---
test_backup
2025-06-28 14:51:26 +01:00
# --- Configure Cron Job ---
print_info "Configuring root cron job..."
2025-06-28 17:42:48 +01:00
# Ensure crontab is writable
local CRON_DIR = "/var/spool/cron/crontabs"
mkdir -p " $CRON_DIR "
chmod 1730 " $CRON_DIR "
chown root:crontab " $CRON_DIR "
# Validate inputs
if [ [ -z " $CRON_SCHEDULE " || -z " $BACKUP_SCRIPT_PATH " ] ] ; then
print_error "Cron schedule or backup script path is empty."
log " Cron configuration failed: CRON_SCHEDULE=' $CRON_SCHEDULE ', BACKUP_SCRIPT_PATH=' $BACKUP_SCRIPT_PATH ' "
return 1
fi
if [ [ ! -f " $BACKUP_SCRIPT_PATH " ] ] ; then
print_error " Backup script $BACKUP_SCRIPT_PATH does not exist. "
log " Cron configuration failed: Backup script $BACKUP_SCRIPT_PATH not found. "
return 1
fi
# Create temporary cron file
local TEMP_CRON
TEMP_CRON = $( mktemp)
if ! crontab -u root -l 2>/dev/null | grep -v " $CRON_MARKER " > " $TEMP_CRON " ; then
print_warning "No existing crontab found or error reading crontab. Creating new one."
: > " $TEMP_CRON " # Create empty file
fi
echo " $CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER " >> " $TEMP_CRON "
if ! crontab -u root " $TEMP_CRON " 2>& 1 | tee -a " $LOG_FILE " ; then
print_error "Failed to configure cron job."
log "Cron configuration failed: Error updating crontab."
rm -f " $TEMP_CRON "
return 1
fi
rm -f " $TEMP_CRON "
2025-06-28 14:51:26 +01:00
print_success " Backup cron job scheduled: $CRON_SCHEDULE "
2025-06-28 11:43:10 +01:00
log "Backup configuration completed."
}
2025-06-29 13:10:41 +01:00
test_backup( ) {
print_section "Backup Configuration Test"
2025-07-01 15:27:04 +01:00
# Ensure script is running with effective root privileges
if [ [ $( id -u) -ne 0 ] ] ; then
print_error "Backup test must be run as root. Re-run with 'sudo -E' or as root."
log " Backup test failed: Script not run as root (UID $( id -u) ). "
return 0
fi
local BACKUP_SCRIPT_PATH = "/root/run_backup.sh"
2025-07-07 19:09:41 +01:00
if [ [ ! -f " $BACKUP_SCRIPT_PATH " || ! -r " $BACKUP_SCRIPT_PATH " ] ] ; then
print_error " Backup script not found or not readable at $BACKUP_SCRIPT_PATH . "
log "Backup test failed: Script not found or not readable."
2025-07-01 15:27:04 +01:00
return 0
fi
if ! command -v timeout >/dev/null 2>& 1; then
print_error "The 'timeout' command is not available. Please install coreutils."
log "Backup test failed: 'timeout' command not found."
return 0
2025-06-29 13:10:41 +01:00
fi
if ! confirm "Run a test backup to verify configuration?" ; then
print_info "Skipping backup test."
log "Backup test skipped by user."
return 0
fi
2025-07-07 19:09:41 +01:00
# Extract backup configuration from the generated backup script
local BACKUP_DEST REMOTE_BACKUP_PATH BACKUP_PORT
2025-07-01 15:27:04 +01:00
BACKUP_DEST = $( grep "^REMOTE_DEST=" " $BACKUP_SCRIPT_PATH " | cut -d'"' -f2 2>/dev/null || echo "unknown" )
BACKUP_PORT = $( grep "^SSH_PORT=" " $BACKUP_SCRIPT_PATH " | cut -d'"' -f2 2>/dev/null || echo "22" )
REMOTE_BACKUP_PATH = $( grep "^REMOTE_PATH=" " $BACKUP_SCRIPT_PATH " | cut -d'"' -f2 2>/dev/null || echo "unknown" )
2025-06-29 13:10:41 +01:00
local BACKUP_LOG = "/var/log/backup_rsync.log"
if [ [ " $BACKUP_DEST " = = "unknown" || " $REMOTE_BACKUP_PATH " = = "unknown" ] ] ; then
2025-07-07 19:09:41 +01:00
print_error " Could not parse backup configuration from $BACKUP_SCRIPT_PATH . "
2025-07-01 15:27:04 +01:00
log " Backup test failed: Invalid configuration in $BACKUP_SCRIPT_PATH . "
return 0
2025-06-29 13:10:41 +01:00
fi
2025-07-07 19:09:41 +01:00
# Create a temporary directory and file for the test
2025-10-14 21:46:39 +01:00
local TEST_DIR
TEST_DIR = " /root/test_backup_ $( date +%Y%m%d_%H%M%S) "
2025-07-07 19:09:41 +01:00
if ! mkdir -p " $TEST_DIR " || ! echo "Test file for backup verification" > " $TEST_DIR /test.txt " ; then
print_error "Failed to create test directory or file in /root/."
log "Backup test failed: Cannot create test directory/file."
2025-07-01 15:27:04 +01:00
rm -rf " $TEST_DIR " 2>/dev/null
return 0
fi
2025-06-29 13:10:41 +01:00
print_info " Running test backup to $BACKUP_DEST : $REMOTE_BACKUP_PATH ... "
2025-07-01 15:27:04 +01:00
local RSYNC_OUTPUT RSYNC_EXIT_CODE TIMEOUT_DURATION = 120
2025-07-07 19:09:41 +01:00
local SSH_KEY = "/root/.ssh/id_ed25519"
2025-07-01 15:27:04 +01:00
local SSH_COMMAND = " ssh -p $BACKUP_PORT -i $SSH_KEY -o BatchMode=yes -o StrictHostKeyChecking=no "
2025-07-07 19:09:41 +01:00
set +e
2025-07-01 15:27:04 +01:00
RSYNC_OUTPUT = $( timeout " $TIMEOUT_DURATION " rsync -avz --delete -e " $SSH_COMMAND " " $TEST_DIR / " " ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } test_backup/ " 2>& 1)
RSYNC_EXIT_CODE = $?
2025-07-07 19:09:41 +01:00
set -e # Re-enable 'exit on error'
2025-06-29 13:10:41 +01:00
echo " --- Test Backup at $( date) --- " >> " $BACKUP_LOG "
echo " $RSYNC_OUTPUT " >> " $BACKUP_LOG "
if [ [ $RSYNC_EXIT_CODE -eq 0 ] ] ; then
print_success " Test backup successful! Check $BACKUP_LOG for details. "
log "Test backup successful."
else
2025-07-07 19:09:41 +01:00
print_warning "The backup test failed. This is not critical, and the script will continue."
print_info "You can troubleshoot this after the server setup is complete."
2025-07-07 19:17:26 +01:00
2025-07-01 15:27:04 +01:00
if [ [ $RSYNC_EXIT_CODE -eq 124 ] ] ; then
2025-07-07 19:09:41 +01:00
print_error " Test backup timed out after $TIMEOUT_DURATION seconds. "
2025-07-01 15:27:04 +01:00
log " Test backup failed: Timeout after $TIMEOUT_DURATION seconds. "
else
2025-07-07 19:09:41 +01:00
print_error " Test backup failed (exit code: $RSYNC_EXIT_CODE ). See $BACKUP_LOG for details. "
2025-07-01 15:27:04 +01:00
log " Test backup failed with exit code $RSYNC_EXIT_CODE . "
fi
2025-06-29 13:10:41 +01:00
2025-07-07 19:09:41 +01:00
print_info "Common troubleshooting steps:"
print_info " - Ensure the root SSH key is copied to the destination: ssh-copy-id -p \" $BACKUP_PORT \" -i \" $SSH_KEY .pub\" \" $BACKUP_DEST \" "
2025-07-07 19:17:26 +01:00
print_info " - Check firewall rules on both this server and the destination."
2025-07-01 15:27:04 +01:00
fi
2025-07-07 19:09:41 +01:00
# Clean up the temporary test directory
rm -rf " $TEST_DIR " 2>/dev/null
2025-06-29 13:10:41 +01:00
print_success "Backup test completed."
log "Backup test completed."
2025-07-01 15:27:04 +01:00
return 0
2025-06-29 13:10:41 +01:00
}
2025-06-26 16:22:23 +01:00
configure_swap( ) {
2025-06-26 21:49:51 +01:00
if [ [ $IS_CONTAINER = = true ] ] ; then
print_info "Swap configuration skipped in container."
2025-06-26 16:22:23 +01:00
return 0
fi
2025-06-26 21:49:51 +01:00
print_section "Swap Configuration"
2025-06-29 19:09:50 +01:00
# Check for existing swap partition
if lsblk -r | grep -q '\[SWAP\]' ; then
print_warning "Existing swap partition found. Verify with 'lsblk -f'. Proceed with caution."
fi
2025-06-26 21:49:51 +01:00
local existing_swap
existing_swap = $( swapon --show --noheadings | awk '{print $1}' || true )
if [ [ -n " $existing_swap " ] ] ; then
local current_size
2025-10-14 13:04:55 +01:00
current_size = $( du -h " $existing_swap " | awk '{print $1}' )
2025-06-26 21:49:51 +01:00
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
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter new swap size (e.g., 2G, 512M) [current: $current_size ]: ${ NC } " ) " SWAP_SIZE
2025-06-26 21:49:51 +01:00
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
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter swap file size (e.g., 2G, 512M) [2G]: ${ NC } " ) " SWAP_SIZE
2025-06-26 21:49:51 +01:00
SWAP_SIZE = ${ SWAP_SIZE :- 2G }
if validate_swap_size " $SWAP_SIZE " ; then
break
else
print_error "Invalid size. Use format like '2G' or '512M'."
fi
done
2025-06-26 22:19:35 +01:00
local REQUIRED_SPACE
2025-06-26 21:49:51 +01:00
REQUIRED_SPACE = $( convert_to_bytes " $SWAP_SIZE " )
2025-06-26 22:19:35 +01:00
local AVAILABLE_SPACE
2025-06-26 21:49:51 +01:00
AVAILABLE_SPACE = $( df -k / | tail -n 1 | awk '{print $4}' )
if ( ( AVAILABLE_SPACE < REQUIRED_SPACE / 1024 ) ) ; then
print_error " Insufficient disk space for $SWAP_SIZE swap file. Available: $(( AVAILABLE_SPACE / 1024 )) MB "
exit 1
fi
print_info " Creating $SWAP_SIZE swap file... "
if ! fallocate -l " $SWAP_SIZE " /swapfile || ! chmod 600 /swapfile || ! mkswap /swapfile || ! swapon /swapfile; then
print_error "Failed to create or enable swap file."
2025-06-26 22:19:35 +01:00
rm -f /swapfile || true
2025-06-26 21:49:51 +01:00
exit 1
fi
2025-06-27 13:47:19 +01:00
# Check for existing swap entry in /etc/fstab to prevent duplicates
if grep -q '^/swapfile ' /etc/fstab; then
print_info "Swap entry already exists in /etc/fstab. Skipping."
else
2025-06-26 21:49:51 +01:00
echo '/swapfile none swap sw 0 0' >> /etc/fstab
2025-06-27 13:47:19 +01:00
print_success "Swap entry added to /etc/fstab."
2025-06-26 21:49:51 +01:00
fi
print_success " Swap file created: $SWAP_SIZE "
2025-06-26 16:22:23 +01:00
fi
2025-06-27 12:56:02 +01:00
print_info "Configuring swap settings..."
local SWAPPINESS = 10
local CACHE_PRESSURE = 50
if confirm "Customize swap settings (vm.swappiness and vm.vfs_cache_pressure)?" ; then
while true; do
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter vm.swappiness (0-100) [default: $SWAPPINESS ]: ${ NC } " ) " INPUT_SWAPPINESS
2025-06-27 12:56:02 +01:00
INPUT_SWAPPINESS = ${ INPUT_SWAPPINESS :- $SWAPPINESS }
if [ [ " $INPUT_SWAPPINESS " = ~ ^[ 0-9] +$ && " $INPUT_SWAPPINESS " -ge 0 && " $INPUT_SWAPPINESS " -le 100 ] ] ; then
SWAPPINESS = $INPUT_SWAPPINESS
break
else
print_error "Invalid value for vm.swappiness. Must be between 0 and 100."
fi
done
while true; do
2025-10-16 18:09:47 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter vm.vfs_cache_pressure (1-1000) [default: $CACHE_PRESSURE ]: ${ NC } " ) " INPUT_CACHE_PRESSURE
2025-06-27 12:56:02 +01:00
INPUT_CACHE_PRESSURE = ${ INPUT_CACHE_PRESSURE :- $CACHE_PRESSURE }
if [ [ " $INPUT_CACHE_PRESSURE " = ~ ^[ 0-9] +$ && " $INPUT_CACHE_PRESSURE " -ge 1 && " $INPUT_CACHE_PRESSURE " -le 1000 ] ] ; then
CACHE_PRESSURE = $INPUT_CACHE_PRESSURE
break
else
print_error "Invalid value for vm.vfs_cache_pressure. Must be between 1 and 1000."
fi
done
else
print_info " Using default swap settings (vm.swappiness= $SWAPPINESS , vm.vfs_cache_pressure= $CACHE_PRESSURE ). "
fi
2025-06-26 22:19:35 +01:00
local NEW_SWAP_CONFIG
2025-06-26 21:49:51 +01:00
NEW_SWAP_CONFIG = $( mktemp)
tee " $NEW_SWAP_CONFIG " > /dev/null <<EOF
2025-06-27 12:56:02 +01:00
vm.swappiness= $SWAPPINESS
vm.vfs_cache_pressure= $CACHE_PRESSURE
2025-06-26 21:49:51 +01:00
EOF
2025-06-27 13:47:19 +01:00
# Check if sysctl settings are already correct to prevent duplicates
2025-06-26 21:49:51 +01:00
if [ [ -f /etc/sysctl.d/99-swap.conf ] ] && cmp -s " $NEW_SWAP_CONFIG " /etc/sysctl.d/99-swap.conf; then
2025-06-27 13:47:19 +01:00
print_info "Swap settings already correct in /etc/sysctl.d/99-swap.conf. Skipping."
2025-06-26 21:49:51 +01:00
rm -f " $NEW_SWAP_CONFIG "
else
2025-06-27 13:47:19 +01:00
# Check for conflicting settings in /etc/sysctl.conf or other sysctl files
local sysctl_conflicts = false
for file in /etc/sysctl.conf /etc/sysctl.d/*.conf; do
if [ [ -f " $file " && " $file " != "/etc/sysctl.d/99-swap.conf" ] ] ; then
if grep -E '^(vm\.swappiness|vm\.vfs_cache_pressure)=' " $file " >/dev/null; then
print_warning " Existing swap settings found in $file . Manual review recommended. "
sysctl_conflicts = true
fi
fi
done
2025-06-26 21:49:51 +01:00
mv " $NEW_SWAP_CONFIG " /etc/sysctl.d/99-swap.conf
chmod 644 /etc/sysctl.d/99-swap.conf
sysctl -p /etc/sysctl.d/99-swap.conf >/dev/null
2025-06-27 13:47:19 +01:00
if [ [ $sysctl_conflicts = = true ] ] ; then
print_warning "Potential conflicting sysctl settings detected. Verify with 'sysctl -a | grep -E \"vm\.swappiness|vm\.vfs_cache_pressure\"'."
else
print_success "Swap settings applied to /etc/sysctl.d/99-swap.conf."
fi
2025-06-26 16:22:23 +01:00
fi
2025-06-26 21:49:51 +01:00
print_success "Swap configured successfully."
swapon --show | tee -a " $LOG_FILE "
free -h | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
log "Swap configuration completed."
}
2025-06-26 21:49:51 +01:00
configure_time_sync( ) {
2025-06-26 16:22:23 +01:00
print_section "Time Synchronization"
2025-06-26 21:49:51 +01:00
print_info "Ensuring chrony is active..."
systemctl enable --now chrony
sleep 2
if systemctl is-active --quiet chrony; then
print_success "Chrony is active for time synchronization."
chronyc tracking | tee -a " $LOG_FILE "
2025-06-26 16:22:23 +01:00
else
2025-06-26 21:49:51 +01:00
print_error "Chrony service failed to start."
2025-06-26 16:22:23 +01:00
exit 1
fi
2025-06-26 17:12:59 +01:00
log "Time synchronization completed."
2025-06-26 16:22:23 +01:00
}
2025-06-29 13:10:41 +01:00
configure_security_audit( ) {
print_section "Security Audit Configuration"
2025-06-29 18:38:12 +01:00
if ! confirm "Run a security audit with Lynis (and optionally debsecan on Debian)?" ; then
print_info "Security audit skipped."
2025-06-29 13:10:41 +01:00
log "Security audit skipped by user."
2025-06-29 18:38:12 +01:00
AUDIT_RAN = false
2025-06-29 13:10:41 +01:00
return 0
fi
2025-06-29 18:38:12 +01:00
AUDIT_LOG = " /var/log/setup_harden_security_audit_ $( date +%Y%m%d_%H%M%S) .log "
2025-06-29 13:10:41 +01:00
touch " $AUDIT_LOG " && chmod 600 " $AUDIT_LOG "
2025-06-29 18:38:12 +01:00
AUDIT_RAN = true
HARDENING_INDEX = ""
DEBSECAN_VULNS = "Not run"
2025-06-29 13:10:41 +01:00
2025-06-29 18:38:12 +01:00
# Install and run Lynis
2025-06-29 13:10:41 +01:00
print_info "Installing Lynis..."
if ! apt-get update -qq; then
2025-06-29 18:38:12 +01:00
print_error "Failed to update package lists. Cannot install Lynis."
log "apt-get update failed for Lynis installation."
return 1
elif ! apt-get install -y -qq lynis; then
print_warning "Failed to install Lynis. Skipping Lynis audit."
log "Lynis installation failed."
2025-06-29 13:10:41 +01:00
else
2025-06-29 23:34:32 +01:00
print_info "Running Lynis audit (non-interactive mode, this will take a few minutes)..."
2025-06-30 13:26:43 +01:00
print_warning " Review audit results in $AUDIT_LOG for security recommendations. "
2025-06-29 18:38:12 +01:00
if lynis audit system --quick >> " $AUDIT_LOG " 2>& 1; then
2025-06-29 13:10:41 +01:00
print_success " Lynis audit completed. Check $AUDIT_LOG for details. "
2025-06-29 18:38:12 +01:00
log "Lynis audit completed successfully."
# Extract hardening index
HARDENING_INDEX = $( grep -oP "Hardening index : \K\d+" " $AUDIT_LOG " || echo "Unknown" )
2025-08-03 22:57:35 +01:00
#Extract top suggestions
grep "Suggestion:" /var/log/lynis-report.dat | head -n 5 > /tmp/lynis_suggestions.txt 2>/dev/null || true
2025-06-29 18:38:12 +01:00
# Append Lynis system log for persistence
cat /var/log/lynis.log >> " $AUDIT_LOG " 2>/dev/null
2025-06-29 13:10:41 +01:00
else
print_error " Lynis audit failed. Check $AUDIT_LOG for details. "
log "Lynis audit failed."
fi
fi
2025-06-29 18:38:12 +01:00
# Check if system is Debian before running debsecan
source /etc/os-release
if [ [ " $ID " = = "debian" ] ] ; then
if confirm "Also run debsecan to check for package vulnerabilities?" ; then
print_info "Installing debsecan..."
if ! apt-get install -y -qq debsecan; then
print_warning "Failed to install debsecan. Skipping debsecan audit."
log "debsecan installation failed."
2025-06-29 13:10:41 +01:00
else
2025-06-29 18:38:12 +01:00
print_info "Running debsecan audit..."
if debsecan --suite " $VERSION_CODENAME " >> " $AUDIT_LOG " 2>& 1; then
DEBSECAN_VULNS = $( grep -c "CVE-" " $AUDIT_LOG " || echo "0" )
print_success " debsecan audit completed. Found $DEBSECAN_VULNS vulnerabilities. "
log " debsecan audit completed with $DEBSECAN_VULNS vulnerabilities. "
else
print_error " debsecan audit failed. Check $AUDIT_LOG for details. "
log "debsecan audit failed."
DEBSECAN_VULNS = "Failed"
fi
2025-06-29 13:10:41 +01:00
fi
2025-06-29 18:38:12 +01:00
else
print_info "debsecan audit skipped."
log "debsecan audit skipped by user."
2025-06-29 13:10:41 +01:00
fi
else
2025-06-29 18:38:12 +01:00
print_info "debsecan is not supported on Ubuntu. Skipping debsecan audit."
log "debsecan audit skipped (Ubuntu detected)."
DEBSECAN_VULNS = "Not supported on Ubuntu"
2025-06-29 13:10:41 +01:00
fi
print_warning " Review audit results in $AUDIT_LOG for security recommendations. "
log "Security audit configuration completed."
}
2025-06-26 21:49:51 +01:00
final_cleanup( ) {
print_section "Final System Cleanup"
print_info "Running final system update and cleanup..."
if ! apt-get update -qq; then
2025-06-26 22:19:35 +01:00
print_warning "Failed to update package lists during final cleanup."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
if ! apt-get upgrade -y -qq || ! apt-get --purge autoremove -y -qq || ! apt-get autoclean -y -qq; then
2025-06-26 22:19:35 +01:00
print_warning "Final system cleanup failed on one or more commands."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
systemctl daemon-reload
print_success "Final system update and cleanup complete."
log "Final system cleanup completed."
}
generate_summary( ) {
2025-07-15 14:03:44 +01:00
# Create the report file and set permissions first
2025-07-15 13:51:34 +01:00
touch " $REPORT_FILE " && chmod 600 " $REPORT_FILE "
2025-07-15 14:03:44 +01:00
# Using a subshell to group all output and tee it to the report file
2025-07-15 13:51:34 +01:00
(
2025-06-26 21:49:51 +01:00
print_section "Setup Complete!"
2025-07-15 13:51:34 +01:00
2025-10-16 11:29:53 +01:00
printf '\n%s\n\n' " ${ GREEN } Server setup and hardening script has finished successfully. ${ NC } "
printf '%s %s\n' " ${ CYAN } 📋 A detailed report has been saved to: ${ NC } " " ${ BOLD } $REPORT_FILE ${ NC } "
printf '%s %s\n' " ${ CYAN } 📜 The full execution log is available at: ${ NC } " " ${ BOLD } $LOG_FILE ${ NC } "
printf '\n'
2025-07-15 13:51:34 +01:00
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ YELLOW } Environment Information ${ NC } "
printf '=====================================\n'
2025-10-13 22:52:07 +01:00
printf "%-20s %s\n" "Virtualization:" " ${ DETECTED_VIRT_TYPE :- unknown } "
printf "%-20s %s\n" "Manufacturer:" " ${ DETECTED_MANUFACTURER :- unknown } "
printf "%-20s %s\n" "Product:" " ${ DETECTED_PRODUCT :- unknown } "
if [ [ " $IS_CLOUD_PROVIDER " = = "true" ] ] ; then
printf "%-20s %s\n" "Environment:" " ${ YELLOW } Cloud VPS ${ NC } "
elif [ [ " $DETECTED_VIRT_TYPE " = = "none" ] ] ; then
printf "%-20s %s\n" "Environment:" " ${ GREEN } Bare Metal ${ NC } "
else
printf "%-20s %s\n" "Environment:" " ${ CYAN } Personal VM ${ NC } "
fi
2025-10-16 11:29:53 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ YELLOW } Final Service Status Check: ${ NC } "
2025-06-26 21:49:51 +01:00
for service in " $SSH_SERVICE " fail2ban chrony; do
if systemctl is-active --quiet " $service " ; then
2025-07-15 13:51:34 +01:00
printf " %-20s ${ GREEN } ✓ Active ${ NC } \n " " $service "
2025-06-26 21:49:51 +01:00
else
2025-07-15 13:51:34 +01:00
printf " %-20s ${ RED } ✗ INACTIVE ${ NC } \n " " $service "
2025-07-15 14:03:44 +01:00
FAILED_SERVICES += ( " $service " )
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
done
if ufw status | grep -q "Status: active" ; then
2025-07-15 13:51:34 +01:00
printf " %-20s ${ GREEN } ✓ Active ${ NC } \n " "ufw (firewall)"
2025-06-26 21:49:51 +01:00
else
2025-07-15 13:51:34 +01:00
printf " %-20s ${ RED } ✗ INACTIVE ${ NC } \n " "ufw (firewall)"
2025-07-15 14:03:44 +01:00
FAILED_SERVICES += ( "ufw" )
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
if command -v docker >/dev/null 2>& 1; then
if systemctl is-active --quiet docker; then
2025-07-15 13:51:34 +01:00
printf " %-20s ${ GREEN } ✓ Active ${ NC } \n " "docker"
2025-06-26 21:49:51 +01:00
else
2025-07-15 13:51:34 +01:00
printf " %-20s ${ RED } ✗ INACTIVE ${ NC } \n " "docker"
2025-07-15 14:03:44 +01:00
FAILED_SERVICES += ( "docker" )
2025-06-26 21:49:51 +01:00
fi
2025-06-26 17:12:59 +01:00
fi
2025-06-28 11:43:10 +01:00
if command -v tailscale >/dev/null 2>& 1; then
2025-07-15 14:03:44 +01:00
if systemctl is-active --quiet tailscaled && tailscale ip >/dev/null 2>& 1; then
printf " %-20s ${ GREEN } ✓ Active & Connected ${ NC } \n " "tailscaled"
tailscale ip 2>/dev/null > /tmp/tailscale_ips.txt || true
else
if grep -q "Tailscale connection failed: tailscale up" " $LOG_FILE " ; then
printf " %-20s ${ RED } ✗ INACTIVE (Connection Failed) ${ NC } \n " "tailscaled"
FAILED_SERVICES += ( "tailscaled" )
TS_COMMAND = $( grep "Tailscale connection failed: tailscale up" " $LOG_FILE " | tail -1 | sed 's/.*Tailscale connection failed: //' )
TS_COMMAND = ${ TS_COMMAND :- "" }
2025-07-04 16:42:46 +01:00
else
2025-07-15 14:03:44 +01:00
printf " %-20s ${ YELLOW } ⚠ Installed but not configured ${ NC } \n " "tailscaled"
TS_COMMAND = ""
2025-07-04 16:42:46 +01:00
fi
2025-07-15 13:51:34 +01:00
fi
2025-06-28 11:43:10 +01:00
fi
2025-07-15 14:03:44 +01:00
if [ [ " ${ AUDIT_RAN :- false } " = = true ] ] ; then
printf " %-20s ${ GREEN } ✓ Performed ${ NC } \n " "Security Audit"
else
printf " %-20s ${ YELLOW } ⚠ Not Performed ${ NC } \n " "Security Audit"
fi
2025-07-15 13:51:34 +01:00
echo
# --- Main Configuration Summary ---
2025-10-16 11:29:53 +01:00
printf '%s\n' " ${ YELLOW } Configuration Summary: ${ NC } "
2025-08-26 15:41:00 +01:00
printf " %-15s %s\n" "Admin User:" " $USERNAME "
printf " %-15s %s\n" "Hostname:" " $SERVER_NAME "
printf " %-15s %s\n" "SSH Port:" " $SSH_PORT "
2025-08-26 16:14:58 +01:00
if [ [ " $SERVER_IP_V4 " != "unknown" ] ] ; then
printf " %-15s %s\n" "Server IPv4:" " $SERVER_IP_V4 "
fi
2025-08-26 11:40:41 +01:00
if [ [ " $SERVER_IP_V6 " != "not available" ] ] ; then
2025-08-26 15:41:00 +01:00
printf " %-15s %s\n" "Server IPv6:" " $SERVER_IP_V6 "
2025-08-26 11:40:41 +01:00
fi
2025-07-15 13:51:34 +01:00
# --- Kernel Hardening Status ---
if [ [ -f /etc/sysctl.d/99-du-hardening.conf ] ] ; then
printf " %-20s ${ GREEN } Applied ${ NC } \n " "Kernel Hardening:"
2025-06-29 18:38:12 +01:00
else
2025-07-15 13:51:34 +01:00
printf " %-20s ${ YELLOW } Not Applied ${ NC } \n " "Kernel Hardening:"
2025-06-29 18:38:12 +01:00
fi
2025-07-15 13:51:34 +01:00
# --- Backup Configuration Summary ---
2025-06-28 17:42:48 +01:00
if [ [ -f /root/run_backup.sh ] ] ; then
2025-10-14 21:46:39 +01:00
local CRON_SCHEDULE NOTIFICATION_STATUS BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH
CRON_SCHEDULE = $( crontab -u root -l 2>/dev/null | grep -F "/root/run_backup.sh" | awk '{print $1, $2, $3, $4, $5}' || echo "Not configured" )
NOTIFICATION_STATUS = "None"
BACKUP_DEST = $( grep "^REMOTE_DEST=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown" )
BACKUP_PORT = $( grep "^SSH_PORT=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown" )
REMOTE_BACKUP_PATH = $( grep "^REMOTE_PATH=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown" )
2025-06-28 17:42:48 +01:00
if grep -q "NTFY_URL=" /root/run_backup.sh && ! grep -q 'NTFY_URL=""' /root/run_backup.sh; then
2025-06-28 11:43:10 +01:00
NOTIFICATION_STATUS = "ntfy"
2025-06-28 17:42:48 +01:00
elif grep -q "DISCORD_WEBHOOK=" /root/run_backup.sh && ! grep -q 'DISCORD_WEBHOOK=""' /root/run_backup.sh; then
2025-06-28 11:43:10 +01:00
NOTIFICATION_STATUS = "Discord"
2025-06-28 17:42:48 +01:00
fi
2025-10-16 12:29:34 +01:00
printf '%s\n' " Remote Backup: ${ GREEN } Enabled ${ NC } "
2025-07-15 13:51:34 +01:00
printf " %-17s%s\n" "- Backup Script:" "/root/run_backup.sh"
printf " %-17s%s\n" "- Destination:" " $BACKUP_DEST "
printf " %-17s%s\n" "- SSH Port:" " $BACKUP_PORT "
printf " %-17s%s\n" "- Remote Path:" " $REMOTE_BACKUP_PATH "
printf " %-17s%s\n" "- Cron Schedule:" " $CRON_SCHEDULE "
printf " %-17s%s\n" "- Notifications:" " $NOTIFICATION_STATUS "
2025-06-29 18:38:12 +01:00
if [ [ -f " $BACKUP_LOG " ] ] && grep -q "Test backup successful" " $BACKUP_LOG " 2>/dev/null; then
2025-07-15 13:51:34 +01:00
printf " %-17s%s\n" "- Test Status:" " ${ GREEN } Successful ${ NC } "
2025-06-29 18:38:12 +01:00
elif [ [ -f " $BACKUP_LOG " ] ] ; then
2025-07-15 13:51:34 +01:00
printf " %-17s%s\n" "- Test Status:" " Failed (check $BACKUP_LOG ) "
2025-06-29 18:38:12 +01:00
else
2025-07-15 13:51:34 +01:00
printf " %-17s%s\n" "- Test Status:" "Not run"
2025-06-29 18:38:12 +01:00
fi
2025-06-28 11:43:10 +01:00
else
2025-10-16 12:29:34 +01:00
printf '%s\n' " Remote Backup: ${ RED } Not configured ${ NC } "
2025-06-28 11:43:10 +01:00
fi
2025-07-15 13:51:34 +01:00
# --- Tailscale Summary ---
2025-06-28 22:13:17 +01:00
if command -v tailscale >/dev/null 2>& 1; then
2025-07-15 13:51:34 +01:00
local TS_CONFIGURED = false
if [ [ -f /tmp/tailscale_ips.txt ] ] && grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' /tmp/tailscale_ips.txt 2>/dev/null; then
TS_CONFIGURED = true
fi
if $TS_CONFIGURED ; then
2025-10-14 21:46:39 +01:00
local TS_SERVER TS_IPS_RAW TS_IPS TS_FLAGS
TS_SERVER = $( cat /tmp/tailscale_server 2>/dev/null || echo "https://controlplane.tailscale.com" )
TS_IPS_RAW = $( cat /tmp/tailscale_ips.txt 2>/dev/null || echo "Not connected" )
TS_IPS = $( echo " $TS_IPS_RAW " | paste -sd ", " -)
TS_FLAGS = $( cat /tmp/tailscale_flags 2>/dev/null || echo "None" )
2025-10-16 12:29:34 +01:00
printf '%s\n' " Tailscale: ${ GREEN } Configured and connected ${ NC } "
2025-07-15 13:51:34 +01:00
printf " %-17s%s\n" "- Server:" " ${ TS_SERVER :- Not set } "
printf " %-17s%s\n" "- Tailscale IPs:" " ${ TS_IPS :- Not connected } "
printf " %-17s%s\n" "- Flags:" " ${ TS_FLAGS :- None } "
else
2025-10-16 12:29:34 +01:00
printf '%s\n' " Tailscale: ${ YELLOW } Installed but not configured ${ NC } "
2025-07-15 13:51:34 +01:00
fi
2025-06-28 22:13:17 +01:00
else
2025-10-16 12:29:34 +01:00
printf '%s\n' " Tailscale: ${ RED } Not installed ${ NC } "
2025-06-28 22:13:17 +01:00
fi
2025-07-15 13:51:34 +01:00
# --- Security Audit Summary ---
2025-07-15 14:03:44 +01:00
if [ [ " ${ AUDIT_RAN :- false } " = = true ] ] ; then
2025-10-16 12:29:34 +01:00
printf '%s\n' " Security Audit: ${ GREEN } Performed ${ NC } "
2025-07-15 14:03:44 +01:00
printf " %-17s%s\n" "- Audit Log:" " ${ AUDIT_LOG :- N /A } "
2025-07-15 13:51:34 +01:00
printf " %-17s%s\n" "- Hardening Index:" " ${ HARDENING_INDEX :- Unknown } "
2025-07-15 14:03:44 +01:00
printf " %-17s%s\n" "- Vulnerabilities:" " ${ DEBSECAN_VULNS :- N /A } "
2025-08-03 22:57:35 +01:00
if [ [ -s /tmp/lynis_suggestions.txt ] ] ; then
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ YELLOW } - Top Lynis Suggestions: ${ NC } "
2025-08-03 22:57:35 +01:00
sed 's/^/ /' /tmp/lynis_suggestions.txt
fi
2025-06-29 18:38:12 +01:00
else
2025-10-16 12:29:34 +01:00
printf '%s\n' " Security Audit: ${ RED } Not run ${ NC } "
2025-06-29 18:38:12 +01:00
fi
2025-06-26 21:49:51 +01:00
echo
2025-07-15 13:51:34 +01:00
2025-08-26 14:05:48 +01:00
# --- Post-Reboot Verification Steps ---
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ YELLOW } Post-Reboot Verification Steps: ${ NC } "
printf ' - SSH access:\n'
2025-08-26 16:14:58 +01:00
if [ [ " $SERVER_IP_V4 " != "unknown" ] ] ; then
2025-08-26 21:05:00 +01:00
printf " %-26s ${ CYAN } %s ${ NC } \n " "- Using IPv4:" " ssh -p $SSH_PORT $USERNAME @ $SERVER_IP_V4 "
2025-08-26 16:14:58 +01:00
fi
2025-08-26 14:05:48 +01:00
if [ [ " $SERVER_IP_V6 " != "not available" ] ] ; then
2025-08-26 21:05:00 +01:00
printf " %-26s ${ CYAN } %s ${ NC } \n " "- Using IPv6:" " ssh -p $SSH_PORT $USERNAME @ $SERVER_IP_V6 "
2025-08-26 14:05:48 +01:00
fi
2025-08-26 16:14:58 +01:00
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Firewall rules:" "sudo ufw status verbose"
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Time sync:" "chronyc tracking"
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Fail2Ban sshd jail:" "sudo fail2ban-client status sshd"
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Fail2Ban ufw jail:" "sudo fail2ban-client status ufw-probes"
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Swap status:" "sudo swapon --show && free -h"
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Kernel settings:" "sudo sysctl fs.protected_hardlinks kernel.yama.ptrace_scope"
2025-06-26 21:49:51 +01:00
if command -v docker >/dev/null 2>& 1; then
2025-08-26 16:14:58 +01:00
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Docker status:" "docker ps"
2025-06-26 21:49:51 +01:00
fi
if command -v tailscale >/dev/null 2>& 1; then
2025-08-26 16:14:58 +01:00
printf " %-28s ${ CYAN } %s ${ NC } \n " "- Tailscale status:" "tailscale status"
2025-06-26 21:49:51 +01:00
fi
2025-06-28 17:42:48 +01:00
if [ [ -f /root/run_backup.sh ] ] ; then
2025-10-16 12:29:34 +01:00
printf ' Remote Backup:\n'
2025-07-15 13:51:34 +01:00
printf " %-23s ${ CYAN } %s ${ NC } \n " "- Test backup:" "sudo /root/run_backup.sh"
printf " %-23s ${ CYAN } %s ${ NC } \n " "- Check logs:" " sudo less $BACKUP_LOG "
2025-06-29 18:38:12 +01:00
fi
2025-07-15 14:03:44 +01:00
if [ [ " ${ AUDIT_RAN :- false } " = = true ] ] ; then
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ YELLOW } Security Audit: ${ NC } "
2025-07-15 14:03:44 +01:00
printf " %-23s ${ CYAN } %s ${ NC } \n " "- Check results:" " sudo less ${ AUDIT_LOG :- /var/log/syslog } "
2025-06-29 18:38:12 +01:00
fi
2025-07-15 13:51:34 +01:00
echo
# --- Final Warnings and Actions ---
2025-06-29 20:08:19 +01:00
if [ [ ${# FAILED_SERVICES [@] } -gt 0 ] ] ; then
print_warning " ACTION REQUIRED: The following services failed: ${ FAILED_SERVICES [*] } . Verify with 'systemctl status <service>'. "
fi
2025-07-15 14:03:44 +01:00
if [ [ -n " ${ TS_COMMAND :- } " ] ] ; then
2025-06-29 20:08:19 +01:00
print_warning "ACTION REQUIRED: Tailscale connection failed. Run the following command to connect manually:"
2025-10-16 12:29:34 +01:00
printf '%s\n' " ${ CYAN } $TS_COMMAND ${ NC } "
2025-06-29 20:08:19 +01:00
fi
2025-07-15 14:03:44 +01:00
if [ [ -f /root/run_backup.sh ] ] && [ [ " ${ KEY_COPY_CHOICE :- 2 } " != "1" ] ] ; then
2025-07-15 13:51:34 +01:00
print_warning "ACTION REQUIRED: Ensure the root SSH key (/root/.ssh/id_ed25519.pub) is copied to the backup destination."
2025-06-28 22:13:17 +01:00
fi
2025-07-15 13:51:34 +01:00
2025-06-28 11:43:10 +01:00
print_warning "A reboot is required to apply all changes cleanly."
2025-06-26 21:49:51 +01:00
if [ [ $VERBOSE = = true ] ] ; then
if confirm "Reboot now?" "y" ; then
2025-06-28 23:12:54 +01:00
print_info "Rebooting, bye!..."
2025-06-26 21:49:51 +01:00
sleep 3
reboot
else
print_warning "Please reboot manually with 'sudo reboot'."
fi
else
print_warning "Quiet mode enabled. Please reboot manually with 'sudo reboot'."
2025-06-26 17:12:59 +01:00
fi
2025-07-15 13:51:34 +01:00
) | tee -a " $REPORT_FILE "
log " Script finished successfully. Report generated at $REPORT_FILE "
2025-06-26 21:49:51 +01:00
}
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
2025-08-03 23:05:44 +01:00
trap 'rm -f /tmp/lynis_suggestions.txt /tmp/tailscale_*.txt /tmp/sshd_config_test.log /tmp/ssh*.log /tmp/sshd_restart*.log' EXIT
2025-07-02 15:01:58 +01:00
if [ [ $( id -u) -ne 0 ] ] ; then
2025-10-16 12:29:34 +01:00
printf '\n%s\n' " ${ RED } ✗ Error: This script must be run with root privileges. ${ NC } "
printf 'You are running as user ' \' '%s' \' ', but root is required for system changes.\n' " $( whoami) "
printf 'Please re-run the script using ' \' 'sudo -E' \' ':\n'
printf ' %s\n\n' " ${ CYAN } sudo -E ./du_setup.sh ${ NC } "
2025-07-02 15:01:58 +01:00
exit 1
fi
2025-06-26 21:49:51 +01:00
touch " $LOG_FILE " && chmod 600 " $LOG_FILE "
log "Starting Debian/Ubuntu hardening script."
2025-06-28 11:43:10 +01:00
2025-10-15 21:00:20 +01:00
# --- PRELIMINARY CHECKS ---
2025-10-15 21:09:51 +01:00
print_header
2025-10-13 22:52:07 +01:00
check_system
2025-10-15 21:00:20 +01:00
run_update_check
check_dependencies
2025-10-14 01:00:59 +01:00
2025-10-15 21:00:20 +01:00
# --- HANDLE SPECIAL OPERATIONAL MODES ---
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_ONLY " = = "true" ] ] ; then
print_info "Running in cleanup-only mode..."
detect_environment
cleanup_provider_packages
print_success "Cleanup-only mode completed."
exit 0
fi
2025-10-14 01:00:59 +01:00
2025-10-13 22:52:07 +01:00
if [ [ " $CLEANUP_PREVIEW " = = "true" ] ] ; then
print_info "Running cleanup preview mode..."
detect_environment
cleanup_provider_packages
print_success "Cleanup preview completed."
exit 0
fi
2025-10-14 01:00:59 +01:00
2025-10-15 21:00:20 +01:00
# --- NORMAL EXECUTION FLOW ---
# Detect environment used for the summary report at the end.
2025-10-13 22:52:07 +01:00
detect_environment
2025-10-15 21:00:20 +01:00
# --- CORE SETUP AND HARDENING ---
2025-06-26 16:22:23 +01:00
collect_config
install_packages
setup_user
configure_system
configure_firewall
configure_fail2ban
2025-10-15 21:36:01 +01:00
configure_ssh
2025-06-26 16:22:23 +01:00
configure_auto_updates
2025-06-26 21:49:51 +01:00
configure_time_sync
2025-07-15 13:51:34 +01:00
configure_kernel_hardening
2025-06-26 16:22:23 +01:00
install_docker
install_tailscale
2025-06-28 11:43:10 +01:00
setup_backup
2025-06-26 16:22:23 +01:00
configure_swap
2025-06-29 13:10:41 +01:00
configure_security_audit
2025-10-15 21:00:20 +01:00
# --- PROVIDER PACKAGE CLEANUP ---
if [ [ " $SKIP_CLEANUP " = = "false" ] ] ; then
cleanup_provider_packages
else
print_info "Skipping provider cleanup (--skip-cleanup flag set)."
log "Provider cleanup skipped via --skip-cleanup flag."
fi
# --- FINAL STEPS ---
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 " $@ "