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-11-27 15:14:06 +00:00
# Version: 0.78.4 | 2025-11-27
2025-06-28 11:43:10 +01:00
# Changelog:
2025-11-27 22:18:17 +00:00
# - v0.78.4: Improved configure_swap to detect swap partitions vs files.
2025-11-27 15:14:06 +00:00
# Prevents 'fallocate' crashes on physical partitions by offering to disable them or skip.
2025-11-26 11:06:12 +00:00
# - v0.78.3: Update the summary to try to show the right environment detection based on finding personal VMs and cloud VPS.
# Run update & upgrade in the final step to ensure system is fully updated after restart.
2025-11-25 17:36:10 +00:00
# - v0.78.2: In configure_system set choosen hostname from collect_config in the /etc/hosts
2025-11-25 16:22:21 +00:00
# - v0.78.1: Collect config failure fixed on IPv6 only VPS.
2025-11-24 17:39:56 +00:00
# - v0.78: Script tries to handles different environments: Direct Public IP, NAT/Router and Local VM only
# The configure_ssh function provides context-aware instructions based on different environments.
2025-11-25 00:31:44 +00:00
# In setup_user handle if group exists but user doesn't - attach user to existing group.
2025-11-24 14:52:56 +00:00
# - v0.77.2: Fixed an unbound variable for SSH when on a local virtual machine;
# check_dependencies should come before check_system to keep minimal servers from failing.
2025-11-19 12:00:43 -06:00
# - v0.77.1: Auto SSH connection whitelist feat & whitelist deduplication.
2025-11-17 09:53:10 +00:00
# - v0.77: User-configurable ignoreip functionality for configure_fail2ban function.
# Add a few more core packages in install_packages function.
2025-11-10 14:37:06 +00:00
# - v0.76: Improve the flexibility of the built-in Docker daemon.json file to prevent any potential Docker issues.
2025-11-09 18:43:06 +00:00
# - v0.75: Updated Docker daemon.json file to be more secure.
2025-11-06 21:20:56 +00:00
# - v0.74: Add optional dtop (https://github.com/amir20/dtop) after docker installation.
#. Update .bashrc
2025-10-22 13:21:00 +01:00
# - v0.73: Revised/improved logic in .bashrc for memory and system updates.
2025-10-20 17:19:23 +01:00
# - v0.72: Added configure_custom_bashrc() function that creates and installs a feature-rich .bashrc file during user creation.
2025-10-20 08:58:36 +01:00
# - v0.71: Simplify test backup function to work reliably with Hetzner storagebox
# - v0.70.1: Fix SSH port validation and improve firewall handling during SSH port transitions.
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-17 22:28:10 +01:00
# Improve SSH port validation and rollback logic.
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
2025-10-19 19:11:35 +01:00
set -euo pipefail
2025-06-26 16:22:23 +01:00
2025-07-15 15:13:58 +01:00
# --- Update Configuration ---
2025-11-27 15:14:06 +00:00
CURRENT_VERSION = "0.78.4"
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-16 19:07:16 +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 = ""
2025-06-26 16:22:23 +01:00
IS_CONTAINER = false
2025-11-26 11:06:12 +00:00
ENVIRONMENT_TYPE = "unknown"
DETECTED_PROVIDER_NAME = ""
2025-10-15 18:03:22 +01:00
2025-11-25 00:13:34 +00:00
SERVER_IP_V4 = "Unknown"
SERVER_IP_V6 = "Not available"
LOCAL_IP_V4 = ""
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-10-19 19:01:46 +01:00
PREVIOUS_SSH_PORT = ""
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 "
2025-10-17 21:06:34 +01:00
printf " %ssudo su %s # Switch to root\n" " $CYAN " " $NC "
2025-10-15 17:27:09 +01:00
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-11-27 15:14:06 +00:00
printf '%s\n' " ${ CYAN } ║ v0.78.4 | 2025-11-27 ║ ${ 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-16 21:27:38 +01:00
print_separator( ) {
local header_text = " $1 "
local color = " ${ 2 :- $YELLOW } "
local separator_char = " ${ 3 :- = } "
2025-10-17 23:06:37 +01:00
2025-10-16 21:27:38 +01:00
printf '%s\n' " ${ color } ${ header_text } ${ NC } "
printf " ${ separator_char } %.0s " $( seq 1 ${# header_text } )
printf '\n'
}
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 } "
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."
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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-17 23:01:22 +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 ==="
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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."
2025-10-16 18:29:39 +01:00
printf '\n'
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
2025-10-16 18:29:39 +01:00
printf '\n'
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
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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."
2025-10-16 18:29:39 +01:00
printf '\n'
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-16 18:29:39 +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 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
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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
2025-10-16 18:29:39 +01:00
printf '\n'
2025-10-13 22:52:07 +01:00
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-10-20 13:26:08 +01:00
configure_custom_bashrc( ) {
local USER_HOME = " $1 "
local USERNAME = " $2 "
local BASHRC_PATH = " $USER_HOME /.bashrc "
2025-10-20 14:45:38 +01:00
local temp_source_bashrc = ""
2025-10-20 16:46:47 +01:00
local keep_temp_source_on_error = false
2025-10-20 14:45:38 +01:00
2025-10-20 16:46:47 +01:00
trap 'rm -f "$temp_source_bashrc" 2>/dev/null' INT TERM
2025-10-20 13:26:08 +01:00
if ! confirm " Replace default .bashrc for ' $USERNAME ' with a custom one? " "n" ; then
print_info "Skipping custom .bashrc configuration."
log " Skipped custom .bashrc for $USERNAME . "
return 0
fi
2025-10-20 14:45:38 +01:00
print_info " Preparing custom .bashrc for ' $USERNAME '... "
temp_source_bashrc = $( mktemp "/tmp/custom_bashrc_source.XXXXXX" )
if [ [ -z " $temp_source_bashrc " || ! -f " $temp_source_bashrc " ] ] ; then
print_error "Failed to create temporary file for .bashrc content."
log "Error: mktemp failed for bashrc source."
return 0
fi
chmod 600 " $temp_source_bashrc "
2025-10-20 13:26:08 +01:00
2025-10-20 16:46:47 +01:00
if ! cat > " $temp_source_bashrc " <<'EOF'
2025-10-20 13:26:08 +01:00
# shellcheck shell=bash
# ===================================================================
# Universal Portable .bashrc for Modern Terminals
# Optimized for Debian/Ubuntu servers with multi-terminal support
# ===================================================================
# If not running interactively, don't do anything.
case $- in
*i*) ; ;
*) return ; ;
esac
# --- History Control ---
# Don't put duplicate lines or lines starting with space in the history.
HISTCONTROL = ignoreboth:erasedups
# Append to the history file, don't overwrite it.
shopt -s histappend
# Set history length with reasonable values for server use.
HISTSIZE = 10000
HISTFILESIZE = 20000
# Allow editing of commands recalled from history.
shopt -s histverify
# Add timestamp to history entries for audit trail (ISO 8601 format).
HISTTIMEFORMAT = "%Y-%m-%d %H:%M:%S "
# Ignore common commands from history to reduce clutter.
HISTIGNORE = "ls:ll:la:l:cd:pwd:exit:clear:c:history:h"
# --- General Shell Behavior & Options ---
# Check the window size after each command and update LINES and COLUMNS.
shopt -s checkwinsize
# Allow using '**' for recursive globbing (Bash 4.0+, suppress errors on older versions).
shopt -s globstar 2>/dev/null
# Allow changing to a directory by just typing its name (Bash 4.0+).
shopt -s autocd 2>/dev/null
# Autocorrect minor spelling errors in directory names (Bash 4.0+).
shopt -s cdspell 2>/dev/null
shopt -s dirspell 2>/dev/null
# Correct multi-line command editing.
shopt -s cmdhist 2>/dev/null
# Case-insensitive globbing (commented out to avoid unexpected behavior).
# shopt -s nocaseglob 2>/dev/null
# Set command-line editing mode. Emacs (default) or Vi.
set -o emacs
# For vi keybindings, uncomment the following line and comment the one above:
# set -o vi
# Make `less` more friendly for non-text input files.
[ -x /usr/bin/lesspipe ] && eval " $( SHELL = /bin/sh lesspipe) "
# --- Better Less Configuration ---
# Make less more friendly - R shows colors, F quits if one screen, X prevents screen clear.
export LESS = '-R -F -X -i -M -w'
# Colored man pages using less (TERMCAP sequences).
export LESS_TERMCAP_mb = $'\e[1;31m' # begin blink
export LESS_TERMCAP_md = $'\e[1;36m' # begin bold
export LESS_TERMCAP_me = $'\e[0m' # reset bold/blink
export LESS_TERMCAP_so = $'\e[01;44;33m' # begin reverse video
export LESS_TERMCAP_se = $'\e[0m' # reset reverse video
export LESS_TERMCAP_us = $'\e[1;32m' # begin underline
export LESS_TERMCAP_ue = $'\e[0m' # reset underline
# --- Terminal & SSH Compatibility Fixes ---
# Handle Kitty terminal over SSH - fallback to xterm-256color if terminfo unavailable.
if [ [ " $TERM " = = "xterm-kitty" ] ] ; then
# Check if kitty terminfo is available, otherwise fallback.
if ! infocmp xterm-kitty & >/dev/null; then
export TERM = xterm-256color
fi
# Ensure the shell looks for user-specific terminfo files.
[ [ -d " $HOME /.terminfo " ] ] && export TERMINFO = " $HOME /.terminfo "
fi
# Fix for other modern terminals that might not be recognized on older servers.
case " $TERM " in
alacritty| wezterm)
if ! infocmp " $TERM " & >/dev/null; then
export TERM = xterm-256color
fi
; ;
esac
# Optional: if kitty exists locally, provide a convenience alias for SSH.
# (No effect on hosts without kitty installed.)
if command -v kitty & >/dev/null; then
alias kssh = 'kitty +kitten ssh'
fi
# --- Prompt Configuration ---
# Set variable identifying the chroot you work in (used in the prompt below).
if [ -z " ${ debian_chroot :- } " ] && [ -r /etc/debian_chroot ] ; then
debian_chroot = $( </etc/debian_chroot)
fi
# Set a colored prompt only if the terminal has color capability.
case " $TERM " in
xterm-color| *-256color| xterm-kitty| alacritty| wezterm) color_prompt = yes; ;
esac
# Force color prompt support check using tput.
if [ -z " ${ color_prompt } " ] && [ -x /usr/bin/tput ] && tput setaf 1 & >/dev/null; then
color_prompt = yes
fi
# --- Function to parse git branch only if in a git repo ---
parse_git_branch( ) {
if git rev-parse --git-dir & >/dev/null; then
git branch 2>/dev/null | sed -n '/^\*/s/* \(.*\)/\1/p'
fi
return 0
}
# --- Main prompt command function ---
__bash_prompt_command( ) {
local rc = $? # Capture last command exit status
history -a
history -n
# --- Initialize prompt components ---
local prompt_err = "" prompt_git = "" prompt_jobs = "" prompt_venv = ""
local git_branch job_count
# Error indicator
( ( rc != 0 ) ) && prompt_err = "\[\e[31m\]✗\[\e[0m\]"
# Git branch (dim yellow)
git_branch = $( parse_git_branch)
[ [ -n " $git_branch " ] ] && prompt_git = " \[\e[2;33m\]( $git_branch )\[\e[0m\] "
# Background jobs (cyan)
job_count = $( jobs -p | wc -l)
( ( job_count > 0 ) ) && prompt_jobs = " \[\e[36m\]⚡ ${ job_count } \[\e[0m\] "
# Python virtualenv (dim green)
[ [ -n " $VIRTUAL_ENV " ] ] && prompt_venv = " \[\e[2;32m\][ ${ VIRTUAL_ENV ##*/ } ]\[\e[0m\] "
# Ensure spacing between components
[ [ -n " $prompt_venv " ] ] && prompt_venv = " $prompt_venv "
[ [ -n " $prompt_git " ] ] && prompt_git = " $prompt_git "
[ [ -n " $prompt_jobs " ] ] && prompt_jobs = " $prompt_jobs "
[ [ -n " $prompt_err " ] ] && prompt_err = " $prompt_err "
# --- Assemble PS1 ---
if [ " $color_prompt " = yes ] ; then
PS1 = '${debian_chroot:+($debian_chroot)}\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]' " ${ prompt_venv } ${ prompt_git } ${ prompt_jobs } ${ prompt_err } " ' \$ '
else
PS1 = '${debian_chroot:+($debian_chroot)}\u@\h:\w' " ${ prompt_venv } ${ git_branch } ${ prompt_jobs } ${ prompt_err } " ' \$ '
fi
# --- Set Terminal Window Title ---
case " $TERM " in
xterm*| rxvt*| xterm-kitty| alacritty| wezterm)
PS1 = " \[\e]0; ${ debian_chroot : +( $debian_chroot ) } \u@\h: \w\a\] $PS1 "
; ;
esac
}
# --- Activate dynamic prompt ---
PROMPT_COMMAND = __bash_prompt_command
# --- Editor Configuration ---
if command -v vim & >/dev/null; then
export EDITOR = vim
export VISUAL = vim
2025-11-06 21:20:56 +00:00
elif command -v nano & >/dev/null; then
2025-10-20 13:26:08 +01:00
export EDITOR = nano
export VISUAL = nano
2025-11-06 21:20:56 +00:00
else
export EDITOR = vi
export VISUAL = vi
2025-10-20 13:26:08 +01:00
fi
# --- Additional Environment Variables ---
# Set default pager.
export PAGER = less
# Prevent Ctrl+S from freezing the terminal.
stty -ixon 2>/dev/null
# --- Useful Functions ---
# Create a directory and change into it.
mkcd( ) {
mkdir -p " $1 " && cd " $1 "
}
# Create a backup of a file with timestamp.
backup( ) {
if [ -f " $1 " ] ; then
2025-11-06 21:20:56 +00:00
local backup_file; backup_file = " $1 .backup- $( date +%Y%m%d-%H%M%S) "
2025-10-20 13:26:08 +01:00
cp " $1 " " $backup_file "
echo " Backup created: $backup_file "
else
echo " ' $1 ' is not a valid file " >& 2
return 1
fi
}
# Extract any archive file with a single command.
extract( ) {
if [ -f " $1 " ] ; then
case " $1 " in
2025-11-06 21:20:56 +00:00
*.tar.bz2) tar xjf " $1 " ; ;
*.tar.gz) tar xzf " $1 " ; ;
*.tar.xz) tar xJf " $1 " ; ;
*.bz2) bunzip2 " $1 " ; ;
*.rar) unrar x " $1 " ; ;
*.gz) gunzip " $1 " ; ;
*.tar) tar xf " $1 " ; ;
*.tbz2) tar xjf " $1 " ; ;
*.tgz) tar xzf " $1 " ; ;
*.zip) unzip " $1 " ; ;
*.Z) uncompress " $1 " ; ;
*.7z) 7z x " $1 " ; ;
*.deb) ar x " $1 " ; ;
2025-10-20 13:26:08 +01:00
*.tar.zst)
if command -v zstd & >/dev/null; then
zstd -dc " $1 " | tar xf -
else
tar --zstd -xf " $1 "
fi
; ;
*)
echo " ' $1 ' cannot be extracted via extract() " >& 2
2025-11-06 21:20:56 +00:00
return 1 # Add return 1 for consistency
2025-10-20 13:26:08 +01:00
; ;
esac
else
echo " ' $1 ' is not a valid file " >& 2
return 1
fi
}
# Quick directory navigation up multiple levels.
up( ) {
local d = ""
local limit = " ${ 1 :- 1 } "
for ( ( i = 1; i<= limit; i++) ) ; do
d = " ../ $d "
done
cd " $d " || return
}
# Find files by name in current directory tree.
ff( ) {
find . -type f -iname " * $1 * " 2>/dev/null
}
# Find directories by name in current directory tree.
fd( ) {
find . -type d -iname " * $1 * " 2>/dev/null
}
# Search for text in files recursively.
ftext( ) {
grep -rnw . -e " $1 " 2>/dev/null
}
2025-11-06 21:20:56 +00:00
# Search history easily
hgrep( ) { history | grep -i --color= auto " $@ " ; }
2025-10-20 13:26:08 +01:00
# Create a tarball of a directory.
targz( ) {
if [ -d " $1 " ] ; then
tar czf " ${ 1 %%/ } .tar.gz " " ${ 1 %%/ } "
echo " Created ${ 1 %%/ } .tar.gz "
else
echo " ' $1 ' is not a valid directory " >& 2
return 1
fi
}
# Show disk usage of current directory, sorted by size.
duh( ) {
du -h --max-depth= 1 " ${ 1 :- . } " | sort -hr
}
# Get the size of a file or directory.
sizeof( ) {
du -sh " $1 " 2>/dev/null
}
# Show most used commands from history.
histop( ) {
2025-11-06 21:20:56 +00:00
history | awk -v ig = " $HISTIGNORE " ' BEGIN{ OFS = "\t" ; gsub( /:/,"|" ,ig) ; ir = "^(" ig" )( $| ) " ; sr = "(^|\\s)\\./" }
{ cmd = $4 ; for ( i = 5; i<= NF; i++) cmd = cmd" " $i }
( cmd = = "" || cmd~ir|| cmd~sr) { next}
{ C[ cmd] ++; t++}
END{ if ( t>0) for ( a in C) printf"%d\t%.2f%%\t%s\n" ,C[ a] ,( C[ a] /t*100) ,a} ' |
sort -nr | head -n20 |
awk ' BEGIN{
FS = "\t" ;
maxc = length( "COUNT" ) ;
maxp = length( "PERCENT" ) ;
}
{
data[ NR] = $0 ;
len1 = length( $1 ) ;
len2 = length( $2 ) ;
if ( len1>maxc) maxc = len1;
if ( len2>maxp) maxp = len2;
}
END{
fmt = " %-4s %-*s %-*s %s\n" ;
printf fmt,"RANK" ,maxc,"COUNT" ,maxp,"PERCENT" ,"COMMAND" ;
sep_c = sep_p = "" ;
for ( i = 1; i<= maxc; i++) sep_c = sep_c"-" ;
for ( i = 1; i<= maxp; i++) sep_p = sep_p"-" ;
printf fmt,"----" ,maxc,sep_c,maxp,sep_p,"-------" ;
for ( i = 1; i<= NR; i++) {
split( data[ i] ,f,"\t" ) ;
printf fmt,i"." ,maxc,f[ 1] ,maxp,f[ 2] ,f[ 3]
}
} '
2025-10-20 13:26:08 +01:00
}
# Quick server info display
sysinfo( ) {
# --- Self-Contained Color Detection ---
local color_support = ""
case " $TERM " in
xterm-color| *-256color| xterm-kitty| alacritty| wezterm) color_support = "yes" ; ;
esac
if [ -z " $color_support " ] && [ -x /usr/bin/tput ] && tput setaf 1 & >/dev/null; then
color_support = "yes"
fi
# --- Color Definitions ---
if [ " $color_support " = "yes" ] ; then
local CYAN = '\e[1;36m'
local YELLOW = '\e[1;33m'
local BOLD_RED = '\e[1;31m'
local BOLD_WHITE = '\e[1;37m'
local GREEN = '\e[1;32m'
local DIM = '\e[2m'
local RESET = '\e[0m'
else
local CYAN = '' YELLOW = '' BOLD_RED = '' BOLD_WHITE = '' GREEN = '' DIM = '' RESET = ''
fi
# --- Header ---
printf " \n ${ BOLD_WHITE } === System Information === ${ RESET } \n "
# --- CPU Info ---
local cpu_info
cpu_info = $( lscpu | awk -F: '/Model name/ {print $2; exit}' | xargs || grep -m1 'model name' /proc/cpuinfo | cut -d ':' -f2 | xargs)
[ -z " $cpu_info " ] && cpu_info = "Unknown"
2025-11-06 21:20:56 +00:00
# --- IP Detection ---
local ip_addr public_ipv4 public_ipv6
# Try to get public IPv4 first
public_ipv4 = $( curl -4 -s -m 2 --connect-timeout 1 https://checkip.amazonaws.com 2>/dev/null || \
curl -4 -s -m 2 --connect-timeout 1 https://ipconfig.io 2>/dev/null || \
curl -4 -s -m 2 --connect-timeout 1 https://api.ipify.org 2>/dev/null)
# If no IPv4, try IPv6
if [ -z " $public_ipv4 " ] ; then
public_ipv6 = $( curl -6 -s -m 2 --connect-timeout 1 https://ipconfig.io 2>/dev/null || \
curl -6 -s -m 2 --connect-timeout 1 https://icanhazip.co 2>/dev/null || \
curl -6 -s -m 2 --connect-timeout 1 https://api64.ipify.org 2>/dev/null)
fi
# Get local/internal IP as fallback
for iface in eth0 ens3 enp0s3 enp0s6 wlan0 ens33 eno1; do
2025-10-20 13:26:08 +01:00
ip_addr = $( ip -4 addr show " $iface " 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
[ -n " $ip_addr " ] && break
done
2025-11-06 21:20:56 +00:00
[ -z " $ip_addr " ] && ip_addr = $( ip -4 addr show scope global 2>/dev/null | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
2025-10-20 13:26:08 +01:00
# --- System Info ---
2025-11-06 21:20:56 +00:00
if [ -n " $public_ipv4 " ] ; then
# Show public IPv4 (preferred)
printf " ${ CYAN } %-15s ${ RESET } %s ${ YELLOW } [%s] ${ RESET } " "Hostname:" " $( hostname) " " $public_ipv4 "
# Show local IP if different from public
if [ -n " $ip_addr " ] && [ " $ip_addr " != " $public_ipv4 " ] ; then
printf " ${ DIM } (local: %s) ${ RESET } \n " " $ip_addr "
else
printf "\n"
fi
elif [ -n " $public_ipv6 " ] ; then
# Show public IPv6 if no IPv4
printf " ${ CYAN } %-15s ${ RESET } %s ${ YELLOW } [%s] ${ RESET } " "Hostname:" " $( hostname) " " $public_ipv6 "
[ -n " $ip_addr " ] && printf " ${ DIM } (local: %s) ${ RESET } \n " " $ip_addr " || printf "\n"
elif [ -n " $ip_addr " ] ; then
# Show local IP only
2025-10-20 13:26:08 +01:00
printf " ${ CYAN } %-15s ${ RESET } %s ${ YELLOW } [%s] ${ RESET } \n " "Hostname:" " $( hostname) " " $ip_addr "
else
2025-11-06 21:20:56 +00:00
# No IP detected
2025-10-20 13:26:08 +01:00
printf " ${ CYAN } %-15s ${ RESET } %s\n " "Hostname:" " $( hostname) "
fi
printf " ${ CYAN } %-15s ${ RESET } %s\n " "OS:" " $( grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'"' -f2 || echo 'Unknown' ) "
printf " ${ CYAN } %-15s ${ RESET } %s\n " "Kernel:" " $( uname -r) "
printf " ${ CYAN } %-15s ${ RESET } %s\n " "Uptime:" " $( uptime -p 2>/dev/null || uptime | sed 's/.*up //' | sed 's/,.*//' ) "
printf " ${ CYAN } %-15s ${ RESET } %s\n " "Server time:" " $( date '+%Y-%m-%d %H:%M:%S %Z' ) "
printf " ${ CYAN } %-15s ${ RESET } %s\n " "CPU:" " $cpu_info "
2025-10-22 13:21:00 +01:00
printf " ${ CYAN } %-15s ${ RESET } " "Memory:"
free -m | awk ' /Mem/ {
used = $3 ; total = $2 ; percent = int( ( used/total) *100) ;
if ( used >= 1024) { used_fmt = sprintf( "%.1fGi" , used/1024) ; } else { used_fmt = sprintf( "%dMi" , used) ; }
if ( total >= 1024) { total_fmt = sprintf( "%.1fGi" , total/1024) ; } else { total_fmt = sprintf( "%dMi" , total) ; }
printf "%s / %s (%d%% used)\n" , used_fmt, total_fmt, percent;
} '
2025-10-20 13:26:08 +01:00
printf " ${ CYAN } %-15s ${ RESET } %s\n " "Disk (/):" " $( df -h / | awk 'NR==2 {print $3 " / " $2 " (" $5 " used)"}' ) "
# --- Reboot Status ---
if [ -f /var/run/reboot-required ] ; then
printf " ${ CYAN } %-15s ${ RESET } ${ BOLD_RED } ⚠ REBOOT REQUIRED ${ RESET } \n " "System:"
[ -s /var/run/reboot-required.pkgs ] && \
2025-11-06 21:20:56 +00:00
printf " ${ DIM } Reason: ${ RESET } %s\n " " $( paste -sd ' ' /var/run/reboot-required.pkgs) "
2025-10-20 13:26:08 +01:00
fi
# --- Available Updates (APT) ---
if command -v apt-get & >/dev/null; then
local total security
local upgradable_all upgradable_list security_list
if [ -x /usr/lib/update-notifier/apt-check ] ; then
2025-10-22 13:21:00 +01:00
local apt_check_output
apt_check_output = $( /usr/lib/update-notifier/apt-check 2>/dev/null)
if [ -n " $apt_check_output " ] ; then
total = " ${ apt_check_output %%;* } "
security = " ${ apt_check_output ##*; } "
fi
fi
2025-11-06 21:20:56 +00:00
2025-10-22 13:21:00 +01:00
# Fallback if apt-check didn't provide values
if [ -z " $total " ] && [ -r /var/lib/update-notifier/updates-available ] ; then
total = $( awk '/[0-9]+ (update|package)s? can be (updated|applied|installed)/ {print $1; exit}' /var/lib/update-notifier/updates-available 2>/dev/null)
security = $( awk '/[0-9]+ (update|package)s? .*security/ {print $1; exit}' /var/lib/update-notifier/updates-available 2>/dev/null)
fi
2025-11-06 21:20:56 +00:00
2025-10-22 13:21:00 +01:00
# Final fallback
if [ -z " $total " ] ; then
2025-10-20 13:26:08 +01:00
total = $( apt list --upgradable 2>/dev/null | grep -c upgradable)
security = $( apt list --upgradable 2>/dev/null | grep -ci security)
fi
2025-11-06 21:20:56 +00:00
2025-10-22 13:21:00 +01:00
total = " ${ total :- 0 } "
security = " ${ security :- 0 } "
2025-10-20 13:26:08 +01:00
2025-11-06 21:20:56 +00:00
# Display updates if available
2025-10-20 13:26:08 +01:00
if [ -n " $total " ] && [ " $total " -gt 0 ] 2>/dev/null; then
printf " ${ CYAN } %-15s ${ RESET } " "Updates:"
if [ -n " $security " ] && [ " $security " -gt 0 ] 2>/dev/null; then
printf " ${ YELLOW } %s packages (%s security) ${ RESET } \n " " $total " " $security "
else
printf "%s packages available\n" " $total "
fi
# List upgradable packages (up to 5) and highlight security
mapfile -t upgradable_all < <( apt list --upgradable 2>/dev/null | tail -n +2)
upgradable_list = $( printf "%s\n" " ${ upgradable_all [@] } " | head -n5 | awk -F/ '{print $1}' )
security_list = $( printf "%s\n" " ${ upgradable_all [@] } " | grep -i security | head -n5 | awk -F/ '{print $1}' )
[ -n " $upgradable_list " ] && \
2025-11-06 21:20:56 +00:00
printf " ${ DIM } Upgradable: ${ RESET } %s " " $( echo " $upgradable_list " | paste -sd ', ' ) "
2025-10-20 13:26:08 +01:00
[ " $total " -gt 5 ] && printf " ... (+%s more)\n" $(( total - 5 )) || printf "\n"
[ -n " $security_list " ] && \
2025-11-06 21:20:56 +00:00
printf " ${ YELLOW } Security: ${ RESET } %s " " $( echo " $security_list " | paste -sd ', ' ) "
2025-10-20 13:26:08 +01:00
[ " $security " -gt 5 ] && printf " ... (+%s more)\n" $(( security - 5 )) || printf "\n"
fi
fi
# --- Docker Info ---
if command -v docker & >/dev/null; then
mapfile -t docker_states < <( docker ps -a --format '{{.State}}' 2>/dev/null)
total = ${# docker_states [@] }
if ( ( total > 0 ) ) ; then
running = $( printf "%s\n" " ${ docker_states [@] } " | grep -c '^running$' )
printf " ${ CYAN } %-15s ${ RESET } ${ GREEN } %s running ${ RESET } / %s total containers\n " "Docker:" " $running " " $total "
fi
fi
2025-11-06 21:20:56 +00:00
# --- Tailscale Info (if installed and connected) ---
if command -v tailscale & >/dev/null; then
local ts_ipv4 ts_ipv6 ts_hostname
# Get Tailscale IPs
ts_ipv4 = $( tailscale ip -4 2>/dev/null)
ts_ipv6 = $( tailscale ip -6 2>/dev/null)
# Only show if connected
if [ -n " $ts_ipv4 " ] || [ -n " $ts_ipv6 " ] ; then
# Get hostname from status (FIXED: use head -n1 to get only first line)
ts_hostname = $( tailscale status --self --peers= false 2>/dev/null | head -n1 | awk '{print $2}' )
printf " ${ CYAN } %-15s ${ RESET } " "Tailscale:"
printf " ${ GREEN } Connected ${ RESET } "
[ -n " $ts_ipv4 " ] && printf " - %s" " $ts_ipv4 "
[ -n " $ts_hostname " ] && printf " ${ DIM } (%s) ${ RESET } " " $ts_hostname "
printf "\n"
# Optional: Show IPv6 on second line if available
if [ -n " $ts_ipv6 " ] ; then
printf " ${ DIM } IPv6: %s ${ RESET } \n " " $ts_ipv6 "
fi
fi
fi
2025-10-20 13:26:08 +01:00
printf "\n"
}
# Check for available updates
checkupdates( ) {
if [ -x /usr/lib/update-notifier/apt-check ] ; then
echo "Checking for updates..."
/usr/lib/update-notifier/apt-check --human-readable
elif command -v apt & >/dev/null; then
apt list --upgradable 2>/dev/null
else
echo "No package manager found"
return 1
fi
}
2025-11-06 21:20:56 +00:00
# Disk space alert (warns if any partition > 80%)
diskcheck( ) {
df -h | awk '
NR > 1 {
usage = $5
gsub( /%/, "" , usage)
if ( usage > 80) {
printf "⚠️ %s\n" , $0
found = 1
}
}
END {
if ( !found) print "✓ All disks below 80%"
}
'
}
# Directory bookmarks
export MARKPATH = $HOME /.marks
[ -d " $MARKPATH " ] || mkdir -p " $MARKPATH "
mark( ) { ln -sfn " $( pwd ) " " $MARKPATH / ${ 1 :- $( basename " $PWD " ) } " ; }
jump( ) { cd -P " $MARKPATH / $1 " 2>/dev/null || ls -l " $MARKPATH " ; }
# Service status shortcut (cleaner output)
svc( ) { sudo systemctl status " $1 " --no-pager -l | head -20; }
alias failed = 'systemctl --failed --no-pager'
# Show top 10 processes by CPU
topcpu( ) { ps aux --sort= -%cpu | head -11; }
# Show top 10 processes by memory
topmem( ) { ps aux --sort= -%mem | head -11; }
# Network connections summary
netsum( ) {
echo "=== Active Connections ==="
ss -s
echo -e "\n=== Listening Ports ==="
sudo ss -tulnp | grep LISTEN | awk '{print $5, $7}' | sort -u
}
2025-10-20 13:26:08 +01:00
# --- Aliases ---
# Enable color support for common commands.
if [ -x /usr/bin/dircolors ] ; then
test -r ~/.dircolors && eval " $( dircolors -b ~/.dircolors) " || eval " $( dircolors -b) "
alias ls = 'ls --color=auto'
alias dir = 'dir --color=auto'
alias vdir = 'vdir --color=auto'
alias grep = 'grep --color=auto'
alias egrep = 'grep -E --color=auto'
alias fgrep = 'grep -F --color=auto'
alias diff = 'diff --color=auto'
alias ip = 'ip --color=auto'
fi
# Standard ls aliases with human-readable sizes.
alias ll = 'ls -alFh'
alias la = 'ls -A'
alias l = 'ls -CF'
2025-11-06 21:20:56 +00:00
alias lt = 'ls -alFht' # Sort by modification time, newest first
alias ltr = 'ls -alFhtr' # Sort by modification time, oldest first
alias lS = 'ls -alFhS' # Sort by size, largest first
# Last command with sudo
alias please = 'sudo $(history -p !!)'
2025-10-20 13:26:08 +01:00
# Safety aliases to prompt before overwriting.
alias rm = 'rm -i'
alias cp = 'cp -i'
alias mv = 'mv -i'
alias ln = 'ln -i'
# Convenience & Navigation aliases.
alias ..= 'cd ..'
alias ...= 'cd ../..'
alias ....= 'cd ../../..'
alias .....= 'cd ../../../..'
2025-11-06 21:20:56 +00:00
alias -- -= 'cd -' # Go to previous directory
2025-10-20 13:26:08 +01:00
alias ~= 'cd ~'
alias h = 'history'
alias c = 'clear'
alias cls = 'clear'
alias reload = 'source ~/.bashrc && echo "Bashrc reloaded!"'
# PATH printer as a function (portable, no echo -e)
unalias path 2>/dev/null
path( ) {
printf '%s\n' " ${ PATH // : / $'\n' } "
}
# Enhanced directory listing.
alias lsd = 'ls -d */ 2>/dev/null' # List only directories
alias lsf = 'find . -maxdepth 1 -type f -printf "%f\n"'
# System resource helpers.
alias df = 'df -h'
alias du = 'du -h'
alias free = 'free -h'
# psgrep as a function to accept patterns reliably
# Ensure no alias conflict before defining the function
unalias psgrep 2>/dev/null
psgrep( ) {
if [ $# -eq 0 ] ; then
echo "Usage: psgrep <pattern>" >& 2
return 1
fi
# Build a pattern like '[n]ginx' to avoid matching the grep process itself
local pattern
local term = " $1 "
pattern = " [ ${ term : 0 : 1 } ] ${ term : 1 } "
ps aux | grep -i " $pattern "
}
alias ports = 'ss -tuln'
alias listening = 'ss -tlnp'
alias meminfo = 'free -h -l -t'
alias psmem = 'ps auxf | sort -nr -k 4 | head -10'
alias pscpu = 'ps auxf | sort -nr -k 3 | head -10'
alias top10 = 'ps aux --sort=-%mem | head -n 11'
# Quick network info.
alias myip = 'curl -s ifconfig.me || curl -s icanhazip.com' # Alternatives: api.ipify.org, icanhazip.co
# Show local IP address(es), excluding loopback.
localip( ) {
ip -4 addr | awk '/inet/ {print $2}' | cut -d/ -f1 | grep -v '127.0.0.1'
}
2025-11-06 21:20:56 +00:00
2025-10-20 13:26:08 +01:00
alias netstat = 'ss'
alias ping = 'ping -c 5'
alias fastping = 'ping -c 100 -i 0.2'
# Date and time helpers.
alias now = 'date +"%Y-%m-%d %H:%M:%S"'
alias nowdate = 'date +"%Y-%m-%d"'
alias timestamp = 'date +%s'
# File operations.
alias count = 'find . -type f | wc -l' # Count files in current directory
alias cpv = 'rsync -ah --info=progress2' # Copy with progress
alias wget = 'wget -c' # Resume wget by default
# Git shortcuts (if git is available).
if command -v git & >/dev/null; then
alias gs = 'git status'
alias ga = 'git add'
alias gc = 'git commit'
alias gp = 'git push'
alias gl = 'git log --oneline --graph --decorate'
alias gd = 'git diff'
alias gb = 'git branch'
alias gco = 'git checkout'
fi
# --- Docker Shortcuts and Functions ---
if command -v docker & >/dev/null; then
# Core Docker aliases
alias d = 'docker'
alias dps = 'docker ps'
alias dpsa = 'docker ps -a'
alias dpsq = 'docker ps -q'
alias di = 'docker images'
alias dv = 'docker volume ls'
alias dn = 'docker network ls'
alias dex = 'docker exec -it'
alias dlog = 'docker logs -f'
alias dins = 'docker inspect'
alias drm = 'docker rm'
alias drmi = 'docker rmi'
alias dpull = 'docker pull'
# Docker system management
alias dprune = 'docker system prune -f'
alias dprunea = 'docker system prune -af'
alias ddf = 'docker system df'
alias dvprune = 'docker volume prune -f'
alias diprune = 'docker image prune -af'
# Docker stats
alias dstats = 'docker stats --no-stream'
alias dstatsa = 'docker stats'
2025-11-06 21:20:56 +00:00
dst( ) {
2025-10-20 13:26:08 +01:00
docker stats --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}'
}
# Safe stop all (shows command instead of executing)
2025-11-06 21:20:56 +00:00
alias dstopa = 'echo "To stop all containers, run: docker stop \$(docker ps -q)"'
# Start all stopped containers
alias dstarta = 'docker start $(docker ps -aq)'
2025-10-20 13:26:08 +01:00
# Docker Compose v2 aliases (check if the compose plugin exists)
if docker compose version & >/dev/null; then
alias dc = 'docker compose'
alias dcup = 'docker compose up -d'
alias dcdown = 'docker compose down'
alias dclogs = 'docker compose logs -f'
alias dcps = 'docker compose ps'
alias dcex = 'docker compose exec'
alias dcbuild = 'docker compose build'
alias dcbn = 'docker compose build --no-cache'
alias dcrestart = 'docker compose restart'
alias dcrecreate = 'docker compose up -d --force-recreate'
alias dcpull = 'docker compose pull'
alias dcstop = 'docker compose stop'
alias dcstart = 'docker compose start'
alias dcconfig = 'docker compose config'
alias dcvalidate = 'docker compose config --quiet && echo "✓ docker-compose.yml is valid" || echo "✗ docker-compose.yml has errors"'
fi
# --- Docker Functions ---
# Enter container shell (bash or sh fallback)
dsh( ) {
if [ -z " $1 " ] ; then
echo "Usage: dsh <container-name-or-id>" >& 2
return 1
fi
docker exec -it " $1 " bash 2>/dev/null || docker exec -it " $1 " sh
}
# Docker Compose enter shell (bash or sh fallback)
dcsh( ) {
if [ -z " $1 " ] ; then
echo "Usage: dcsh <service-name>" >& 2
return 1
fi
docker compose exec " $1 " bash 2>/dev/null || docker compose exec " $1 " sh
}
# Follow logs for a specific container with tail
dfollow( ) {
if [ -z " $1 " ] ; then
echo "Usage: dfollow <container-name-or-id> [lines]" >& 2
return 1
fi
local lines = " ${ 2 :- 100 } "
docker logs -f --tail " $lines " " $1 "
}
# Show container IP addresses
dip( ) {
if [ -z " $1 " ] ; then
docker ps -q | xargs -I { } docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' { } 2>/dev/null
else
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' " $1 " 2>/dev/null
fi
}
# Show bind mounts for containers
dbinds( ) {
if [ -z " $1 " ] ; then
printf "\n\033[1;32mContainer Bind Mounts:\033[0m\n"
printf "═══════════════════════════════════════════════════════════════\n"
docker ps --format '{{.Names}}' | while IFS = read -r container; do
printf "\n\033[1;32m%s\033[0m:\n" " $container "
docker inspect " $container " --format '{{range .Mounts}}{{if eq .Type "bind"}} {{.Source}} → {{.Destination}}{{println}}{{end}}{{end}}' 2>/dev/null
done
printf "\n"
else
printf "\nBind mounts for %s:\n" " $1 "
docker inspect " $1 " --format '{{range .Mounts}}{{if eq .Type "bind"}} {{.Source}} → {{.Destination}}{{println}}{{end}}{{end}}' 2>/dev/null
fi
}
# Show disk usage by containers (enable size reporting)
dsize( ) {
printf "\n%-40s %s\n" "Container" "Size"
printf "═══════════════════════════════════════════════════════════════\n"
docker ps -a --size --format '{{.Names}}\t{{.Size}}' | column -t
printf "\n"
}
# Restart a compose service and follow logs
dcreload( ) {
if [ -z " $1 " ] ; then
echo "Usage: dcreload <service-name>" >& 2
return 1
fi
docker compose restart " $1 " && docker compose logs -f " $1 "
}
# Update and restart a single compose service
dcupdate( ) {
if [ -z " $1 " ] ; then
echo "Usage: dcupdate <service-name>" >& 2
return 1
fi
docker compose pull " $1 " && docker compose up -d " $1 " && docker compose logs -f " $1 "
}
# Show Docker Compose services status with detailed info
dcstatus( ) {
printf "\n=== Docker Compose Services ===\n\n"
docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'
printf "\n=== Resource Usage ===\n\n"
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}'
printf "\n"
}
# Watch Docker Compose logs for specific service with grep
dcgrep( ) {
if [ -z " $1 " ] || [ -z " $2 " ] ; then
echo "Usage: dcgrep <service-name> <search-pattern>" >& 2
return 1
fi
docker compose logs -f " $1 " | grep --color= auto -i " $2 "
}
# Show environment variables for a container
denv( ) {
if [ -z " $1 " ] ; then
echo "Usage: denv <container-name-or-id>" >& 2
return 1
fi
docker inspect " $1 " --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | sort
}
# Remove all stopped containers
drmall( ) {
# Using the modern, direct command
docker container prune -f
}
fi
# Systemd shortcuts.
if command -v systemctl & >/dev/null; then
alias sysstart = 'sudo systemctl start'
alias sysstop = 'sudo systemctl stop'
alias sysrestart = 'sudo systemctl restart'
alias sysstatus = 'sudo systemctl status'
alias sysenable = 'sudo systemctl enable'
alias sysdisable = 'sudo systemctl disable'
alias sysreload = 'sudo systemctl daemon-reload'
fi
# Apt aliases for Debian/Ubuntu (only if apt is available).
if command -v apt & >/dev/null; then
alias aptup = 'sudo apt update && sudo apt upgrade'
alias aptin = 'sudo apt install'
alias aptrm = 'sudo apt remove'
alias aptsearch = 'apt search'
alias aptshow = 'apt show'
alias aptclean = 'sudo apt autoremove && sudo apt autoclean'
alias aptlist = 'apt list --installed'
fi
# --- PATH Configuration ---
# Add user's local bin directories to PATH if they exist.
[ -d " $HOME /.local/bin " ] && export PATH = " $HOME /.local/bin: $PATH "
[ -d " $HOME /bin " ] && export PATH = " $HOME /bin: $PATH "
# --- Server-Specific Configuration ---
# Load hostname-specific configurations if they exist.
# This allows per-server customization without modifying the main bashrc.
if [ -f ~/.bashrc." $( hostname -s) " ] ; then
# shellcheck disable=SC1090
source ~/.bashrc." $( hostname -s) "
fi
# --- Bash Completion & Personal Aliases ---
# Enable programmable completion features.
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ] ; then
2025-11-06 21:20:56 +00:00
# shellcheck disable=SC1091
. /usr/share/bash-completion/bash_completion
2025-10-20 13:26:08 +01:00
elif [ -f /etc/bash_completion ] ; then
2025-11-06 21:20:56 +00:00
# shellcheck disable=SC1091
. /etc/bash_completion
2025-10-20 13:26:08 +01:00
fi
fi
# Source personal aliases if the file exists.
if [ -f ~/.bash_aliases ] ; then
# shellcheck disable=SC1090
. ~/.bash_aliases
fi
# Source local machine-specific settings that shouldn't be in version control.
if [ -f ~/.bashrc.local ] ; then
# shellcheck disable=SC1090
. ~/.bashrc.local
fi
# --- Welcome Message for SSH Sessions ---
# Show system info and context on login for SSH sessions.
if [ -n " $SSH_CONNECTION " ] ; then
# Use the existing sysinfo function for a full system overview.
sysinfo
# Display previous login information (skip current session)
last_login = $( last -R " $USER " 2>/dev/null | sed -n '2p' | awk '{$1=""; print}' | xargs)
[ -n " $last_login " ] && printf "Last login: %s\n" " $last_login "
# Show active sessions
printf "Active sessions: %s\n" " $( who | wc -l) "
printf -- "-----------------------------------------------------\n\n"
fi
# --- Help System ---
# Display all custom functions and aliases with descriptions
bashhelp( ) {
local category = " ${ 1 :- all } "
case " $category " in
all| "" )
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
╔════════════════════════════════════════════════════════╗
2025-11-06 21:20:56 +00:00
║ .bashrc - Quick Reference ║
2025-10-20 13:26:08 +01:00
╚════════════════════════════════════════════════════════╝
Usage: bashhelp [ category]
Categories: navigation, files, system, docker, git, network
═══════════════════════════════════════════════════════════════════
📁 NAVIGATION & DIRECTORY
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
.. Go up one directory
... Go up two directories
.... Go up three directories
..... Go up four directories
- Go to previous directory
~ Go to home directory
mkcd <dir> Create directory and cd into it
up <n> Go up N directories ( e.g., up 3)
path Display PATH variable ( one per line)
mark <name> Bookmark current directory
jump <name> Jump to a bookmarked directory
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
📄 FILE OPERATIONS
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
ll List all files with details ( human-readable)
la List all files including hidden
l List files in column format
lt List by time, newest first
ltr List by time, oldest first
lS List by size, largest first
lsd List only directories
lsf List only files
ff <name> Find files by name ( case -insensitive)
fd <name> Find directories by name ( case -insensitive)
ftext <text> Search for text in files recursively
extract <file> Extract any archive ( tar, zip, 7z, etc.)
targz <dir> Create tar.gz of directory
backup <file> Create timestamped backup of file
sizeof <path> Get size of file or directory
duh [ path] Disk usage sorted by size
count Count files in current directory
cpv <src> <dst> Copy with progress bar ( rsync)
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
💻 SYSTEM & MONITORING
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
sysinfo Display comprehensive system information
checkupdates Check for available system updates
diskcheck Check for disk partitions over 80%
psgrep <pat> Search for process by name
topcpu Show top 10 processes by CPU
topmem Show top 10 processes by Memory
pscpu Show top 10 processes by CPU ( tree view)
psmem Show top 10 processes by Memory ( tree view)
ports Show all listening ports ( TCP/UDP)
listening Show listening ports with process info
meminfo Display detailed memory information
h Show command history
hgrep <pat> Search command history
histop Show most used commands
c, cls Clear the screen
reload Reload bashrc configuration
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
🐳 DOCKER & DOCKER COMPOSE
═══════════════════════════════════════════════════════════════════
Docker Commands:
2025-11-06 21:20:56 +00:00
d docker ( shortcut)
dps List running containers
dpsa List all containers
di List images
dv List volumes
dn List networks
dex <id> Execute interactive shell in container
dlog <id> Follow container logs
dsh <id> Enter container shell ( bash/sh)
dip [ id] Show container IP addresses
dsize Show disk usage by containers
dbinds [ id] Show bind mounts for containers
denv <id> Show environment variables
dfollow <id> Follow logs with tail ( default 100 lines)
dstats Container stats snapshot
dstatsa Container stats live
dst Container stats formatted table
dprune Prune system ( remove unused data)
dprunea Prune all ( including images)
dvprune Prune unused volumes
diprune Prune unused images
drmall Remove all stopped containers
2025-10-20 13:26:08 +01:00
Docker Compose:
2025-11-06 21:20:56 +00:00
dc docker compose ( shortcut)
dcup Start services in background
dcdown Stop and remove services
dclogs Follow compose logs
dcps List compose services
dcex <srv> Execute command in service
dcsh <srv> Enter service shell ( bash/sh)
dcbuild Build services
dcbn Build with no cache
dcrestart Restart services
dcrecreate Recreate services
dcpull Pull service images
dcstop Stop services
dcstart Start services
dcstatus Show service status & resource usage
dcreload <srv> Restart service and follow logs
dcupdate <srv> Pull, restart service, follow logs
2025-10-20 13:26:08 +01:00
dcgrep <srv> <pattern> Filter service logs
2025-11-06 21:20:56 +00:00
dcconfig Show resolved compose configuration
dcvalidate Validate compose file syntax
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
🔀 GIT SHORTCUTS
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
gs git status
ga git add
gc git commit
gp git push
gl git log ( graph view)
gd git diff
gb git branch
gco git checkout
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
🌐 NETWORK
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
myip Show external IP address
localip Show local IP address( es)
netsum Network connections summary
kssh SSH wrapper for kitty terminal
ping Ping with 5 packets ( default)
fastping Fast ping ( 100 packets, 0.2s interval)
netstat Show network connections ( ss)
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
⚙️ SYSTEM ADMINISTRATION
═══════════════════════════════════════════════════════════════════
Systemd:
2025-11-06 21:20:56 +00:00
svc <srv> Show service status ( brief)
failed List failed systemd services
sysstart <srv> Start service
sysstop <srv> Stop service
sysrestart <srv> Restart service
sysstatus <srv> Show service status
sysenable <srv> Enable service
sysdisable <srv> Disable service
sysreload Reload systemd daemon
2025-10-20 13:26:08 +01:00
APT ( Debian/Ubuntu) :
2025-11-06 21:20:56 +00:00
aptup Update and upgrade packages
aptin <pkg> Install package
aptrm <pkg> Remove package
aptsearch <term> Search for packages
aptshow <pkg> Show package information
aptclean Remove unused packages
aptlist List installed packages
Sudo:
please Run last command with sudo
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
🕒 DATE & TIME
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
now Current date and time ( YYYY-MM-DD HH:MM:SS)
nowdate Current date ( YYYY-MM-DD)
timestamp Unix timestamp
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
ℹ ️ HELP & INFORMATION
═══════════════════════════════════════════════════════════════════
2025-11-06 21:20:56 +00:00
bashhelp Show this help ( all categories)
bh Alias for bashhelp
commands List all custom functions and aliases
2025-10-20 13:26:08 +01:00
bashhelp navigation Show navigation commands only
2025-11-06 21:20:56 +00:00
bashhelp files Show file operation commands
bashhelp system Show system monitoring commands
bashhelp docker Show docker commands only
bashhelp git Show git shortcuts
bashhelp network Show network commands
2025-10-20 13:26:08 +01:00
═══════════════════════════════════════════════════════════════════
💡 TIP: Most commands support --help or -h for more information
2025-11-06 21:20:56 +00:00
The prompt shows: ✗ for failed commands, ( git branch) when in repo
2025-10-20 13:26:08 +01:00
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
navigation)
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
═══ NAVIGATION & DIRECTORY COMMANDS ═══
2025-11-06 21:20:56 +00:00
.. Go up one directory
... Go up two directories
.... Go up three directories
..... Go up four directories
- Go to previous directory
~ Go to home directory
2025-10-20 13:26:08 +01:00
2025-11-06 21:20:56 +00:00
mkcd <dir> Create directory and cd into it
up <n> Go up N directories
path Display PATH variable
mark <name> Bookmark current directory
jump <name> Jump to a bookmarked directory
2025-10-20 13:26:08 +01:00
Examples:
2025-11-06 21:20:56 +00:00
mkcd ~/projects/newapp # Create and enter directory
up 3 # Go up 3 levels
mark proj1 # Bookmark current dir as 'proj1'
jump proj1 # Jump back to 'proj1'
2025-10-20 13:26:08 +01:00
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
files)
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
═══ FILE OPERATION COMMANDS ═══
Listing:
ll, la, l, lt, ltr, lS, lsd, lsf
Finding:
2025-11-06 21:20:56 +00:00
ff <name> Find files by name
fd <name> Find directories by name
ftext <text> Search text in files
2025-10-20 13:26:08 +01:00
Archives:
2025-11-06 21:20:56 +00:00
extract <file> Extract any archive type
targz <dir> Create tar.gz archive
backup <file> Create timestamped backup
2025-10-20 13:26:08 +01:00
Size Info:
2025-11-06 21:20:56 +00:00
sizeof <path> Get size of file/directory
duh [ path] Disk usage sorted by size
count Count files in directory
cpv Copy with progress ( rsync)
2025-10-20 13:26:08 +01:00
Examples:
2025-11-06 21:20:56 +00:00
ff README # Find files named *README*
2025-10-20 13:26:08 +01:00
extract data.tar.gz
backup ~/.bashrc
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
system)
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
═══ SYSTEM MONITORING COMMANDS ═══
Overview:
2025-11-06 21:20:56 +00:00
sysinfo Comprehensive system info
checkupdates Check for package updates
diskcheck Check for disks > 80%
2025-10-20 13:26:08 +01:00
Processes:
2025-11-06 21:20:56 +00:00
psgrep <pat> Search processes
topcpu Top 10 by CPU
topmem Top 10 by Memory
pscpu Top 10 by CPU ( tree view)
psmem Top 10 by Memory ( tree view)
2025-10-20 13:26:08 +01:00
Network:
2025-11-06 21:20:56 +00:00
ports Listening ports
listening Ports with process info
2025-10-20 13:26:08 +01:00
Memory:
2025-11-06 21:20:56 +00:00
meminfo Detailed memory info
free Free memory ( human-readable)
2025-10-20 13:26:08 +01:00
2025-11-06 21:20:56 +00:00
Shell:
h Show history
hgrep <pat> Search history
histop Most used commands
c, cls Clear screen
reload Reload bashrc
2025-10-20 13:26:08 +01:00
Examples:
psgrep nginx
psmem | grep docker
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
docker)
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
═══ DOCKER COMMANDS ═══
Basic:
dps, dpsa, di, dv, dn, dex, dlog
Management:
2025-11-06 21:20:56 +00:00
dsh <id> Enter container shell
dip [ id] Show IP addresses
dsize Show disk usage
dbinds [ id] Show bind mounts
denv <id> Show environment variables
dfollow <id> Follow logs
2025-10-20 13:26:08 +01:00
Stats & Cleanup:
2025-11-06 21:20:56 +00:00
dstats, dstatsa, dst
2025-10-20 13:26:08 +01:00
dprune, dprunea, dvprune, diprune
2025-11-06 21:20:56 +00:00
drmall Remove stopped containers
2025-10-20 13:26:08 +01:00
Docker Compose:
dcup, dcdown, dclogs, dcps, dcex, dcsh
dcbuild, dcrestart, dcrecreate
2025-11-06 21:20:56 +00:00
dcstatus Status & resource usage
dcreload <srv> Restart & follow logs
dcupdate <srv> Pull & update service
dcgrep <s> <p> Filter logs
dcvalidate Validate compose file
2025-10-20 13:26:08 +01:00
Examples:
dsh mycontainer
dcsh web bash
dcupdate nginx
dcgrep app "error"
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
git)
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
═══ GIT SHORTCUTS ═══
2025-11-06 21:20:56 +00:00
gs git status
ga git add
gc git commit
gp git push
gl git log ( graph)
gd git diff
gb git branch
gco git checkout
2025-10-20 13:26:08 +01:00
Examples:
2025-11-06 21:20:56 +00:00
gs # Check status
ga . # Add all changes
gc -m "Update docs" # Commit
gp # Push to remote
2025-10-20 13:26:08 +01:00
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
network)
2025-10-20 15:26:57 +01:00
cat << 'HELPTEXT'
2025-10-20 13:26:08 +01:00
═══ NETWORK COMMANDS ═══
2025-11-06 21:20:56 +00:00
myip Show external IP
localip Show local IP( s)
netsum Network connection summary
kssh SSH wrapper for kitty
ports Show listening ports
listening Ports with process info
ping Ping ( 5 packets)
fastping Fast ping ( 100 packets)
netstat Network connections ( ss)
2025-10-20 13:26:08 +01:00
Examples:
2025-11-06 21:20:56 +00:00
myip # Get public IP
2025-10-20 13:26:08 +01:00
listening | grep 80
ping google.com
2025-10-20 15:26:57 +01:00
HELPTEXT
2025-10-20 13:26:08 +01:00
; ;
*)
echo " Unknown category: $category "
echo "Available categories: navigation, files, system, docker, git, network"
echo "Use 'bashhelp' or 'bashhelp all' for complete reference"
return 1
; ;
esac
}
# Preserve Bash's builtin `help` while integrating bashhelp
2025-11-06 21:20:56 +00:00
# This wrapper routes custom help to bashhelp, bash builtins to builtin help
2025-10-20 13:26:08 +01:00
help( ) {
case " ${ 1 :- } " in
"" | all| navigation| files| system| docker| git| network)
bashhelp " $@ "
; ;
*)
command help " $@ " 2>/dev/null || builtin help " $@ "
; ;
esac
}
# Shorter alias for bashhelp (not for help - that's a function now)
alias bh = 'bashhelp'
# Quick command list (compact)
alias commands = 'compgen -A function -A alias | grep -v "^_" | sort | column'
# --- Performance Note ---
# This configuration is optimized for performance using built-in bash operations
# and minimizing external command calls. If startup feels slow, check:
# - ~/.bash_aliases and ~/.bashrc.local for expensive operations
# - Consider moving rarely-used functions to separate files
# - Use 'time bash -i -c exit' to measure startup time
EOF
2025-10-20 16:46:47 +01:00
then
2025-10-20 14:45:38 +01:00
print_error " Failed to write .bashrc content to temporary file $temp_source_bashrc . "
log " Critical error: Failed to write bashrc content to $temp_source_bashrc . "
rm -f " $temp_source_bashrc " 2>/dev/null
return 0
2025-10-20 13:26:08 +01:00
fi
2025-10-20 14:45:38 +01:00
log " Successfully created temporary .bashrc source at $temp_source_bashrc "
2025-10-20 16:46:47 +01:00
if [ [ -f " $BASHRC_PATH " ] ] && ! grep -q "generated by /usr/sbin/adduser" " $BASHRC_PATH " 2>/dev/null; then
2025-10-20 17:06:21 +01:00
local BASHRC_BACKUP
BASHRC_BACKUP = " $BASHRC_PATH .backup_ $( date +%Y%m%d_%H%M%S) "
2025-10-20 16:46:47 +01:00
print_info " Backing up existing non-default .bashrc to $BASHRC_BACKUP "
cp " $BASHRC_PATH " " $BASHRC_BACKUP "
log " Backed up existing .bashrc to $BASHRC_BACKUP "
fi
2025-10-20 14:45:38 +01:00
local temp_fallback_path = " /tmp/custom_bashrc_for_ ${ USERNAME } .txt "
2025-10-20 16:46:47 +01:00
if ! tee " $BASHRC_PATH " < " $temp_source_bashrc " > /dev/null
then
2025-10-20 13:57:48 +01:00
print_error " Failed to automatically write custom .bashrc to $BASHRC_PATH . "
log " Error writing custom .bashrc for $USERNAME to $BASHRC_PATH (likely permissions issue). "
2025-10-20 14:45:38 +01:00
if cp " $temp_source_bashrc " " $temp_fallback_path " ; then
chmod 644 " $temp_fallback_path "
2025-10-20 13:57:48 +01:00
print_warning "ACTION REQUIRED: The custom .bashrc content has been saved to:"
2025-10-20 14:45:38 +01:00
print_warning " ${ temp_fallback_path } "
2025-10-20 13:57:48 +01:00
print_info "After setup, please manually copy it:"
2025-10-20 14:45:38 +01:00
print_info " sudo cp ${ temp_fallback_path } ${ BASHRC_PATH } "
2025-10-20 13:57:48 +01:00
print_info " sudo chown ${ USERNAME } : ${ USERNAME } ${ BASHRC_PATH } "
print_info " sudo chmod 644 ${ BASHRC_PATH } "
2025-10-20 14:45:38 +01:00
log " Saved custom .bashrc content to $temp_fallback_path for manual installation. "
2025-10-20 16:46:47 +01:00
keep_temp_source_on_error = true
2025-10-20 13:57:48 +01:00
else
2025-10-20 14:45:38 +01:00
print_error " Also failed to save custom .bashrc content to $temp_fallback_path . "
log " Critical error: Failed both writing to $BASHRC_PATH and copying $temp_source_bashrc to $temp_fallback_path . "
2025-10-20 13:57:48 +01:00
fi
else
if ! chown " $USERNAME : $USERNAME " " $BASHRC_PATH " || ! chmod 644 " $BASHRC_PATH " ; then
print_warning " Failed to set correct ownership/permissions on $BASHRC_PATH . "
log " Failed to chown/chmod $BASHRC_PATH "
print_warning "ACTION REQUIRED: Please manually set ownership/permissions:"
print_info " sudo chown ${ USERNAME } : ${ USERNAME } ${ BASHRC_PATH } "
print_info " sudo chmod 644 ${ BASHRC_PATH } "
2025-10-20 16:46:47 +01:00
print_info " (Source content is in ${ temp_source_bashrc } ) "
keep_temp_source_on_error = true
2025-10-20 13:57:48 +01:00
else
print_success " Custom .bashrc created for ' $USERNAME '. "
log " Custom .bashrc configuration completed for $USERNAME . "
2025-10-20 14:45:38 +01:00
rm -f " $temp_fallback_path " 2>/dev/null
2025-10-20 13:57:48 +01:00
fi
2025-10-20 13:26:08 +01:00
fi
2025-10-20 16:46:47 +01:00
if [ [ " $keep_temp_source_on_error " = = false ] ] ; then
rm -f " $temp_source_bashrc " 2>/dev/null
fi
trap - INT TERM
2025-10-20 14:45:38 +01:00
2025-10-20 13:57:48 +01:00
return 0
2025-10-20 13:26:08 +01:00
}
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_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) ?$ ] ]
}
2025-11-17 10:56:49 +00:00
validate_ip_or_cidr( ) {
local input = " $1 "
# IPv4 address (simple check)
if [ [ " $input " = ~ ^( [ 0-9] { 1,3} \. ) { 3} [ 0-9] { 1,3} $ ] ] ; then
2025-11-17 11:12:06 +00:00
local -a octets
IFS = '.' read -ra octets <<< " $input "
2025-11-17 10:56:49 +00:00
for octet in " ${ octets [@] } " ; do
if [ [ " $octet " -gt 255 ] ] ; then
return 1
fi
done
return 0
fi
# IPv4 CIDR (e.g., 10.0.0.0/8)
if [ [ " $input " = ~ ^( [ 0-9] { 1,3} \. ) { 3} [ 0-9] { 1,3} /[ 0-9] { 1,2} $ ] ] ; then
local ip = " ${ input %/* } "
local cidr = " ${ input ##*/ } "
2025-11-17 11:12:06 +00:00
local -a octets
IFS = '.' read -ra octets <<< " $ip "
2025-11-17 10:56:49 +00:00
for octet in " ${ octets [@] } " ; do
if [ [ " $octet " -gt 255 ] ] ; then
return 1
fi
done
if [ [ " $cidr " -ge 0 && " $cidr " -le 32 ] ] ; then
return 0
fi
return 1
fi
# IPv6 address (basic check)
2025-11-17 11:42:32 +00:00
if [ [ " $input " = ~ ^[ 0-9a-fA-F:] +$ && " $input " = = *":" * && " $input " != *"/" * ] ] ; then
2025-11-17 10:56:49 +00:00
return 0
fi
2025-11-17 11:42:32 +00:00
# IPv6 CIDR (permissive check, allows compressed ::)
if [ [ " $input " = ~ ^[ 0-9a-fA-F:] +/[ 0-9] { 1,3} $ && " $input " = = *":" * ] ] ; then
2025-11-17 10:56:49 +00:00
local cidr = " ${ input ##*/ } "
if [ [ " $cidr " -ge 0 && " $cidr " -le 128 ] ] ; then
return 0
fi
return 1
fi
return 1
}
2025-11-27 16:25:48 +00:00
# Convert size (e.g., 2G) to MB for validation/dd
convert_to_mb( ) {
local input = " ${ 1 :- } "
local size = " ${ input ^^ } "
size = " ${ size // / } "
local num = " ${ size %[MG] } "
local unit = " ${ size : -1 } "
if ! [ [ " $num " = ~ ^[ 0-9] +$ ] ] ; then
print_error " Invalid swap size format: ' $input '. Expected format: 2G, 512M. " >& 2
return 1
2025-06-26 16:22:23 +01:00
fi
2025-11-27 16:25:48 +00:00
case " $unit " in
G) echo " $(( num * 1024 )) " ; ;
M) echo " $num " ; ;
*)
print_error " Unknown or missing unit in swap size: ' $input '. Use 'M' or 'G'. " >& 2
return 1
; ;
esac
2025-06-26 16:22:23 +01:00
}
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
2025-11-25 00:13:34 +00:00
# shellcheck source=/dev/null
2025-06-26 16:22:23 +01:00
source /etc/os-release
2025-11-25 00:13:34 +00:00
ID = ${ ID :- unknown } # 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
2025-11-24 17:39:56 +00:00
if curl -s --head https://deb.debian.org >/dev/null || \
curl -s --head https://archive.ubuntu.com >/dev/null || \
wget -q --spider https://deb.debian.org || \
wget -q --spider https://archive.ubuntu.com; then
2025-06-26 16:22:23 +01:00
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-11-24 17:39:56 +00:00
# --- Input Collection ---
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-11-24 17:39:56 +00:00
[ [ -z " $PRETTY_NAME " ] ] && PRETTY_NAME = " $SERVER_NAME "
# --- SSH Port Detection ---
2025-10-19 19:01:46 +01:00
PREVIOUS_SSH_PORT = $( ss -tlpn | grep sshd | grep -oP ':\K\d+' | head -n 1)
local PROMPT_DEFAULT_PORT = ${ PREVIOUS_SSH_PORT :- 2222 }
2025-06-26 21:49:51 +01:00
while true; do
2025-10-17 20:11:42 +01:00
read -rp " $( printf '%s' " ${ CYAN } Enter custom SSH port (1024-65535) [ $PROMPT_DEFAULT_PORT ]: ${ NC } " ) " SSH_PORT
SSH_PORT = ${ SSH_PORT :- $PROMPT_DEFAULT_PORT }
2025-10-19 19:01:46 +01:00
if validate_port " $SSH_PORT " || [ [ -n " $PREVIOUS_SSH_PORT " && " $SSH_PORT " = = " $PREVIOUS_SSH_PORT " ] ] ; then
2025-10-17 20:45:43 +01:00
break; else print_error "Invalid port. Choose a port between 1024-65535." ; fi
2025-06-26 21:49:51 +01:00
done
2025-11-24 17:39:56 +00:00
# --- IP Detection ---
print_info "Detecting network configuration..."
2025-11-25 16:22:21 +00:00
# 1. Get the Local LAN IP (Explicit Check)
# This prevents crashing on IPv6-only servers
if ip -4 route get 8.8.8.8 >/dev/null 2>& 1; then
LOCAL_IP_V4 = $( ip -4 route get 8.8.8.8 | head -1 | awk '{print $7}' )
else
LOCAL_IP_V4 = ""
fi
# 2. Get Public IPs with timeouts
2025-11-24 15:37:19 +00:00
SERVER_IP_V4 = $( curl -4 -s --connect-timeout 4 --max-time 5 https://ifconfig.me 2>/dev/null || \
curl -4 -s --connect-timeout 4 --max-time 5 https://ip.me 2>/dev/null || \
curl -4 -s --connect-timeout 4 --max-time 5 https://icanhazip.com 2>/dev/null || \
2025-11-24 17:39:56 +00:00
echo "Unknown" )
2025-11-24 15:37:19 +00:00
SERVER_IP_V6 = $( curl -6 -s --connect-timeout 4 --max-time 5 https://ifconfig.me 2>/dev/null || \
curl -6 -s --connect-timeout 4 --max-time 5 https://ip.me 2>/dev/null || \
curl -6 -s --connect-timeout 4 --max-time 5 https://icanhazip.com 2>/dev/null || \
2025-11-24 17:39:56 +00:00
echo "Not available" )
2025-11-25 16:22:21 +00:00
2025-11-24 17:39:56 +00:00
# --- Display Summary ---
2025-10-16 12:29:34 +01:00
printf '\n%s\n' " ${ YELLOW } Configuration Summary: ${ NC } "
2025-11-24 17:39:56 +00:00
printf " %-22s %s\n" "Username:" " $USERNAME "
printf " %-22s %s\n" "Hostname:" " $SERVER_NAME "
2025-10-19 19:01:46 +01:00
if [ [ -n " $PREVIOUS_SSH_PORT " && " $SSH_PORT " != " $PREVIOUS_SSH_PORT " ] ] ; then
2025-11-24 17:39:56 +00:00
printf " %-22s %s (change from current: %s)\n" "SSH Port:" " $SSH_PORT " " $PREVIOUS_SSH_PORT "
2025-10-17 20:11:42 +01:00
else
2025-11-24 17:39:56 +00:00
printf " %-22s %s\n" "SSH Port:" " $SSH_PORT "
2025-10-17 20:11:42 +01:00
fi
2025-11-24 17:39:56 +00:00
# --- IP Display Logic ---
if [ [ " $SERVER_IP_V4 " != "Unknown" ] ] ; then
if [ [ " $SERVER_IP_V4 " = = " $LOCAL_IP_V4 " ] ] ; then
2025-11-25 16:22:21 +00:00
# 1: Direct Public IP
2025-11-24 17:39:56 +00:00
printf " %-22s %s (Direct)\n" "Server IPv4:" " $SERVER_IP_V4 "
else
2025-11-25 16:22:21 +00:00
# 2: NAT
2025-11-24 17:39:56 +00:00
printf " %-22s %s (Internet)\n" "Public IPv4:" " $SERVER_IP_V4 "
if [ [ -n " $LOCAL_IP_V4 " ] ] ; then
printf " %-22s %s (Internal)\n" "Local IPv4:" " $LOCAL_IP_V4 "
fi
fi
else
2025-11-25 16:22:21 +00:00
# Fallback if public check failed
2025-11-24 17:39:56 +00:00
if [ [ -n " $LOCAL_IP_V4 " ] ] ; then
printf " %-22s %s (Local)\n" "Server IPv4:" " $LOCAL_IP_V4 "
fi
2025-08-26 16:14:58 +01:00
fi
2025-11-24 17:39:56 +00:00
if [ [ " $SERVER_IP_V6 " != "Not available" ] ] ; then
printf " %-22s %s\n" "Public IPv6:" " $SERVER_IP_V6 "
2025-08-26 11:40:41 +01:00
fi
2025-10-16 20:07:11 +01:00
if ! confirm $'\nContinue with this configuration?' "y" ; then print_info "Exiting." ; exit 0; fi
2025-11-24 17:39:56 +00:00
log " Configuration collected: USER= $USERNAME , HOST= $SERVER_NAME , PORT= $SSH_PORT , IPV4= $SERVER_IP_V4 , IPV6= $SERVER_IP_V6 , LOCAL= $LOCAL_IP_V4 "
2025-06-26 16:22:23 +01:00
}
install_packages( ) {
print_section "Package Installation"
print_info "Updating package lists and upgrading system..."
2025-11-27 22:18:17 +00:00
print_info "This may take a moment. Please wait..."
2025-06-26 16:22:23 +01:00
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-11-17 09:53:10 +00:00
apt-listchanges ca-certificates gnupg logrotate \
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 '... "
2025-11-25 00:27:51 +00:00
# Check if group exists but user doesn't (common with 'admin' on Ubuntu)
local -a ADDUSER_OPTS = ( "--disabled-password" "--gecos" "" )
if getent group " $USERNAME " >/dev/null 2>& 1; then
print_warning " Group ' $USERNAME ' already exists. Attaching new user to this existing group. "
ADDUSER_OPTS += ( "--ingroup" " $USERNAME " )
fi
if ! adduser " ${ ADDUSER_OPTS [@] } " " $USERNAME " ; then
2025-06-26 16:22:23 +01:00
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-10-16 18:29:39 +01:00
printf '\n'
2025-10-16 18:09:47 +01:00
read -rsp " $( printf '%s' " ${ CYAN } Retype new password: ${ NC } " ) " PASS2
2025-10-16 18:29:39 +01:00
printf '\n'
2025-06-26 16:22:23 +01:00
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-10-16 19:54:58 +01:00
print_error "Failed to set password. Possible causes:"
print_info " • permissions issue or password policy restrictions."
print_info " • VPS provider password requirements (min. 8-12 chars, complexity rules)"
printf '\n'
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-10-16 21:55:17 +01:00
rm -f " $TEMP_KEY_FILE " 2>/dev/null
2025-07-02 16:35:44 +01:00
print_info "Temporary key file deleted."
LOCAL_KEY_ADDED = true
2025-10-16 21:55:17 +01:00
trap - EXIT
2025-07-02 16:35:44 +01:00
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
2025-10-20 13:26:08 +01:00
# Add custom .bashrc
configure_custom_bashrc " $USER_HOME " " $USERNAME "
2025-06-26 16:22:23 +01:00
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-10-16 22:13:17 +01:00
log "Backing up script itself for audit trail"
cp " ${ SCRIPT_DIR } / $( basename " $0 " ) " " $BACKUP_DIR /du_setup_v ${ CURRENT_VERSION } .sh "
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..."
2025-11-25 21:34:09 +00:00
# System Hostname
2025-06-26 16:22:23 +01:00
if [ [ $( hostnamectl --static) != " $SERVER_NAME " ] ] ; then
hostnamectl set-hostname " $SERVER_NAME "
hostnamectl set-hostname " $PRETTY_NAME " --pretty
2025-11-25 20:05:25 +00:00
print_success " Hostname updated to: $SERVER_NAME "
2025-06-26 16:22:23 +01:00
else
2025-11-25 20:05:25 +00:00
print_info " Hostname is already set to $SERVER_NAME . "
2025-06-26 16:22:23 +01:00
fi
2025-11-25 20:55:10 +00:00
if [ [ -d /etc/cloud/cloud.cfg.d ] ] ; then
print_info "Disabling cloud-init host management via override file..."
echo "manage_etc_hosts: false" > /etc/cloud/cloud.cfg.d/99-du-setup-hosts.cfg
echo "preserve_hostname: true" >> /etc/cloud/cloud.cfg.d/99-du-setup-hosts.cfg
2025-11-25 21:34:09 +00:00
log "Created /etc/cloud/cloud.cfg.d/99-du-setup-hosts.cfg"
2025-11-25 20:55:10 +00:00
elif [ [ -f /etc/cloud/cloud.cfg ] ] ; then
2025-11-25 20:05:25 +00:00
if grep -q "manage_etc_hosts: true" /etc/cloud/cloud.cfg; then
2025-11-25 20:55:10 +00:00
print_info "Disabling cloud-init 'manage_etc_hosts' in main config..."
2025-11-25 20:05:25 +00:00
sed -i 's/manage_etc_hosts: true/manage_etc_hosts: false/g' /etc/cloud/cloud.cfg
log "Disabled manage_etc_hosts in /etc/cloud/cloud.cfg"
fi
fi
2025-11-25 21:34:09 +00:00
# Stop cloud-init from overwriting /etc/hosts
local TEMPLATE_FILE = ""
if [ [ -n " $ID " && -f " /etc/cloud/templates/hosts. ${ ID } .tmpl " ] ] ; then
TEMPLATE_FILE = " /etc/cloud/templates/hosts. ${ ID } .tmpl "
elif [ [ -f "/etc/cloud/templates/hosts.tmpl" ] ] ; then
TEMPLATE_FILE = "/etc/cloud/templates/hosts.tmpl"
fi
if [ [ -n " $TEMPLATE_FILE " ] ] ; then
print_info " Patching cloud-init hosts template ( $TEMPLATE_FILE ) to enforce persistence... "
cp " $TEMPLATE_FILE " " $BACKUP_DIR / $( basename " $TEMPLATE_FILE " ) .backup "
sed -i " s/^127.0.1.1.*/127.0.1.1\t $SERVER_NAME /g " " $TEMPLATE_FILE "
log " Hardcoded hostname into $TEMPLATE_FILE "
else
if [ [ -d /etc/cloud/templates ] ] ; then
print_warning "Could not locate a standard hosts template in /etc/cloud/templates."
log "Warning: Cloud-init template patching skipped (no matching template found)."
fi
fi
2025-11-25 17:33:43 +00:00
if grep -q "^127.0.1.1" /etc/hosts; then
2025-11-25 20:05:25 +00:00
if ! grep -qE " ^127.0.1.1[[:space:]]+ $SERVER_NAME " /etc/hosts; then
2025-11-25 17:33:43 +00:00
sed -i " s/^127.0.1.1.*/127.0.1.1\t $SERVER_NAME / " /etc/hosts
2025-11-25 20:05:25 +00:00
print_success " Fixed /etc/hosts to map 127.0.1.1 to $SERVER_NAME . "
2025-11-25 17:33:43 +00:00
else
2025-11-25 20:05:25 +00:00
print_info "/etc/hosts is already correct."
2025-11-25 17:33:43 +00:00
fi
else
echo " 127.0.1.1 $SERVER_NAME " >> /etc/hosts
2025-11-25 20:05:25 +00:00
print_success "Added missing 127.0.1.1 entry to /etc/hosts."
2025-11-25 17:33:43 +00:00
fi
2025-06-26 16:22:23 +01:00
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 ... "
2025-10-19 19:01:46 +01:00
print_info "Rolling back firewall rules..."
ufw delete allow " $SSH_PORT " /tcp 2>/dev/null || true
if [ [ -n " $PREVIOUS_SSH_PORT " ] ] ; then
ufw allow " $PREVIOUS_SSH_PORT " /tcp comment 'SSH Rollback' 2>/dev/null || true
print_info " Firewall rolled back to allow port $PREVIOUS_SSH_PORT . "
else
print_warning "Could not determine previous SSH port for firewall rollback."
fi
2025-11-25 00:13:34 +00:00
if ! rollback_ssh_changes; then
2025-06-30 21:10:35 +01:00
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-10-19 19:19:59 +01:00
local CURRENT_SSH_PORT USER_HOME SSH_DIR SSH_KEY AUTH_KEYS
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-10-19 19:19:59 +01:00
# Check globally detected port, falling back to 22 if detection failed
2025-10-17 20:11:42 +01:00
if [ [ -z " $PREVIOUS_SSH_PORT " ] ] ; then
print_warning "Could not detect an active SSH port. Assuming port 22 for the initial test."
2025-10-19 19:19:59 +01:00
log "Could not detect active SSH port, fell back to 22."
2025-10-17 20:11:42 +01:00
PREVIOUS_SSH_PORT = "22"
fi
2025-06-30 21:10:35 +01:00
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-11-24 17:39:56 +00:00
printf '%s\n' " ${ CYAN } Test SSH access from a SEPARATE terminal now. ${ NC } "
2025-11-24 18:02:58 +00:00
# --- Connection Display Function ---
show_connection_options( ) {
local port = " $1 "
local public_ip = " $2 "
2025-11-24 18:22:39 +00:00
2025-11-24 18:02:58 +00:00
local TS_IP = ""
if command -v tailscale >/dev/null 2>& 1 && tailscale ip >/dev/null 2>& 1; then
TS_IP = $( tailscale ip -4 2>/dev/null)
fi
2025-11-24 17:39:56 +00:00
2025-11-24 18:02:58 +00:00
printf "\n"
2025-11-24 17:39:56 +00:00
2025-11-24 18:02:58 +00:00
# 1. Public IP (Internet)
# Only show if valid and not "Unknown"
if [ [ -n " $public_ip " && " $public_ip " != "Unknown" ] ] ; then
printf " %-20s ${ CYAN } ssh -p %s %s@%s ${ NC } \n " "Public (Internet):" " $port " " $USERNAME " " $public_ip "
fi
2025-11-24 17:39:56 +00:00
2025-11-24 18:02:58 +00:00
# 2. Internal/LAN IPs
# scan all interfaces. exclude the Public IP (already shown) and Loopback.
local found_internal = false
while read -r ip_addr; do
# Remove subnet mask if present
local clean_ip = " ${ ip_addr %/* } "
# Skip if empty, loopback, or matches the Public IP we just displayed
if [ [ -n " $clean_ip " && " $clean_ip " != "127.0.0.1" && " $clean_ip " != " $public_ip " ] ] ; then
printf " %-20s ${ CYAN } ssh -p %s %s@%s ${ NC } \n " "Internal/Private:" " $port " " $USERNAME " " $clean_ip "
found_internal = true
fi
done < <( ip -4 -o addr show scope global | awk '{print $4}' )
2025-11-24 18:22:39 +00:00
# Fallback: If we found NO internal IPs and NO Public IP (local VM offline?),
2025-11-24 18:02:58 +00:00
# show the detected local IP from route (Home VM scenario)
if [ [ " $found_internal " = = false && " $public_ip " = = "Unknown" ] ] ; then
local fallback_ip
fallback_ip = $( ip -4 route get 8.8.8.8 2>/dev/null | head -1 | awk '{print $7}' )
if [ [ -n " $fallback_ip " ] ] ; then
printf " %-20s ${ CYAN } ssh -p %s %s@%s ${ NC } \n " "Local (LAN):" " $port " " $USERNAME " " $fallback_ip "
fi
fi
2025-11-24 17:39:56 +00:00
2025-11-24 18:02:58 +00:00
# 3. IPv6
if [ [ -n " $SERVER_IP_V6 " && " $SERVER_IP_V6 " != "Not available" ] ] ; then
printf " %-20s ${ CYAN } ssh -p %s %s@%s ${ NC } \n " "IPv6:" " $port " " $USERNAME " " $SERVER_IP_V6 "
fi
2025-11-24 17:39:56 +00:00
2025-11-24 18:02:58 +00:00
# 4. Tailscale IP (VPN)
if [ [ -n " $TS_IP " ] ] ; then
printf " %-20s ${ CYAN } ssh -p %s %s@%s ${ NC } \n " "Tailscale (VPN):" " $port " " $USERNAME " " $TS_IP "
fi
printf "\n"
}
# Show options for CURRENT port
show_connection_options " $CURRENT_SSH_PORT " " $SERVER_IP_V4 "
2025-08-26 11:40:41 +01:00
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 18:52:23 +01:00
printf '%s\n' "[Socket]" "ListenStream=" " ListenStream= $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 18:52:23 +01:00
printf '%s\n' "[Service]" "ExecStart=" " ExecStart=/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-11-24 17:39:56 +00:00
2025-11-24 18:02:58 +00:00
# Show options for NEW port
show_connection_options " $SSH_PORT " " $SERVER_IP_V4 "
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-10-19 19:01:46 +01:00
# Remove temporary UFW rule
if [ [ -n " $PREVIOUS_SSH_PORT " && " $PREVIOUS_SSH_PORT " != " $SSH_PORT " ] ] ; then
print_info " Removing temporary UFW rule for old SSH port $PREVIOUS_SSH_PORT ... "
ufw delete allow " $PREVIOUS_SSH_PORT " /tcp 2>/dev/null || true
fi
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-10-17 21:45:51 +01:00
# Ensure correct port rollback if already using custom port
print_info " Applying a systemd override to ensure rollback to port $PREVIOUS_SSH_PORT ... "
log " Rollback: Creating override to enforce port $PREVIOUS_SSH_PORT . "
if [ [ " $USE_SOCKET " = = true ] ] ; then
mkdir -p /etc/systemd/system/ssh.socket.d
printf '%s\n' "[Socket]" "ListenStream=" " ListenStream= $PREVIOUS_SSH_PORT " > /etc/systemd/system/ssh.socket.d/override.conf
else
local service_for_rollback = "ssh.service"
if systemctl list-units --full -all --no-pager | grep -qE "[[:space:]]sshd.service[[:space:]]" ; then
2025-10-17 23:01:22 +01:00
service_for_rollback = "sshd.service"
2025-10-17 21:45:51 +01:00
fi
mkdir -p " /etc/systemd/system/ ${ service_for_rollback } .d "
printf '%s\n' "[Service]" "ExecStart=" " ExecStart=/usr/sbin/sshd -D -p $PREVIOUS_SSH_PORT " > " /etc/systemd/system/ ${ service_for_rollback } .d/override.conf "
fi
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-10-19 19:01:46 +01:00
# Add temporary rule for current SSH port
if [ [ -n " $PREVIOUS_SSH_PORT " && " $PREVIOUS_SSH_PORT " != " $SSH_PORT " ] ] ; then
print_info " Temporarily adding UFW rule for current SSH port $PREVIOUS_SSH_PORT for transition... "
if ! ufw status | grep -qw " $PREVIOUS_SSH_PORT /tcp " ; then
ufw allow " $PREVIOUS_SSH_PORT " /tcp comment 'Temporary SSH for transition'
fi
fi
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-11-17 09:53:10 +00:00
# --- Collect User IPs to Ignore ---
2025-11-19 05:24:45 -06:00
local -a IGNORE_IPS = ( "127.0.0.1/8" "::1" ) # Array for easier dedup.
2025-11-19 11:54:54 -06:00
local -a INVALID_IPS = ( )
2025-11-19 05:24:45 -06:00
local prompt_change = ""
2025-11-24 13:12:06 +00:00
# Auto-detect and offer to whitelist current SSH connection
2025-11-24 15:06:56 +00:00
local DETECTED_IP = ""
2025-11-24 13:12:06 +00:00
if [ [ -n " ${ SSH_CONNECTION :- } " ] ] ; then
2025-11-24 15:06:56 +00:00
DETECTED_IP = " ${ SSH_CONNECTION %% * } "
fi
if [ [ -z " $DETECTED_IP " ] ] ; then
local WHO_IP
WHO_IP = $( who -m 2>/dev/null | awk '{print $NF}' | tr -d '()' )
if validate_ip_or_cidr " $WHO_IP " ; then
DETECTED_IP = " $WHO_IP "
fi
fi
if [ [ -z " $DETECTED_IP " ] ] ; then
local SS_IP
SS_IP = $( ss -tnH state established '( dport = :22 or sport = :22 )' 2>/dev/null | head -n 1 | awk '{print $NF}' | cut -d: -f1 | cut -d] -f1)
if validate_ip_or_cidr " $SS_IP " ; then
DETECTED_IP = " $SS_IP "
2025-11-19 05:24:45 -06:00
fi
2025-11-24 15:06:56 +00:00
fi
if [ [ -n " $DETECTED_IP " ] ] ; then
print_info " Detected SSH connection from: $DETECTED_IP "
if confirm " Whitelist your current IP ( $DETECTED_IP ) in Fail2Ban? " ; then
IGNORE_IPS += ( " $DETECTED_IP " )
print_success "Added your current IP to whitelist."
log " Auto-whitelisted SSH connection IP: $DETECTED_IP "
fi
prompt_change = " additional"
else
print_warning "Could not auto-detect current SSH IP. (This is normal in some VM/sudo environments)"
print_info "You can manually add your IP in the next step."
2025-11-19 05:24:45 -06:00
fi
2025-11-17 09:53:10 +00:00
2025-11-19 11:54:54 -06:00
if [ [ $VERBOSE != false ] ] && \
confirm " Add $prompt_change IP addresses or CIDR ranges to Fail2Ban ignore list (e.g., Tailscale)? " ; then
2025-11-19 05:24:45 -06:00
while true; do
local -a WHITELIST_IPS = ( )
log "Prompting user for IP addresses or CIDR ranges to whitelist via Fail2Ban ignore list..."
printf '%s\n' " ${ CYAN } Enter IP addresses or CIDR ranges to whitelist, separated by spaces. ${ NC } "
printf '%s\n' "Examples:"
printf ' %-24s %s\n' "Single IP:" "192.168.1.100"
printf ' %-24s %s\n' "CIDR Range:" "10.0.0.0/8"
printf ' %-24s %s\n' "IPv6 Address:" "2606:4700::1111"
2025-11-19 12:11:40 -06:00
printf ' %-24s %s\n' "Tailscale Range:" "100.64.0.0/10"
2025-11-19 05:24:45 -06:00
read -ra WHITELIST_IPS -p " > "
if ( ( ${# WHITELIST_IPS [@] } = = 0 ) ) ; then
print_info "No IP addresses entered. Skipping."
break
fi
local valid = true
2025-11-19 11:54:54 -06:00
INVALID_IPS = ( )
2025-11-19 05:24:45 -06:00
for ip in " ${ WHITELIST_IPS [@] } " ; do
if ! validate_ip_or_cidr " $ip " ; then
valid = false
INVALID_IPS += ( " $ip " )
2025-11-17 09:53:10 +00:00
fi
done
2025-11-19 05:24:45 -06:00
if [ [ " $valid " = = true ] ] ; then
IGNORE_IPS += ( " ${ WHITELIST_IPS [@] } " )
break
else
2025-11-19 11:54:54 -06:00
local s = ""
2025-11-19 05:24:45 -06:00
( ( ${# INVALID_IPS [@] } > 1 ) ) && s = "s" # Plural if > 1
print_error " Invalid IP $s : ${ INVALID_IPS [*] } "
2025-11-19 11:54:54 -06:00
printf '%s\n\n' "Please try again. Leave blank to skip."
2025-11-17 09:53:10 +00:00
fi
2025-11-19 05:24:45 -06:00
done
fi
# Deduplicate final IGNORE_IPS
if ( ( ${# IGNORE_IPS [@] } > 0 ) ) ; then
local -A seen = ( )
local -a unique = ( )
for ip in " ${ IGNORE_IPS [@] } " ; do
2025-11-19 11:54:54 -06:00
if [ [ ! -v seen[ $ip ] ] ] ; then
2025-11-19 05:24:45 -06:00
seen[ $ip ] = 1
unique += ( " $ip " )
2025-11-19 11:54:54 -06:00
fi
2025-11-19 05:24:45 -06:00
done
IGNORE_IPS = ( " ${ unique [@] } " )
2025-11-17 09:53:10 +00:00
fi
2025-11-19 11:54:54 -06:00
if ( ( ${# IGNORE_IPS [@] } > 2 ) ) ; then
local WHITELIST_STR
printf -v WHITELIST_STR "Whitelisting:\n"
for ip in " ${ IGNORE_IPS [@] : 2 } " ; do # Skip first two entries in console output ("127.0.0.1/8" "::1").
printf -v WHITELIST_STR "%s %s\n" " $WHITELIST_STR " " $ip "
done
print_info " $WHITELIST_STR "
fi
2025-11-17 09:53:10 +00:00
2025-07-07 21:22:47 +01:00
# --- Define Desired Configurations ---
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-11-19 05:24:45 -06:00
ignoreip = ${ IGNORE_IPS [*] }
2025-07-07 21:08:21 +01:00
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 ---
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 ---
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-11-17 09:53:10 +00:00
sleep 2
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."
fail2ban-client status | tee -a " $LOG_FILE "
2025-11-17 11:12:06 +00:00
# Show how to add IPs later
2025-11-19 05:24:45 -06:00
if ( ( ${# INVALID_IPS [@] } > 0 ) ) || confirm "Show instructions for adding IPs later?" "n" ; then
2025-11-17 11:12:06 +00:00
printf "\n"
2025-11-19 11:54:54 -06:00
if [ [ $VERBOSE = = false ] ] ; then
printf '%s\n' " ${ PURPLE } ℹ Fail2Ban ignore list modification:${ NC } "
fi
2025-11-17 11:12:06 +00:00
print_info "To add more IP addresses to Fail2Ban ignore list later:"
2025-11-17 11:20:44 +00:00
printf "%s1. Edit the configuration file:%s\n" " $CYAN " " $NC "
printf " %ssudo nano /etc/fail2ban/jail.local%s\n" " $BOLD " " $NC "
printf "%s2. Update the 'ignoreip' line under [DEFAULT]:%s\n" " $CYAN " " $NC "
printf " %signoreip = 127.0.0.1/8 ::1 YOUR_IP_HERE%s\n" " $BOLD " " $NC "
printf "%s3. Restart Fail2Ban:%s\n" " $CYAN " " $NC "
printf " %ssudo systemctl restart fail2ban%s\n" " $BOLD " " $NC "
printf "%s4. Verify the configuration:%s\n" " $CYAN " " $NC "
printf " %ssudo fail2ban-client status%s\n" " $BOLD " " $NC "
2025-11-17 11:12:06 +00:00
printf "\n"
log "Displayed post-installation Fail2Ban instructions."
fi
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
2025-11-25 00:13:34 +00:00
curl -fsSL " https://download.docker.com/linux/ ${ ID } /gpg " | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
2025-06-28 12:51:55 +01:00
chmod a+r /etc/apt/keyrings/docker.gpg
2025-11-25 00:13:34 +00:00
# shellcheck source=/dev/null
2025-06-28 12:51:55 +01:00
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)
2025-11-09 18:43:06 +00:00
tee " $NEW_DOCKER_CONFIG " > /dev/null <<DAEMONFILE
2025-06-28 12:51:55 +01:00
{
2025-11-10 14:26:36 +00:00
"log-driver" : "json-file" ,
"log-opts" : {
"max-size" : "10m" ,
"max-file" : "5" ,
"compress" : "true"
} ,
"live-restore" : true,
"dns" : [
"9.9.9.9" ,
"1.1.1.1" ,
"208.67.222.222"
] ,
"default-address-pools" : [
{
"base" : "172.80.0.0/16" ,
"size" : 24
}
] ,
"userland-proxy" : false,
"default-ulimits" : {
"nofile" : {
"Name" : "nofile" ,
"Hard" : 64000,
"Soft" : 64000
}
} ,
"features" : {
"buildkit" : true
2025-11-09 18:43:06 +00:00
}
2025-06-28 12:51:55 +01:00
}
2025-11-09 18:43:06 +00:00
DAEMONFILE
2025-06-28 12:51:55 +01:00
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."
2025-11-06 21:20:56 +00:00
# Offer dtop installation
install_dtop_optional
}
install_dtop_optional( ) {
if sudo sh -c 'command -v dtop' >/dev/null 2>& 1 || command -v dtop >/dev/null 2>& 1; then
print_info "dtop is already installed."
return 0
fi
if ! confirm "Install 'dtop' (Docker container monitoring TUI)?" ; then
print_info "Skipping dtop installation."
return 0
fi
print_info " Installing dtop for user ' $USERNAME '... "
2025-11-06 21:52:46 +00:00
local DTOP_INSTALLER = "/tmp/dtop-installer.sh"
2025-11-06 21:20:56 +00:00
if ! curl -fsSL "https://github.com/amir20/dtop/releases/latest/download/dtop-installer.sh" -o " $DTOP_INSTALLER " ; then
print_warning "Failed to download dtop installer. Continuing setup..."
log "Failed to download dtop installer."
return 0
2025-11-06 21:52:46 +00:00
fi
2025-11-06 21:20:56 +00:00
chmod +x " $DTOP_INSTALLER "
2025-11-06 21:52:46 +00:00
# shellcheck disable=SC2064
trap " rm -f ' $DTOP_INSTALLER ' " RETURN
2025-11-06 21:20:56 +00:00
local USER_HOME
USER_HOME = $( getent passwd " $USERNAME " | cut -d: -f6)
local USER_LOCAL_BIN = " $USER_HOME /.local/bin "
if [ [ ! -d " $USER_LOCAL_BIN " ] ] ; then
print_info " Creating $USER_LOCAL_BIN ... "
if ! sudo -u " $USERNAME " mkdir -p " $USER_LOCAL_BIN " ; then
print_warning " Failed to create $USER_LOCAL_BIN . Skipping dtop. "
return 0
fi
fi
2025-11-06 22:45:27 +00:00
# shellcheck disable=SC2024
2025-11-06 21:20:56 +00:00
if sudo -u " $USERNAME " bash " $DTOP_INSTALLER " < /dev/null >> " $LOG_FILE " 2>& 1; then
# Verify installation
if [ [ -f " $USER_LOCAL_BIN /dtop " ] ] ; then
sudo -u " $USERNAME " chmod +x " $USER_LOCAL_BIN /dtop "
local BASHRC = " $USER_HOME /.bashrc "
if [ [ -f " $BASHRC " ] ] && ! grep -q "\.local/bin" " $BASHRC " ; then
print_info " Adding ~/.local/bin to PATH in $BASHRC ... "
{
echo ''
echo '# Add local bin to PATH'
# shellcheck disable=SC2016
echo 'if [ -d "$HOME/.local/bin" ]; then PATH="$HOME/.local/bin:$PATH"; fi'
} >> " $BASHRC "
chown " $USERNAME : $USERNAME " " $BASHRC "
if grep -q "\.local/bin" " $BASHRC " ; then
print_info "PATH configuration updated successfully."
else
print_warning "Failed to update PATH, but dtop is still installed."
fi
fi
print_success " dtop installed successfully to $USER_LOCAL_BIN . "
log " dtop installed to $USER_LOCAL_BIN for user $USERNAME "
else
print_warning " dtop installer finished, but binary not found at $USER_LOCAL_BIN /dtop "
log "dtop binary missing after user installation attempt."
fi
else
print_warning "dtop installation script failed. Continuing setup..."
log "dtop installation script failed."
fi
2025-06-28 12:51:55 +01:00
}
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"
2025-10-20 12:59:08 +01:00
local CRON_MARKER = "#-*- installed by du_setup script -*-"
2025-06-28 14:51:26 +01:00
# --- 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 18:52:23 +01:00
read -rsp " $( printf '%s' " ${ CYAN } Enter password for $BACKUP_DEST : ${ NC } " ) " BACKUP_PASSWORD; printf '\n'
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-11-25 00:13:34 +00: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-10-16 18:35:19 +01:00
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-10-20 12:59:08 +01:00
printf -v message "Backup completed successfully.\nData Transferred: %s" " ${ 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-19 22:17:27 +01:00
local TEST_DIR TEST_FILE
2025-10-14 21:46:39 +01:00
TEST_DIR = " /root/test_backup_ $( date +%Y%m%d_%H%M%S) "
2025-10-19 22:17:27 +01:00
TEST_FILE = " $TEST_DIR /test_backup_verification_ $( date +%s) .txt "
if ! mkdir -p " $TEST_DIR " || ! echo " Test file for backup verification - $( date) " > " $TEST_FILE " ; then
2025-07-07 19:09:41 +01:00
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
2025-10-19 22:17:27 +01:00
print_info " Running test backup of single file to ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } ... "
local RSYNC_OUTPUT RSYNC_EXIT_CODE TIMEOUT_DURATION = 60
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-10-19 22:17:27 +01:00
RSYNC_OUTPUT = $( timeout " $TIMEOUT_DURATION " rsync -avz -e " $SSH_COMMAND " " $TEST_FILE " " ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } " 2>& 1)
2025-07-01 15:27:04 +01:00
RSYNC_EXIT_CODE = $?
2025-10-19 22:17:27 +01:00
set -e
2025-07-07 19:09:41 +01:00
2025-10-20 08:58:36 +01:00
{
echo " --- Test Backup at $( date) --- "
echo " Command: rsync -avz -e \" $SSH_COMMAND \" \" $TEST_FILE \" \" ${ BACKUP_DEST } : ${ REMOTE_BACKUP_PATH } \" "
echo "Output:"
echo " $RSYNC_OUTPUT "
echo " Exit Code: $RSYNC_EXIT_CODE "
} >> " $BACKUP_LOG "
2025-06-29 13:10:41 +01:00
if [ [ $RSYNC_EXIT_CODE -eq 0 ] ] ; then
2025-10-19 22:17:27 +01:00
print_success " Test backup (single file) successful! Check $BACKUP_LOG for details. "
log "Test backup successful (single file)."
ssh -p " $BACKUP_PORT " -i " $SSH_KEY " -o BatchMode = yes -o StrictHostKeyChecking = no " $BACKUP_DEST " " rm -f ' ${ REMOTE_BACKUP_PATH } $( basename " $TEST_FILE " ) ' " > /dev/null 2>& 1 || true
log " Attempted cleanup of remote test file: ${ REMOTE_BACKUP_PATH } $( basename " $TEST_FILE " ) "
2025-06-29 13:10:41 +01:00
else
2025-10-19 22:17:27 +01:00
print_warning "The backup test (single file transfer) failed. This is not critical, and the script will continue."
2025-07-07 19:09:41 +01:00
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 . "
2025-10-20 08:58:36 +01:00
# Hints based on common rsync errors
case " $RSYNC_OUTPUT " in
*"Permission denied" *)
print_info "Hint: Check SSH key authentication and permissions on the remote path."
; ;
*"Connection timed out" *| *"Connection refused" *| *"Network is unreachable" *)
print_info "Hint: Check network connectivity, firewall rules (local and remote), and the SSH port."
; ;
*"No such file or directory" *)
print_info " Hint: Verify the remote path ' ${ REMOTE_BACKUP_PATH } ' is correct and accessible. "
; ;
esac
2025-07-01 15:27:04 +01:00
fi
2025-06-29 13:10:41 +01:00
2025-07-07 19:09:41 +01:00
print_info "Common troubleshooting steps:"
2025-10-19 22:17:27 +01:00
print_info " - Ensure the root SSH key is copied: ssh-copy-id -p \" $BACKUP_PORT \" -i \" $SSH_KEY .pub\" \" $BACKUP_DEST \" "
print_info " - Manually test SSH connection: ssh -p \" $BACKUP_PORT \" -i \" $SSH_KEY \" \" $BACKUP_DEST \" "
print_info " - Check permissions on the remote path: ' ${ REMOTE_BACKUP_PATH } ' "
2025-07-01 15:27:04 +01:00
fi
2025-10-19 22:17:27 +01:00
# Clean up the local temporary test directory and file
2025-07-07 19:09:41 +01:00
rm -rf " $TEST_DIR " 2>/dev/null
2025-10-19 22:17:27 +01:00
print_info "Local test directory cleaned up."
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-11-26 21:33:25 +00:00
if [ [ " $IS_CONTAINER " = = true ] ] ; then
2025-06-26 21:49:51 +01:00
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-11-26 21:33:25 +00:00
2025-11-27 15:06:12 +00:00
# Check for existing swap partition entries in fstab
2025-06-29 19:09:50 +01:00
if lsblk -r | grep -q '\[SWAP\]' ; then
2025-11-26 21:33:25 +00:00
print_warning "Existing swap partition found on disk."
2025-06-29 19:09:50 +01:00
fi
2025-11-26 21:33:25 +00:00
# Detect active swap
local existing_swap swap_type
existing_swap = $( swapon --show= NAME,TYPE,SIZE --noheadings --bytes 2>/dev/null | head -n 1 | awk '{print $1}' || true )
2025-06-26 21:49:51 +01:00
if [ [ -n " $existing_swap " ] ] ; then
2025-11-26 21:33:25 +00:00
swap_type = $( swapon --show= NAME,TYPE,SIZE --noheadings | head -n 1 | awk '{print $2}' )
local display_size
display_size = $( du -h " $existing_swap " 2>/dev/null | awk '{print $1}' || echo "Unknown" )
print_info " Existing swap detected: $existing_swap (Type: $swap_type , Size: $display_size ) "
# --- Case 1: Partition detected ---
if [ [ " $swap_type " = = "partition" ] ] || [ [ " $existing_swap " = ~ ^/dev/ ] ] ; then
print_warning "The detected swap is a disk partition, which cannot be resized by this script."
if confirm "Disable this swap partition and create a standard /swapfile instead?" "n" ; then
print_info " Disabling swap partition $existing_swap ... "
if ! swapoff " $existing_swap " ; then
print_error "Failed to disable swap partition. Keeping it active."
return 0
2025-06-26 21:49:51 +01:00
fi
2025-11-26 21:33:25 +00:00
2025-11-27 14:26:57 +00:00
sed -i " s|^ ${ existing_swap } [[:space:]]|#&| " /etc/fstab
local swap_uuid
swap_uuid = $( blkid -s UUID -o value " $existing_swap " 2>/dev/null || true )
if [ [ -n " $swap_uuid " ] ] ; then
sed -i " s|^UUID= ${ swap_uuid } [[:space:]]|#&| " /etc/fstab
fi
2025-11-26 21:33:25 +00:00
print_success "Swap partition disabled and removed from fstab."
existing_swap = ""
else
print_info "Keeping existing swap partition."
configure_swap_settings
return 0
2025-06-26 21:49:51 +01:00
fi
else
2025-11-27 15:06:12 +00:00
# --- Case 2: Resize Existing Swap File ---
2025-11-26 21:33:25 +00:00
if confirm "Modify existing swap file size?" ; then
2025-11-27 15:06:12 +00:00
local SWAP_SIZE REQUIRED_MB
2025-11-26 21:33:25 +00:00
while true; do
read -rp " $( printf '%s' " ${ CYAN } Enter new swap size (e.g., 2G, 512M) [current: $display_size ]: ${ NC } " ) " SWAP_SIZE
SWAP_SIZE = ${ SWAP_SIZE :- $display_size }
2025-11-27 15:06:12 +00:00
# 1. Validate Format
if ! REQUIRED_MB = $( convert_to_mb " $SWAP_SIZE " ) ; then
continue
fi
# 2. Min Size
if ( ( REQUIRED_MB < 128 ) ) ; then
print_error "Swap size too small (minimum: 128M)."
continue
fi
# 3. Disk Space Check
local AVAILABLE_KB AVAILABLE_MB
AVAILABLE_KB = $( df -k / | tail -n 1 | awk '{print $4}' )
AVAILABLE_MB = $(( AVAILABLE_KB / 1024 ))
if ( ( AVAILABLE_MB < REQUIRED_MB ) ) ; then
print_error " Insufficient disk space. Required: ${ REQUIRED_MB } MB, Available: ${ AVAILABLE_MB } MB "
# Suggest Max Safe Size (80% of free space)
local MAX_SAFE_MB = $(( AVAILABLE_MB * 80 / 100 ))
if ( ( MAX_SAFE_MB >= 128 ) ) ; then
print_info " Suggested maximum: ${ MAX_SAFE_MB } M (leaves 20% free space) "
fi
continue
2025-11-27 14:26:57 +00:00
fi
2025-11-27 15:06:12 +00:00
break
2025-11-26 21:33:25 +00:00
done
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 ... "
2025-11-27 15:06:12 +00:00
# Try fallocate, fallback to dd
2025-11-26 21:33:25 +00:00
if ! fallocate -l " $SWAP_SIZE " " $existing_swap " 2>/dev/null; then
2025-11-27 14:26:57 +00:00
print_warning "fallocate failed. Using dd (slower)..."
2025-11-26 21:33:25 +00:00
rm -f " $existing_swap "
2025-11-27 14:26:57 +00:00
local dd_status = ""
if dd --version 2>& 1 | grep -q "progress" ; then dd_status = "status=progress" ; fi
if ! dd if = /dev/zero of = " $existing_swap " bs = 1M count = " $REQUIRED_MB " $dd_status ; then
2025-11-26 21:33:25 +00:00
print_error "Failed to create swap file with dd."
exit 1
fi
fi
if ! chmod 600 " $existing_swap " || ! mkswap " $existing_swap " >/dev/null || ! swapon " $existing_swap " ; then
print_error "Failed to configure swap file."
exit 1
fi
print_success " Swap file resized to $SWAP_SIZE . "
else
print_info "Keeping existing swap file."
fi
configure_swap_settings
2025-06-26 21:49:51 +01:00
return 0
fi
2025-11-26 21:33:25 +00:00
fi
2025-11-27 14:26:57 +00:00
# --- Case 3: Create New Swap ---
2025-11-26 21:33:25 +00:00
if [ [ -z " $existing_swap " ] ] ; then
2025-06-26 21:49:51 +01:00
if ! confirm "Configure a swap file (recommended for < 4GB RAM)?" ; then
print_info "Skipping swap configuration."
return 0
fi
2025-11-26 21:33:25 +00:00
2025-11-27 15:06:12 +00:00
local SWAP_SIZE REQUIRED_MB
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 }
2025-11-27 15:06:12 +00:00
# 1. Validate Format
if ! REQUIRED_MB = $( convert_to_mb " $SWAP_SIZE " ) ; then
continue
fi
# 2. Min Size
if ( ( REQUIRED_MB < 128 ) ) ; then
print_error "Swap size too small (minimum: 128M)."
continue
fi
# 3. Disk Space Check
local AVAILABLE_KB AVAILABLE_MB
AVAILABLE_KB = $( df -k / | tail -n 1 | awk '{print $4}' )
AVAILABLE_MB = $(( AVAILABLE_KB / 1024 ))
if ( ( AVAILABLE_MB < REQUIRED_MB ) ) ; then
print_error " Insufficient disk space. Required: ${ REQUIRED_MB } MB, Available: ${ AVAILABLE_MB } MB "
# Suggest Max Safe Size
local MAX_SAFE_MB = $(( AVAILABLE_MB * 80 / 100 ))
if ( ( MAX_SAFE_MB >= 128 ) ) ; then
print_info " Suggested maximum: ${ MAX_SAFE_MB } M (leaves 20% free space) "
fi
continue
fi
break
2025-06-26 21:49:51 +01:00
done
2025-11-26 21:33:25 +00:00
print_info " Creating $SWAP_SIZE swap file at /swapfile... "
if ! fallocate -l " $SWAP_SIZE " /swapfile 2>/dev/null; then
print_warning "fallocate failed. Using dd (slower)..."
2025-11-27 14:26:57 +00:00
local dd_status = ""
if dd --version 2>& 1 | grep -q "progress" ; then dd_status = "status=progress" ; fi
if ! dd if = /dev/zero of = /swapfile bs = 1M count = " $REQUIRED_MB " $dd_status ; then
2025-11-26 21:33:25 +00:00
print_error "Failed to create swap file."
rm -f /swapfile || true
exit 1
fi
fi
if ! chmod 600 /swapfile || ! mkswap /swapfile >/dev/null || ! swapon /swapfile; then
print_error "Failed to 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-11-26 21:33:25 +00:00
if ! grep -q '^/swapfile ' /etc/fstab; then
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-11-26 21:33:25 +00:00
else
print_info "Swap entry already exists in /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-11-26 21:33:25 +00:00
configure_swap_settings
}
# Helper: Apply Sysctl Settings for Swap
configure_swap_settings( ) {
2025-06-27 12:56:02 +01:00
print_info "Configuring swap settings..."
local SWAPPINESS = 10
local CACHE_PRESSURE = 50
2025-11-26 21:33:25 +00:00
2025-06-27 12:56:02 +01:00
if confirm "Customize swap settings (vm.swappiness and vm.vfs_cache_pressure)?" ; then
while true; do
2025-11-26 21:33:25 +00:00
read -rp " $( printf '%s' " ${ CYAN } Enter vm.swappiness (0-100) [default: $SWAPPINESS ]: ${ NC } " ) " INPUT
INPUT = ${ INPUT :- $SWAPPINESS }
if [ [ " $INPUT " = ~ ^[ 0-9] +$ && " $INPUT " -ge 0 && " $INPUT " -le 100 ] ] ; then SWAPPINESS = $INPUT ; break; fi
print_error "Invalid value (0-100)."
2025-06-27 12:56:02 +01:00
done
while true; do
2025-11-26 21:33:25 +00:00
read -rp " $( printf '%s' " ${ CYAN } Enter vm.vfs_cache_pressure (1-1000) [default: $CACHE_PRESSURE ]: ${ NC } " ) " INPUT
INPUT = ${ INPUT :- $CACHE_PRESSURE }
if [ [ " $INPUT " = ~ ^[ 0-9] +$ && " $INPUT " -ge 1 && " $INPUT " -le 1000 ] ] ; then CACHE_PRESSURE = $INPUT ; break; fi
print_error "Invalid value (1-1000)."
2025-06-27 12:56:02 +01:00
done
else
print_info " Using default swap settings (vm.swappiness= $SWAPPINESS , vm.vfs_cache_pressure= $CACHE_PRESSURE ). "
fi
2025-11-26 21:33:25 +00:00
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-11-26 21:33:25 +00:00
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-11-26 21:33:25 +00:00
print_info "Swap settings already correct. Skipping."
2025-06-26 21:49:51 +01:00
rm -f " $NEW_SWAP_CONFIG "
else
2025-11-26 21:33:25 +00:00
# Scan for conflicts
2025-06-27 13:47:19 +01:00
for file in /etc/sysctl.conf /etc/sysctl.d/*.conf; do
2025-11-26 21:33:25 +00:00
[ [ -f " $file " && " $file " != "/etc/sysctl.d/99-swap.conf" ] ] && \
grep -E '^(vm\.swappiness|vm\.vfs_cache_pressure)=' " $file " >/dev/null && \
print_warning " Note: Duplicate swap settings found in $file . "
2025-06-27 13:47:19 +01:00
done
2025-11-26 21:33:25 +00:00
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-11-26 21:33:25 +00:00
print_success "Swap settings applied to /etc/sysctl.d/99-swap.conf."
2025-06-26 16:22:23 +01:00
fi
2025-11-26 21:33:25 +00:00
2025-06-26 21:49:51 +01:00
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
2025-11-25 00:13:34 +00:00
# shellcheck source=/dev/null
2025-06-29 18:38:12 +01:00
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( ) {
2025-11-26 11:06:12 +00:00
print_section "Final System Update & Cleanup"
print_info "Performing final system upgrade (dist-upgrade) and cleanup..."
print_info "This may take a moment. Please wait..."
# Upgrade ALL packages (including kernels)
if ! apt-get update -qq >/dev/null 2>& 1; then
2025-06-26 22:19:35 +01:00
print_warning "Failed to update package lists during final cleanup."
2025-11-26 11:06:12 +00:00
log "Final apt-get update failed."
2025-06-26 17:12:59 +01:00
fi
2025-11-26 11:06:12 +00:00
if ! DEBIAN_FRONTEND = noninteractive apt-get dist-upgrade -y -qq -o Dpkg::Options::= "--force-confdef" -o Dpkg::Options::= "--force-confold" >> " $LOG_FILE " 2>& 1; then
print_warning "Final system upgrade encountered issues. Check log for details."
log "Final apt-get dist-upgrade failed."
else
print_success "System packages (including kernels) upgraded successfully."
log "Final apt-get dist-upgrade completed."
fi
# Final cleanup
print_info "Removing unused packages..."
if ! apt-get --purge autoremove -y -qq >> " $LOG_FILE " 2>& 1 || ! apt-get autoclean -y -qq >> " $LOG_FILE " 2>& 1; then
print_warning "Cleanup commands encountered minor issues."
else
print_success "Unused packages removed."
2025-06-26 17:12:59 +01:00
fi
2025-06-26 21:49:51 +01:00
systemctl daemon-reload
2025-11-26 11:06:12 +00:00
print_success "Final cleanup complete."
2025-06-26 21:49:51 +01:00
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 21:27:38 +01:00
print_separator "Final Service Status Check:"
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-10-16 18:29:39 +01:00
printf '\n'
2025-07-15 13:51:34 +01:00
# --- Main Configuration Summary ---
2025-10-16 21:27:38 +01:00
print_separator "Configuration Summary:"
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-11-26 11:19:34 +00:00
if [ [ " ${ SERVER_IP_V4 :- } " != "unknown" && " ${ SERVER_IP_V4 :- } " != "Unknown" ] ] ; then
2025-08-26 16:14:58 +01:00
printf " %-15s %s\n" "Server IPv4:" " $SERVER_IP_V4 "
fi
2025-11-26 11:19:34 +00:00
if [ [ " ${ SERVER_IP_V6 :- } " != "not available" && " ${ 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-10-16 18:29:39 +01:00
printf '\n'
2025-07-15 13:51:34 +01:00
2025-11-26 11:19:34 +00:00
# --- System & Environment Information ---
print_separator "System & Environment Information"
# OS and Kernel Info
printf "%-20s %s\n" "OS:" " ${ PRETTY_NAME :- Unknown } "
printf "%-20s %s\n" "Kernel:" " $( uname -r) "
printf "%-20s %s\n" "Uptime:" " $( uptime -p 2>/dev/null || uptime | sed 's/.*up //;s/,.*//' ) "
# Hardware/Virtualization Info
2025-10-16 19:31:13 +01:00
printf "%-20s %s\n" "Virtualization:" " ${ DETECTED_VIRT_TYPE :- unknown } "
2025-11-26 11:19:34 +00:00
if [ [ " ${ DETECTED_MANUFACTURER :- unknown } " != "unknown" ] ] ; then
printf "%-20s %s\n" "Manufacturer:" " $DETECTED_MANUFACTURER "
fi
if [ [ " ${ DETECTED_PRODUCT :- unknown } " != "unknown" ] ] ; then
printf "%-20s %s\n" "Product:" " $DETECTED_PRODUCT "
fi
if [ [ " ${ DETECTED_BIOS_VENDOR :- unknown } " != "unknown" ] ] ; then
printf "%-20s %s\n" "BIOS Vendor:" " $DETECTED_BIOS_VENDOR "
fi
2025-11-26 11:06:12 +00:00
2025-11-26 11:19:34 +00:00
# Environment Classification
2025-11-26 11:06:12 +00:00
printf "%-20s " "Environment:"
case " $ENVIRONMENT_TYPE " in
2025-11-26 12:05:18 +00:00
commercial-cloud) printf "%sCloud VPS%s\n" " $YELLOW " " $NC " ; ;
bare-metal) printf "%sBare Metal%s\n" " $GREEN " " $NC " ; ;
uncertain-kvm) printf "%sGeneric KVM (Likely Cloud VPS)%s\n" " $YELLOW " " $NC " ; ;
personal-vm) printf "%sPersonal VM%s\n" " $CYAN " " $NC " ; ;
*) printf "Unknown\n" ; ;
2025-11-26 11:06:12 +00:00
esac
if [ [ -n " $DETECTED_PROVIDER_NAME " ] ] ; then
printf "%-20s %s\n" "Detected Provider:" " $DETECTED_PROVIDER_NAME "
2025-10-16 19:31:13 +01:00
fi
printf '\n'
2025-08-26 14:05:48 +01:00
# --- Post-Reboot Verification Steps ---
2025-10-16 21:27:38 +01:00
print_separator "Post-Reboot Verification Steps:"
2025-10-16 12:29:34 +01:00
printf ' - SSH access:\n'
2025-11-24 18:22:39 +00:00
# 1. Public Access
2025-11-25 00:13:34 +00:00
if [ [ " ${ SERVER_IP_V4 :- } " != "unknown" && " ${ SERVER_IP_V4 :- } " != "Unknown" ] ] ; then
2025-11-24 18:22:39 +00:00
printf " %-26s ${ CYAN } %s ${ NC } \n " "- Public (Internet):" " ssh -p $SSH_PORT $USERNAME @ $SERVER_IP_V4 "
fi
2025-11-25 00:13:34 +00:00
# 2. Local Access
if [ [ -n " ${ LOCAL_IP_V4 :- } " ] ] ; then
# Show local if public is unknown OR if they are different IPs
if [ [ " ${ SERVER_IP_V4 :- } " = = "Unknown" || " ${ SERVER_IP_V4 :- } " = = "unknown" || " ${ LOCAL_IP_V4 :- } " != " ${ SERVER_IP_V4 :- } " ] ] ; then
2025-11-24 18:22:39 +00:00
printf " %-26s ${ CYAN } %s ${ NC } \n " "- Local (LAN):" " ssh -p $SSH_PORT $USERNAME @ $LOCAL_IP_V4 "
fi
2025-08-26 16:14:58 +01:00
fi
2025-11-24 18:22:39 +00:00
# 3. Tailscale Access
if [ [ -f /tmp/tailscale_ips.txt ] ] ; then
local TS_SUMMARY_IP
TS_SUMMARY_IP = $( grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' /tmp/tailscale_ips.txt | head -n 1)
if [ [ -n " $TS_SUMMARY_IP " ] ] ; then
printf " %-26s ${ CYAN } %s ${ NC } \n " "- Tailscale (VPN):" " ssh -p $SSH_PORT $USERNAME @ $TS_SUMMARY_IP "
fi
2025-08-26 14:05:48 +01:00
fi
2025-11-24 18:22:39 +00:00
# 4. IPv6 Access
2025-11-25 00:13:34 +00:00
if [ [ " ${ SERVER_IP_V6 :- } " != "not available" && " ${ SERVER_IP_V6 :- } " != "Not available" ] ] ; then
2025-11-24 18:22:39 +00:00
printf " %-26s ${ CYAN } %s ${ NC } \n " "- IPv6:" " ssh -p $SSH_PORT $USERNAME @ $SERVER_IP_V6 "
fi
# Other verification commands
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-10-16 18:29:39 +01:00
printf '\n'
2025-07-15 13:51:34 +01:00
# --- 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-11-24 14:52:56 +00:00
check_dependencies
2025-10-13 22:52:07 +01:00
check_system
2025-10-15 21:00:20 +01:00
run_update_check
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 " $@ "