From 8658dac58c52466c8b762d13e1051f0b4a0cc3dd Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 11:43:10 +0100 Subject: [PATCH 01/22] Added rsync backup option --- README.md | 161 ++++++++++----- setup_harden_debian_ubuntu.sh | 371 +++++++++++++++++++++++++++++++++- 2 files changed, 466 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index a13d7c8..347f5d8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ # Debian & Ubuntu Server Setup & Hardening Script -**Version:** 3.13 +**Version:** 4.1 -**Last Updated:** 2025-06-27 +**Last Updated:** 2025-06-28 **Compatible With:** - Debian 12 -- Ubuntu 22.04, 24.04, 24.10 - -* * * +- Ubuntu 22.04, 24.04, 24.10 (24.10 experimental) ## Overview @@ -17,8 +15,6 @@ This script automates the initial setup and security hardening of a fresh Debian It runs interactively, guiding the user through critical choices while automating the tedious but essential steps of securing a new server. -* * * - ## Features - **Secure User Management:** Creates a new administrator user with `sudo` privileges and disables the root account's SSH access. @@ -27,15 +23,14 @@ It runs interactively, guiding the user through critical choices while automatin - **Intrusion Prevention:** Installs and configures **Fail2Ban** to automatically block IPs that show malicious signs, such as repeated password failures. - **Automated Security Updates:** Configures `unattended-upgrades` to automatically install new security patches. - **System Stability:** Sets up NTP time synchronization with `chrony` and can configure a swap file for systems with low RAM. +- **Remote rsync Backups:** Configures a root cron job for `rsync` backups to any SSH-accessible server (e.g., Hetzner Storage Box, NAS, or custom server), with SSH key automation, cron scheduling, ntfy/Discord notifications, and customizable exclude file. - **Safety First:** Automatically backs up all critical configuration files before modification, with simple restoration instructions. - **Optional Software:** Provides optional, interactive installation for: - - Docker & Docker Compose - - Tailscale (Mesh VPN) + - Docker & Docker Compose + - Tailscale (Mesh VPN) - **Comprehensive Logging:** All actions are logged to `/var/log/setup_harden_debian_ubuntu_*.log`. - **Automation-Friendly:** Includes a `--quiet` mode to suppress non-essential output for use in automated provisioning workflows. -* * * - ## Installation & Usage ### Prerequisites @@ -43,43 +38,45 @@ It runs interactively, guiding the user through critical choices while automatin - A fresh installation of a compatible OS. - Root or `sudo` privileges. - Internet access for downloading packages. +- For remote backups: An SSH-accessible server (e.g., Hetzner Storage Box or custom server) with credentials or SSH key access. -### 1\. Download the Script +### 1. Download the Script -``` +```bash wget https://raw.githubusercontent.com/buildplan/setup_harden_server/refs/heads/main/setup_harden_debian_ubuntu.sh chmod +x setup_harden_debian_ubuntu.sh ``` -### 2\. Run the Script Interactively +### 2. Run the Script Interactively It is highly recommended to run the script interactively the first time. -``` +```bash sudo ./setup_harden_debian_ubuntu.sh ``` -### 3\. Run in Quiet Mode (for automation - not recmmended) +### 3. Run in Quiet Mode (for automation - not recommended) -``` +```bash sudo ./setup_harden_debian_ubuntu.sh --quiet ``` -> :warning: **Critical Safety Check:** The script will pause and require you to test your new SSH connection from a separate terminal before it proceeds to disable old access methods. **Do not skip this step!** +> **Warning:** The script will pause and require you to test your new SSH connection from a separate terminal before it proceeds to disable old access methods. **Do not skip this step!** > -> *Make sure to check VPS providers firewall, you will have to open your selected custom SSH port there.* - -* * * +> *Make sure to check your VPS provider's firewall; you will have to open your selected custom SSH port there.* +> +> *For remote backups, ensure the backup server's SSH port is open and accessible.* ## What It Does in Detail | Task | Description | | --- | --- | | **System Checks** | Verifies OS compatibility, root privileges, and internet connectivity. | -| **Package Management** | Updates all packages and installs essential tools (`ufw`, `fail2ban`, `chrony`, etc.). | +| **Package Management** | Updates all packages and installs essential tools (`ufw`, `fail2ban`, `chrony`, `rsync`, etc.). | | **Admin User Creation** | Creates a new `sudo` user with a password and/or a provided SSH public key. | | **SSH Hardening** | Disables root login, enforces key-based auth, and sets a custom port. | | **Firewall Setup** | Configures UFW to deny incoming traffic by default and allow specific ports. | +| **Remote Backup Setup** | (Optional) Configures `rsync` backups to a user-specified SSH server (e.g., `user@host:port`), including root SSH key generation, cron job scheduling, ntfy/Discord notifications, and an exclude file with defaults (e.g., `*~`, `*.tmp`). | | **System Backups** | Creates timestamped backups of configs in `/root/` before modification. | | **Swap File Setup** | (Optional) Creates a swap file with a user-selected size. | | **Timezone & Locales** | (Optional) Interactive configuration for timezone and system locales. | @@ -87,31 +84,43 @@ sudo ./setup_harden_debian_ubuntu.sh --quiet | **Tailscale Install** | (Optional) Installs the Tailscale client. | | **Final Cleanup** | Removes unused packages and reloads system daemons. | -* * * - ## Logs & Backups - **Log Files:** `/var/log/setup_harden_debian_ubuntu_*.log` +- **Backup Logs:** `/var/log/backup_*.log` (for remote backup operations) - **Configuration Backups:** `/root/setup_harden_backup_*` -* * * +## Post-Reboot Verification Steps + +After rebooting, verify the setup with the following commands: + +- **SSH Access**: `ssh -p @` +- **Firewall Rules**: `sudo ufw status verbose` +- **Time Synchronization**: `chronyc tracking` +- **Fail2Ban Status**: `sudo fail2ban-client status sshd` +- **Swap Status**: `sudo swapon --show && free -h` +- **Hostname**: `hostnamectl` +- **Docker Status** (if installed): `docker ps` +- **Tailscale Status** (if installed): `tailscale status` +- **Remote Backup** (if configured): + - Verify SSH key: `cat /root/.ssh/id_ed25519.pub` + - Copy key to backup server (if not done during setup): `ssh-copy-id -p -s ` + - Test backup: `sudo /root/backup.sh` + - Check backup logs: `sudo less /var/log/backup_*.log` ## Tested On - Debian 12 -- Ubuntu 24.04 and 24.10 -- Cloud providers (DigitalOcean, Oracle Cloud, Hetzner, Netcup) and local VMs. +- Ubuntu 22.04, 24.04, 24.10 (experimental) +- Cloud providers (DigitalOcean, Oracle Cloud, Hetzner, Netcup) and local VMs, including Hetzner Storage Box for backups. -* * * - -## :exclamation: Important Notes +## Important Notes - **Run this on a fresh system.** While idempotent, the script is designed for initial provisioning. - **A system reboot is required** after the script completes to ensure all changes, especially to the kernel and services, are applied cleanly. - Always test the script in a non-production environment (like a staging VM) before deploying to a live server. - Ensure you have out-of-band console access to your server in case you accidentally lock yourself out. - -* * * +- For remote backups, ensure the root SSH key is copied to the backup server (`ssh-copy-id -p -s `) to enable automated backups. ## Troubleshooting @@ -119,31 +128,71 @@ sudo ./setup_harden_debian_ubuntu.sh --quiet If you are locked out of SSH, use your provider's web console to perform the following steps: -1. **Remove the hardened configuration:** - - ``` - # This file overrides the main config, so it must be removed. - rm /etc/ssh/sshd_config.d/99-hardening.conf - ``` - -2. **Restore the original `sshd_config` file:** - - ``` - # Find the latest backup directory - LATEST_BACKUP=$(ls -td /root/setup_harden_backup_* | head -1) - - # Copy the original config back into place - cp "$LATEST_BACKUP"/sshd_config.backup_* /etc/ssh/sshd_config - ``` +1. **Remove the hardened configuration:** -3. **Restart the SSH service:** - ``` - systemctl restart ssh - ``` - You should now be able to log in using the original port (usually 22) and credentials. + ```bash + # This file overrides the main config, so it must be removed. + rm /etc/ssh/sshd_config.d/99-hardening.conf + ``` ---- +2. **Restore the original `sshd_config` file:** -## [MIT](https://github.com/buildplan/setup_harden_server/blob/main/LICENSE "LICENCE") License + ```bash + # Find the latest backup directory + LATEST_BACKUP=$(ls -td /root/setup_harden_backup_* | head -1) + + # Copy the original config back into place + cp "$LATEST_BACKUP"/sshd_config.backup_* /etc/ssh/sshd_config + ``` -This script is open-source and provided "as is" without warranty. Use at your own risk. +3. **Restart the SSH service:** + + ```bash + systemctl restart ssh + ``` + + You should now be able to log in using the original port (usually 22) and credentials. + +### Backup Issues + +If backups fail, check the following: + +1. **Verify SSH Key Setup**: + - Ensure the root SSH key is copied to the backup server: + ```bash + ssh-copy-id -p -s + ``` + - Test SSH connectivity: + ```bash + ssh -p exit + ``` + +2. **Check Backup Logs**: + - Review logs for errors: + ```bash + sudo less /var/log/backup_*.log + ``` + +3. **Test Backup Manually**: + - Run the backup script to identify issues: + ```bash + sudo /root/backup.sh + ``` + +4. **Verify Cron Job**: + - Check the cron schedule: + ```bash + sudo crontab -l + ``` + - Ensure the schedule is valid (e.g., `0 3 * * *` for daily at 3 AM). + +5. **Network Issues**: + - Verify the backup server’s SSH port is open: + ```bash + nc -zv + ``` + - Check your VPS provider’s firewall for outbound access to the backup server’s port. + +## [MIT](https://github.com/buildplan/setup_harden_server/blob/main/LICENSE "LICENSE") License + +This script is open-source and provided "as is" without warranty. Use at your own risk. \ No newline at end of file diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index da040ed..e96045b 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,9 +1,12 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 3.13 | 2025-06-27 -# Compatible with: Debian 12 (Bookworm), Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS. 24.10 (experimental) -# Tested on Debian 12, Ubuntu 24.04 and 24.10 at DigitalOcean, Oracle Cloud, Netcup, Hetzner and local VMs +# Version: 4.1 | 2025-06-28 +# Changelog: +# - v4.1: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. +# - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. +# - v4.0: Enhanced generate_summary to include backup details (script path, cron schedule, notifications). +# - v4.0: Tested on Debian 12, Ubuntu 20.04, 22.04, 24.04, and 24.10 (experimental) at DigitalOcean, Oracle Cloud, Netcup, Hetzner, and local VMs. # # Description: # This script provisions and hardens a fresh Debian 12 or Ubuntu server with essential security @@ -80,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v3.13 | 2025-06-27 ║${NC}" + echo -e "${CYAN}║ v4.1 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo @@ -335,8 +338,8 @@ install_packages() { print_info "Installing essential packages..." if ! apt-get install -y -qq \ ufw fail2ban unattended-upgrades chrony \ - rsync wget vim htop iotop nethogs ncdu tree \ - rsyslog cron jq gawk coreutils perl skopeo git \ + rsync wget vim htop iotop nethogs netcat-traditional ncdu \ + tree rsyslog cron jq gawk coreutils perl skopeo git \ openssh-client openssh-server; then print_error "Failed to install one or more essential packages." exit 1 @@ -601,7 +604,7 @@ EOF mv "$NEW_SSH_CONFIG" /etc/systemd/system/ssh.service.d/override.conf chmod 644 /etc/systemd/system/ssh.service.d/override.conf fi - + # Apply additional hardening via sshd_config.d NEW_SSH_CONFIG=$(mktemp) tee "$NEW_SSH_CONFIG" > /dev/null </dev/null; then + print_success "SSH key copied successfully." + else + print_warning "SSH key copy failed. You must manually copy the key later." + fi + fi + + # Display SSH key copy instructions + print_warning "ACTION REQUIRED: If not already done, copy the root SSH key to the backup destination to enable backups." + echo -e "${YELLOW}Root public key:${NC}" + cat "$ROOT_SSH_KEY.pub" + echo -e "${CYAN}Run this command on your local machine or another terminal:${NC}" + echo -e " ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" + print_info "You can copy the SSH key later, but the backup cron job will fail until this is done." + + # Optional SSH connection test + if confirm "Test SSH connection to the backup destination (optional)?"; then + print_info "Testing connection (timeout: 5 seconds)..." + DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2) + if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=5 "$BACKUP_DEST" exit 2>/dev/null; then + print_success "SSH connection successful!" + else + print_error "SSH connection failed." + print_info "Verify the following:" + print_info " 1. The key was copied: ${YELLOW}ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST${NC}" + if command -v nc >/dev/null 2>&1; then + print_info " 2. Port $BACKUP_PORT is open: ${YELLOW}nc -zv $DEST_HOST $BACKUP_PORT${NC}" + else + print_info " 2. Port $BACKUP_PORT is open: ${YELLOW}Contact your network admin or try 'telnet $DEST_HOST $BACKUP_PORT'${NC}" + fi + fi + fi + + # Create exclude file + local EXCLUDE_FILE="/root/backup_exclude.txt" + print_info "Creating rsync exclude file at $EXCLUDE_FILE..." + cat > "$EXCLUDE_FILE" <> "$EXCLUDE_FILE" + done + fi + chmod 600 "$EXCLUDE_FILE" + print_success "Rsync exclude file created." + + # Ask for cron schedule + print_info "Configuring cron schedule for backups..." + read -rp "$(echo -e "${CYAN}Enter cron schedule (e.g., '0 3 * * *' for daily at 3 AM): ${NC}")" CRON_SCHEDULE + CRON_SCHEDULE=${CRON_SCHEDULE:-0 3 * * *} + if ! echo "$CRON_SCHEDULE" | grep -qE '^((\*|[0-9,-]+(/[0-9]+)?)\s*){5}$'; then + print_error "Invalid cron expression. Using default daily at 3 AM." + CRON_SCHEDULE="0 3 * * *" + fi + # Ask for notification preference + local NOTIFICATION_SETUP="none" NTFY_URL NTFY_TOPIC NTFY_TOKEN DISCORD_WEBHOOK + if confirm "Enable backup notifications?"; then + echo -e "${CYAN}Choose notification method:${NC}" + echo -e " 1) ntfy" + echo -e " 2) Discord" + echo -e " 3) None" + read -rp "$(echo -e "${CYAN}Enter choice (1-3): ${NC}")" NOTIFICATION_CHOICE + case "$NOTIFICATION_CHOICE" in + 1) + read -rp "$(echo -e "${CYAN}Enter ntfy URL (e.g., https://ntfy.sh): ${NC}")" NTFY_URL + read -rp "$(echo -e "${CYAN}Enter ntfy topic: ${NC}")" NTFY_TOPIC + read -rp "$(echo -e "${CYAN}Enter ntfy token (optional, press Enter to skip): ${NC}")" NTFY_TOKEN + NTFY_URL=${NTFY_URL:-https://ntfy.sh} + NTFY_TOPIC=${NTFY_TOPIC:-vps-backups} + NOTIFICATION_SETUP="ntfy" + ;; + 2) + read -rp "$(echo -e "${CYAN}Enter Discord webhook URL: ${NC}")" DISCORD_WEBHOOK + if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then + print_error "Invalid Discord webhook URL." + exit 1 + fi + NOTIFICATION_SETUP="discord" + ;; + *) NOTIFICATION_SETUP="none" ;; + esac + fi + + # Create backup script + local BACKUP_SCRIPT="/root/backup.sh" + print_info "Creating backup script at $BACKUP_SCRIPT..." + cat > "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <<'EOF' +# Notification function +send_notification() { + local title="$1" + local message="$2" + local priority="${3:-default}" + local color=65280 # Green for success + if [[ "$title" == *"FAILED"* ]]; then + color=16711680 # Red for failure + fi +EOF + if [[ "$NOTIFICATION_SETUP" == "ntfy" ]]; then + cat >> "$BACKUP_SCRIPT" < /dev/null 2>> "$LOG_FILE" +EOF + elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then + cat >> "$BACKUP_SCRIPT" < /dev/null 2>> "$LOG_FILE" +EOF + else + cat >> "$BACKUP_SCRIPT" <<'EOF' + : # No notifications configured +EOF + fi + cat >> "$BACKUP_SCRIPT" <<'EOF' +} + +# Format backup stats +format_backup_stats() { + local stats_line + stats_line=$("$GREP_CMD" 'Total transferred file size' "$LOG_FILE" | tail -n 1) + if [ -n "$stats_line" ]; then + local bytes + bytes=$(echo "$stats_line" | "$AWK_CMD" '{gsub(/,/, ""); print $5}') + if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then + local human_readable + human_readable=$("$NUMFMT_CMD" --to=iec-i --suffix=B --format="%.2f" "$bytes") + printf "Data Transferred: $human_readable" + else + printf "Data Transferred: 0 B (No changes)" + fi + else + printf "See log for statistics." + fi +} + +# Dependency check +for cmd in "$RSYNC_CMD" "$CURL_CMD" "$NC_CMD" "$AWK_CMD" "$NUMFMT_CMD" "$GREP_CMD" "$HOSTNAME_CMD" "$DATE_CMD" "$STAT_CMD" "$MV_CMD" "$TOUCH_CMD"; do + if ! command -v "$cmd" &>/dev/null; then + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Required command not found at '$cmd'" >> "$LOG_FILE" + send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Required command not found at '$cmd'" "high" + exit 10 + fi +done + +# Pre-flight checks +if [[ ! -f "$EXCLUDE_FILE" ]]; then + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Exclude file not found at $EXCLUDE_FILE" >> "$LOG_FILE" + send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Exclude file not found at $EXCLUDE_FILE" "high" + exit 3 +fi +if [[ "$LOCAL_DIR" != */ ]]; then + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: LOCAL_DIR must end with a trailing slash" >> "$LOG_FILE" + send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "LOCAL_DIR must end with a trailing slash" "high" + exit 2 +fi + +# Log rotation +if [ -f "$LOG_FILE" ] && [ "$("$STAT_CMD" -c%s "$LOG_FILE")" -gt "$MAX_LOG_SIZE" ]; then + "$MV_CMD" "$LOG_FILE" "${LOG_FILE}.$("$DATE_CMD" +%Y%m%d_%H%M%S)" + "$TOUCH_CMD" "$LOG_FILE" +fi + +# Network connectivity check +DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2) +if ! "$NC_CMD" -z -w 5 "$DEST_HOST" "$SSH_PORT"; then + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Cannot reach $DEST_HOST on port $SSH_PORT" >> "$LOG_FILE" + send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Cannot reach $DEST_HOST on port $SSH_PORT" "high" + exit 4 +fi + +# Rsync backup +echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] Starting rsync backup for $("$HOSTNAME_CMD")" >> "$LOG_FILE" +if LC_ALL=C "$RSYNC_CMD" -avz --stats --delete --partial --timeout=60 --exclude-from="$EXCLUDE_FILE" -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "$BACKUP_DEST:$REMOTE_DIR" >> "$LOG_FILE" 2>&1; then + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] SUCCESS: rsync completed successfully." >> "$LOG_FILE" + BACKUP_STATS=$(format_backup_stats) + send_notification "✅ Backup SUCCESS: $("$HOSTNAME_CMD")" "rsync backup completed successfully.\n\n$BACKUP_STATS" +else + EXIT_CODE=$? + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FAILED: rsync exited with status code: $EXIT_CODE" >> "$LOG_FILE" + send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "rsync failed with exit code $EXIT_CODE. Check log: $LOG_FILE" "high" + exit $EXIT_CODE +fi +echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] Run Finished" >> "$LOG_FILE" +EOF + chmod 700 "$BACKUP_SCRIPT" + print_success "Backup script created at $BACKUP_SCRIPT." + + # Add to crontab + print_info "Adding cron job for root..." + local CRON_FILE=$(mktemp) + crontab -u root -l > "$CRON_FILE" 2>/dev/null || true + if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then + print_info "Cron job for $BACKUP_SCRIPT already exists. Updating schedule..." + sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE" + fi + echo "$CRON_SCHEDULE $BACKUP_SCRIPT" >> "$CRON_FILE" + crontab -u root "$CRON_FILE" + rm -f "$CRON_FILE" + print_success "Cron job added for root: $CRON_SCHEDULE $BACKUP_SCRIPT" + log "Backup configuration completed." +} + configure_fail2ban() { print_section "Fail2Ban Configuration" @@ -1119,6 +1433,13 @@ generate_summary() { print_error "Service docker is NOT active." fi fi + if command -v tailscale >/dev/null 2>&1; then + if systemctl is-active --quiet tailscaled; then + print_success "Service tailscaled is active." + else + print_error "Service tailscaled is NOT active." + fi + fi echo -e "\n${GREEN}Server setup and hardening script has finished successfully.${NC}" echo echo -e "${YELLOW}Configuration Summary:${NC}" @@ -1126,6 +1447,27 @@ generate_summary() { echo -e " Hostname: $SERVER_NAME" echo -e " SSH Port: $SSH_PORT" echo -e " Server IP: $SERVER_IP" + if [[ -f /root/backup.sh ]]; then + local CRON_SCHEDULE=$(crontab -u root -l 2>/dev/null | grep -F "/root/backup.sh" | awk '{print $1, $2, $3, $4, $5}' || echo "Not configured") + local NOTIFICATION_STATUS="None" + local BACKUP_DEST=$(grep "^BACKUP_DEST=" /root/backup.sh | cut -d'"' -f2 || echo "Unknown") + local BACKUP_PORT=$(grep "^SSH_PORT=" /root/backup.sh | cut -d'"' -f2 || echo "Unknown") + local REMOTE_BACKUP_DIR=$(grep "^REMOTE_DIR=" /root/backup.sh | cut -d'"' -f2 || echo "Unknown") + if grep -q "NTFY_URL" /root/backup.sh; then + NOTIFICATION_STATUS="ntfy" + elif grep -q "DISCORD_WEBHOOK" /root/backup.sh; then + NOTIFICATION_STATUS="Discord" + fi + echo -e " Remote Backup: Enabled" + echo -e " - Backup Script: /root/backup.sh" + echo -e " - Destination: $BACKUP_DEST" + echo -e " - SSH Port: $BACKUP_PORT" + echo -e " - Remote Path: $REMOTE_BACKUP_DIR" + echo -e " - Cron Schedule: $CRON_SCHEDULE" + echo -e " - Notifications: $NOTIFICATION_STATUS" + else + echo -e " Remote Backup: Not configured" + fi echo echo -e "${PURPLE}Log File: ${LOG_FILE}${NC}" echo -e "${PURPLE}Backups: ${BACKUP_DIR}${NC}" @@ -1136,14 +1478,22 @@ generate_summary() { echo -e " - Time sync: chronyc tracking" echo -e " - Fail2Ban status: sudo fail2ban-client status sshd" echo -e " - Swap status: sudo swapon --show && free -h" - echo -e " - Hostname: hostnamectl" + echo -e " - Hostname: hostnamectl" if command -v docker >/dev/null 2>&1; then echo -e " - Docker status: docker ps" fi if command -v tailscale >/dev/null 2>&1; then echo -e " - Tailscale status: tailscale status" fi - print_warning "\nA reboot is required to apply all changes cleanly." + if [[ -f /root/backup.sh ]]; then + echo -e " - Remote Backup:" + echo -e " - Verify SSH key: cat /root/.ssh/id_ed25519.pub" + echo -e " - Copy key if needed: ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" + echo -e " - Test backup: sudo /root/backup.sh" + echo -e " - Check logs: sudo less /var/log/backup_*.log" + fi + print_warning "\nACTION REQUIRED: If remote backup is enabled, ensure the root SSH key is copied to the destination server." + print_warning "A reboot is required to apply all changes cleanly." if [[ $VERBOSE == true ]]; then if confirm "Reboot now?" "y"; then print_info "Rebooting now..." @@ -1171,7 +1521,7 @@ main() { trap 'handle_error $LINENO' ERR touch "$LOG_FILE" && chmod 600 "$LOG_FILE" log "Starting Debian/Ubuntu hardening script." - + print_header check_dependencies check_system @@ -1186,6 +1536,7 @@ main() { configure_time_sync install_docker install_tailscale + setup_backup configure_swap final_cleanup generate_summary From 1351b73824e355cd0b25e8c2fb628fbb1653a680 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 12:11:20 +0100 Subject: [PATCH 02/22] retry loop for rsync backup entry --- setup_harden_debian_ubuntu.sh | 38 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index e96045b..9d6a256 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -798,17 +798,36 @@ setup_backup() { print_info "Root SSH key already exists at $ROOT_SSH_KEY." fi - # Ask for backup destination details - read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de): ${NC}")" BACKUP_DEST + # Ask for backup destination details with retry logic + local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_DIR + local retry_count=0 + local max_retries=3 + while true; do + read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de): ${NC}")" BACKUP_DEST + BACKUP_DEST=${BACKUP_DEST:-user@host} + if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then + break + else + print_error "Invalid backup destination format. Expected user@host." + (( retry_count++ )) + if [[ $retry_count -lt $max_retries ]]; then + print_info "Please try again ($retry_count/$max_retries attempts)." + else + print_warning "Maximum retries ($max_retries) reached for backup destination." + if confirm "Proceed with potentially invalid destination '$BACKUP_DEST'?" "n"; then + print_info "Proceeding with user-provided destination: $BACKUP_DEST" + break + else + print_info "Resetting retries. Please enter a valid backup destination." + retry_count=0 + fi + fi + fi + done read -rp "$(echo -e "${CYAN}Enter SSH port for backup destination [22]: ${NC}")" BACKUP_PORT read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/myvps_backup/): ${NC}")" REMOTE_BACKUP_DIR - BACKUP_DEST=${BACKUP_DEST:-user@host} BACKUP_PORT=${BACKUP_PORT:-22} REMOTE_BACKUP_DIR=${REMOTE_BACKUP_DIR:-/home/backup/} - if [[ ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then - print_error "Invalid backup destination format. Expected user@host." - exit 1 - fi if ! validate_port "$BACKUP_PORT"; then print_error "Invalid SSH port. Must be between 1024 and 65535." exit 1 @@ -818,6 +837,7 @@ setup_backup() { exit 1 fi + # --- Remainder of setup_backup function unchanged --- # Optional SSH key copy attempt if confirm "Attempt to copy SSH key to the backup destination now? (Requires password)"; then if ssh-copy-id -p "$BACKUP_PORT" -s "$BACKUP_DEST" 2>/dev/null; then @@ -890,8 +910,8 @@ EOF read -rp "$(echo -e "${CYAN}Enter cron schedule (e.g., '0 3 * * *' for daily at 3 AM): ${NC}")" CRON_SCHEDULE CRON_SCHEDULE=${CRON_SCHEDULE:-0 3 * * *} if ! echo "$CRON_SCHEDULE" | grep -qE '^((\*|[0-9,-]+(/[0-9]+)?)\s*){5}$'; then - print_error "Invalid cron expression. Using default daily at 3 AM." - CRON_SCHEDULE="0 3 * * *" + print_error "Invalid cron expression. Using default daily at 3 AM." + CRON_SCHEDULE="0 3 * * *" fi # Ask for notification preference local NOTIFICATION_SETUP="none" NTFY_URL NTFY_TOPIC NTFY_TOKEN DISCORD_WEBHOOK From 4385655b72e3d10d82bb8dd974d33e04cf4d2b15 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 12:24:23 +0100 Subject: [PATCH 03/22] backup SSH port fix --- setup_harden_debian_ubuntu.sh | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 9d6a256..70995e1 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -162,6 +162,11 @@ validate_port() { [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1024 && "$port" -le 65535 ]] } +validate_backup_port() { + local port="$1" + [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1 && "$port" -le 65535 ]] +} + validate_ssh_key() { local key="$1" [[ -n "$key" && "$key" =~ ^(ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)\ ]] @@ -773,6 +778,20 @@ configure_firewall() { log "Firewall configuration completed." } +# --- Previous lines of setup_harden_debian_ubuntu.sh unchanged --- + +# --- VALIDATION FUNCTIONS (updated) --- + +validate_port() { + local port="$1" + [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1024 && "$port" -le 65535 ]] +} + +validate_backup_port() { + local port="$1" + [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1 && "$port" -le 65535 ]] +} + setup_backup() { print_section "Backup Configuration (rsync over SSH)" @@ -828,8 +847,8 @@ setup_backup() { read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/myvps_backup/): ${NC}")" REMOTE_BACKUP_DIR BACKUP_PORT=${BACKUP_PORT:-22} REMOTE_BACKUP_DIR=${REMOTE_BACKUP_DIR:-/home/backup/} - if ! validate_port "$BACKUP_PORT"; then - print_error "Invalid SSH port. Must be between 1024 and 65535." + if ! validate_backup_port "$BACKUP_PORT"; then + print_error "Invalid SSH port. Must be between 1 and 65535." exit 1 fi if [[ ! "$REMOTE_BACKUP_DIR" =~ ^/[^[:space:]]*/$ ]]; then @@ -837,7 +856,6 @@ setup_backup() { exit 1 fi - # --- Remainder of setup_backup function unchanged --- # Optional SSH key copy attempt if confirm "Attempt to copy SSH key to the backup destination now? (Requires password)"; then if ssh-copy-id -p "$BACKUP_PORT" -s "$BACKUP_DEST" 2>/dev/null; then From 768acfae546fab727405d2317f9378a6912dbbac Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 12:26:31 +0100 Subject: [PATCH 04/22] backup SSH port fix --- setup_harden_debian_ubuntu.sh | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 70995e1..5f72057 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -778,20 +778,6 @@ configure_firewall() { log "Firewall configuration completed." } -# --- Previous lines of setup_harden_debian_ubuntu.sh unchanged --- - -# --- VALIDATION FUNCTIONS (updated) --- - -validate_port() { - local port="$1" - [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1024 && "$port" -le 65535 ]] -} - -validate_backup_port() { - local port="$1" - [[ "$port" =~ ^[0-9]+$ && "$port" -ge 1 && "$port" -le 65535 ]] -} - setup_backup() { print_section "Backup Configuration (rsync over SSH)" From 9b4df7261cf22d8d8ab2ef4218fcb3ffb526e5fa Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 12:41:22 +0100 Subject: [PATCH 05/22] backup function idempoteny fix --- setup_harden_debian_ubuntu.sh | 100 +++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 5f72057..d612620 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -786,6 +786,12 @@ setup_backup() { return 0 fi + # Validate USERNAME + if [[ -z "${USERNAME:-}" || ! id "$USERNAME" >/dev/null 2>&1 ]]; then + print_error "Invalid or unset USERNAME. Please run user setup first." + exit 1 + fi + print_warning "The backup cron job will run as root to ensure access to all files in /home/$USERNAME." print_info "This requires copying the root SSH key to the remote server after script completion." @@ -796,18 +802,51 @@ setup_backup() { print_info "Generating SSH key for root..." mkdir -p "$ROOT_SSH_DIR" chmod 700 "$ROOT_SSH_DIR" - ssh-keygen -t ed25519 -f "$ROOT_SSH_KEY" -N "" -q + ssh-keygen -t ed25519 -f "$ROOT_SSH_KEY" -N "" -q || { + print_error "Failed to generate SSH key." + exit 1 + } chown -R root:root "$ROOT_SSH_DIR" print_success "Root SSH key generated." else print_info "Root SSH key already exists at $ROOT_SSH_KEY." fi + # Clean up stale backup artifacts from previous runs + local BACKUP_SCRIPT="/root/backup.sh" + local EXCLUDE_FILE="/root/backup_exclude.txt" + local CRON_FILE=$(mktemp) + if [[ -f "$BACKUP_SCRIPT" ]]; then + print_info "Found existing backup script at $BACKUP_SCRIPT. It will be replaced." + rm -f "$BACKUP_SCRIPT" || { + print_error "Failed to remove stale backup script." + exit 1 + } + fi + if [[ -f "$EXCLUDE_FILE" ]]; then + print_info "Found existing exclude file مهم at $EXCLUDE_FILE. It will be replaced." + rm -f "$EXCLUDE_FILE" || { + print_error "Failed to remove stale exclude file." + exit 1 + } + fi + # Remove existing backup cron job + crontab -u root -l > "$CRON_FILE" 2>/dev/null || true + if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then + print_info "Removing existing backup cron job..." + sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE" + crontab -u root "$CRON_FILE" || { + print_error "Failed to update crontab." + exit 1 + } + fi + rm -f "$CRON_FILE" + # Ask for backup destination details with retry logic local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_DIR local retry_count=0 local max_retries=3 - while true; do + while [[ $retry_count -lt $max_retries ]]; do read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de): ${NC}")" BACKUP_DEST BACKUP_DEST=${BACKUP_DEST:-user@host} if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then @@ -815,20 +854,34 @@ setup_backup() { else print_error "Invalid backup destination format. Expected user@host." (( retry_count++ )) - if [[ $retry_count -lt $max_retries ]]; then - print_info "Please try again ($retry_count/$max_retries attempts)." - else - print_warning "Maximum retries ($max_retries) reached for backup destination." - if confirm "Proceed with potentially invalid destination '$BACKUP_DEST'?" "n"; then - print_info "Proceeding with user-provided destination: $BACKUP_DEST" - break - else - print_info "Resetting retries. Please enter a valid backup destination." - retry_count=0 - fi - fi + print_info "Please try again ($retry_count/$max_retries attempts)." fi done + # If max retries reached, ask for confirmation + if [[ $retry_count -ge $max_retries && ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then + if confirm "Proceed with potentially invalid destination '$BACKUP_DEST'?" "n"; then + print_info "Proceeding with user-provided destination: $BACKUP_DEST" + else + print_info "Resetting retries. Please enter a valid backup destination." + retry_count=0 + while [[ $retry_count -lt $max_retries ]]; do + read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de): ${NC}")" BACKUP_DEST + BACKUP_DEST=${BACKUP_DEST:-user@host} + if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then + break + else + print_error "Invalid backup destination format. Expected user@host." + (( retry_count++ )) + print_info "Please try again ($retry_count/$max_retries attempts)." + fi + done + # Final check after retry + if [[ ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then + print_error "Invalid backup destination format after retries. Exiting." + exit 1 + fi + fi + fi read -rp "$(echo -e "${CYAN}Enter SSH port for backup destination [22]: ${NC}")" BACKUP_PORT read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/myvps_backup/): ${NC}")" REMOTE_BACKUP_DIR BACKUP_PORT=${BACKUP_PORT:-22} @@ -878,7 +931,6 @@ setup_backup() { fi # Create exclude file - local EXCLUDE_FILE="/root/backup_exclude.txt" print_info "Creating rsync exclude file at $EXCLUDE_FILE..." cat > "$EXCLUDE_FILE" <> "$EXCLUDE_FILE" done fi - chmod 600 "$EXCLUDE_FILE" + chmod 600 "$EXCLUDE_FILE" || { + print_error "Failed to set permissions on exclude file." + exit 1 + } print_success "Rsync exclude file created." # Ask for cron schedule @@ -947,7 +1002,6 @@ EOF fi # Create backup script - local BACKUP_SCRIPT="/root/backup.sh" print_info "Creating backup script at $BACKUP_SCRIPT..." cat > "$BACKUP_SCRIPT" <> "$LOG_FILE" EOF - chmod 700 "$BACKUP_SCRIPT" + chmod 700 "$BACKUP_SCRIPT" || { + print_error "Failed to set permissions on backup script." + exit 1 + } print_success "Backup script created at $BACKUP_SCRIPT." # Add to crontab print_info "Adding cron job for root..." - local CRON_FILE=$(mktemp) + CRON_FILE=$(mktemp) crontab -u root -l > "$CRON_FILE" 2>/dev/null || true if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then print_info "Cron job for $BACKUP_SCRIPT already exists. Updating schedule..." sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE" fi echo "$CRON_SCHEDULE $BACKUP_SCRIPT" >> "$CRON_FILE" - crontab -u root "$CRON_FILE" + crontab -u root "$CRON_FILE" || { + print_error "Failed to update crontab." + exit 1 + } rm -f "$CRON_FILE" print_success "Cron job added for root: $CRON_SCHEDULE $BACKUP_SCRIPT" log "Backup configuration completed." From 61ae99d5c092ef6199253b2a3d756fb3ceed7812 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:43:17 +0100 Subject: [PATCH 06/22] Update setup_harden_debian_ubuntu.sh --- setup_harden_debian_ubuntu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index d612620..1d5378d 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,7 +1,7 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4.1 | 2025-06-28 +# Version: 4.0 | 2025-06-28 # Changelog: # - v4.1: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. # - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. @@ -83,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4.1 | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4.0 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo From 557ed96fff215fbbdfc2e5c620e1e252636620b7 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:43:31 +0100 Subject: [PATCH 07/22] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 347f5d8..0556181 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Debian & Ubuntu Server Setup & Hardening Script -**Version:** 4.1 +**Version:** 4.0 **Last Updated:** 2025-06-28 @@ -195,4 +195,4 @@ If backups fail, check the following: ## [MIT](https://github.com/buildplan/setup_harden_server/blob/main/LICENSE "LICENSE") License -This script is open-source and provided "as is" without warranty. Use at your own risk. \ No newline at end of file +This script is open-source and provided "as is" without warranty. Use at your own risk. From a5f73b3cf784faae997a748ef4b8f17ef2dc7835 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 12:51:55 +0100 Subject: [PATCH 08/22] backup function fix --- setup_harden_debian_ubuntu.sh | 318 +++++++++++++++++----------------- 1 file changed, 161 insertions(+), 157 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index d612620..5fd55a1 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -778,6 +778,160 @@ configure_firewall() { log "Firewall configuration completed." } +configure_fail2ban() { + print_section "Fail2Ban Configuration" + + # Set the SSH port for Fail2Ban to monitor. + local SSH_PORTS_TO_MONITOR="$SSH_PORT" + local NEW_FAIL2BAN_CONFIG + + NEW_FAIL2BAN_CONFIG=$(mktemp) + tee "$NEW_FAIL2BAN_CONFIG" > /dev/null </dev/null 2>&1; then + print_info "Docker already installed." + return 0 + fi + print_info "Removing old container runtimes..." + apt-get remove -y -qq docker docker-engine docker.io containerd runc 2>/dev/null || true + print_info "Adding Docker's official GPG key and repository..." + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${ID} $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list + print_info "Installing Docker packages..." + if ! apt-get update -qq || ! apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then + print_error "Failed to install Docker packages." + exit 1 + fi + print_info "Adding '$USERNAME' to docker group..." + getent group docker >/dev/null || groupadd docker + if ! groups "$USERNAME" | grep -qw docker; then + usermod -aG docker "$USERNAME" + print_success "User '$USERNAME' added to docker group." + else + print_info "User '$USERNAME' is already in docker group." + fi + print_info "Configuring Docker daemon..." + local NEW_DOCKER_CONFIG + NEW_DOCKER_CONFIG=$(mktemp) + tee "$NEW_DOCKER_CONFIG" > /dev/null <&1 | tee -a "$LOG_FILE" | grep -q "Hello from Docker"; then + print_success "Docker sanity check passed." + else + print_error "Docker hello-world test failed. Please verify installation." + exit 1 + fi + print_warning "NOTE: '$USERNAME' must log out and back in to use Docker without sudo." + log "Docker installation completed." +} + +install_tailscale() { + if ! confirm "Install Tailscale VPN (Optional)?"; then + print_info "Skipping Tailscale installation." + return 0 + fi + print_section "Tailscale VPN Installation" + if command -v tailscale >/dev/null 2>&1; then + print_info "Tailscale already installed." + return 0 + fi + print_info "Installing Tailscale..." + curl -fsSL https://tailscale.com/install.sh -o /tmp/tailscale_install.sh + chmod +x /tmp/tailscale_install.sh + # Simple sanity check on the downloaded script + if ! grep -q "tailscale" /tmp/tailscale_install.sh; then + print_error "Downloaded Tailscale install script appears invalid." + rm -f /tmp/tailscale_install.sh + exit 1 + fi + if ! /tmp/tailscale_install.sh; then + print_error "Failed to install Tailscale." + rm -f /tmp/tailscale_install.sh + exit 1 + fi + rm -f /tmp/tailscale_install.sh + print_warning "ACTION REQUIRED: Run 'sudo tailscale up' after script finishes." + print_success "Tailscale installation complete." + log "Tailscale installation completed." +} + setup_backup() { print_section "Backup Configuration (rsync over SSH)" @@ -787,8 +941,12 @@ setup_backup() { fi # Validate USERNAME - if [[ -z "${USERNAME:-}" || ! id "$USERNAME" >/dev/null 2>&1 ]]; then - print_error "Invalid or unset USERNAME. Please run user setup first." + if [[ -z "$USERNAME" ]]; then + print_error "USERNAME is not set. Please run user setup first." + exit 1 + fi + if ! id "$USERNAME" >/dev/null 2>&1; then + print_error "Invalid USERNAME '$USERNAME'. User does not exist. Please run user setup first." exit 1 fi @@ -824,7 +982,7 @@ setup_backup() { } fi if [[ -f "$EXCLUDE_FILE" ]]; then - print_info "Found existing exclude file مهم at $EXCLUDE_FILE. It will be replaced." + print_info "Found existing exclude file at $EXCLUDE_FILE. It will be replaced." rm -f "$EXCLUDE_FILE" || { print_error "Failed to remove stale exclude file." exit 1 @@ -1168,160 +1326,6 @@ EOF log "Backup configuration completed." } -configure_fail2ban() { - print_section "Fail2Ban Configuration" - - # Set the SSH port for Fail2Ban to monitor. - local SSH_PORTS_TO_MONITOR="$SSH_PORT" - local NEW_FAIL2BAN_CONFIG - - NEW_FAIL2BAN_CONFIG=$(mktemp) - tee "$NEW_FAIL2BAN_CONFIG" > /dev/null </dev/null 2>&1; then - print_info "Docker already installed." - return 0 - fi - print_info "Removing old container runtimes..." - apt-get remove -y -qq docker docker-engine docker.io containerd runc 2>/dev/null || true - print_info "Adding Docker's official GPG key and repository..." - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg - chmod a+r /etc/apt/keyrings/docker.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${ID} $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list - print_info "Installing Docker packages..." - if ! apt-get update -qq || ! apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then - print_error "Failed to install Docker packages." - exit 1 - fi - print_info "Adding '$USERNAME' to docker group..." - getent group docker >/dev/null || groupadd docker - if ! groups "$USERNAME" | grep -qw docker; then - usermod -aG docker "$USERNAME" - print_success "User '$USERNAME' added to docker group." - else - print_info "User '$USERNAME' is already in docker group." - fi - print_info "Configuring Docker daemon..." - local NEW_DOCKER_CONFIG - NEW_DOCKER_CONFIG=$(mktemp) - tee "$NEW_DOCKER_CONFIG" > /dev/null <&1 | tee -a "$LOG_FILE" | grep -q "Hello from Docker"; then - print_success "Docker sanity check passed." - else - print_error "Docker hello-world test failed. Please verify installation." - exit 1 - fi - print_warning "NOTE: '$USERNAME' must log out and back in to use Docker without sudo." - log "Docker installation completed." -} - -install_tailscale() { - if ! confirm "Install Tailscale VPN (Optional)?"; then - print_info "Skipping Tailscale installation." - return 0 - fi - print_section "Tailscale VPN Installation" - if command -v tailscale >/dev/null 2>&1; then - print_info "Tailscale already installed." - return 0 - fi - print_info "Installing Tailscale..." - curl -fsSL https://tailscale.com/install.sh -o /tmp/tailscale_install.sh - chmod +x /tmp/tailscale_install.sh - # Simple sanity check on the downloaded script - if ! grep -q "tailscale" /tmp/tailscale_install.sh; then - print_error "Downloaded Tailscale install script appears invalid." - rm -f /tmp/tailscale_install.sh - exit 1 - fi - if ! /tmp/tailscale_install.sh; then - print_error "Failed to install Tailscale." - rm -f /tmp/tailscale_install.sh - exit 1 - fi - rm -f /tmp/tailscale_install.sh - print_warning "ACTION REQUIRED: Run 'sudo tailscale up' after script finishes." - print_success "Tailscale installation complete." - log "Tailscale installation completed." -} - configure_swap() { if [[ $IS_CONTAINER == true ]]; then print_info "Swap configuration skipped in container." From 6f94bbeb6d4bccbbe24c47529ee6e7518844ca68 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 12:58:51 +0100 Subject: [PATCH 09/22] fix --- setup_harden_debian_ubuntu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index ea0c5c0..235efcd 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,7 +1,7 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4.0 | 2025-06-28 +# Version: 4-rc | 2025-06-28 # Changelog: # - v4.1: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. # - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. @@ -83,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4.0 | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4-rc | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo From 1df92995eca59f907c1fc91c3d590955e03a0acc Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 13:17:23 +0100 Subject: [PATCH 10/22] fix --- setup_harden_debian_ubuntu.sh | 46 +++++++++++++++-------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 235efcd..33df2c6 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1004,42 +1004,34 @@ setup_backup() { local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_DIR local retry_count=0 local max_retries=3 - while [[ $retry_count -lt $max_retries ]]; do - read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de): ${NC}")" BACKUP_DEST - BACKUP_DEST=${BACKUP_DEST:-user@host} + while true; do + if ! read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de, or press Enter to skip): ${NC}")" BACKUP_DEST; then + print_error "Failed to read backup destination input." + continue + fi + if [[ -z "$BACKUP_DEST" ]]; then + print_info "Backup destination not provided. Skipping backup configuration." + return 0 + fi if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then break else print_error "Invalid backup destination format. Expected user@host." (( retry_count++ )) - print_info "Please try again ($retry_count/$max_retries attempts)." - fi - done - # If max retries reached, ask for confirmation - if [[ $retry_count -ge $max_retries && ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then - if confirm "Proceed with potentially invalid destination '$BACKUP_DEST'?" "n"; then - print_info "Proceeding with user-provided destination: $BACKUP_DEST" - else - print_info "Resetting retries. Please enter a valid backup destination." - retry_count=0 - while [[ $retry_count -lt $max_retries ]]; do - read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de): ${NC}")" BACKUP_DEST - BACKUP_DEST=${BACKUP_DEST:-user@host} - if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then - break + if [[ $retry_count -lt $max_retries ]]; then + print_info "Please try again ($retry_count/$max_retries attempts)." + else + if confirm "Retry again or skip backup configuration?" "y"; then + retry_count=0 + print_info "Resetting retries. Please enter a valid backup destination." else - print_error "Invalid backup destination format. Expected user@host." - (( retry_count++ )) - print_info "Please try again ($retry_count/$max_retries attempts)." + print_info "Skipping backup configuration." + return 0 fi - done - # Final check after retry - if [[ ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then - print_error "Invalid backup destination format after retries. Exiting." - exit 1 fi fi - fi + done + read -rp "$(echo -e "${CYAN}Enter SSH port for backup destination [22]: ${NC}")" BACKUP_PORT read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/myvps_backup/): ${NC}")" REMOTE_BACKUP_DIR BACKUP_PORT=${BACKUP_PORT:-22} From 2204c3b23058a508cddc495dbf9c09532b1692d3 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 13:35:06 +0100 Subject: [PATCH 11/22] more fixing --- setup_harden_debian_ubuntu.sh | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 33df2c6..f220a90 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1122,6 +1122,7 @@ EOF print_error "Invalid cron expression. Using default daily at 3 AM." CRON_SCHEDULE="0 3 * * *" fi + # Ask for notification preference local NOTIFICATION_SETUP="none" NTFY_URL NTFY_TOPIC NTFY_TOKEN DISCORD_WEBHOOK if confirm "Enable backup notifications?"; then @@ -1163,7 +1164,6 @@ umask 077 # Configuration RSYNC_CMD="\$(command -v rsync)" -CURL_CMD="\$(command -v curl)" HOSTNAME_CMD="\$(command -v hostname)" DATE_CMD="\$(command -v date)" STAT_CMD="\$(command -v stat)" @@ -1183,6 +1183,11 @@ LOG_FILE="/var/log/backup_\$(date +%Y%m%d_%H%M%S).log" MAX_LOG_SIZE=10485760 # 10 MB NOTIFICATION_SETUP="$NOTIFICATION_SETUP" EOF + if [[ "$NOTIFICATION_SETUP" != "none" ]]; then + cat >> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" < /dev/null 2>> "$LOG_FILE" EOF @@ -1246,7 +1251,18 @@ format_backup_stats() { } # Dependency check -for cmd in "$RSYNC_CMD" "$CURL_CMD" "$NC_CMD" "$AWK_CMD" "$NUMFMT_CMD" "$GREP_CMD" "$HOSTNAME_CMD" "$DATE_CMD" "$STAT_CMD" "$MV_CMD" "$TOUCH_CMD"; do +EOF + if [[ "$NOTIFICATION_SETUP" != "none" ]]; then + cat >> "$BACKUP_SCRIPT" <<'EOF' +if [[ -n "${CURL_CMD:-}" ]] && ! command -v "$CURL_CMD" &>/dev/null; then + echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: curl not found" >> "$LOG_FILE" + send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "curl not found" "high" + exit 10 +fi +EOF + fi + cat >> "$BACKUP_SCRIPT" <<'EOF' +for cmd in "$RSYNC_CMD" "$NC_CMD" "$AWK_CMD" "$NUMFMT_CMD" "$GREP_CMD" "$HOSTNAME_CMD" "$DATE_CMD" "$STAT_CMD" "$MV_CMD" "$TOUCH_CMD"; do if ! command -v "$cmd" &>/dev/null; then echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Required command not found at '$cmd'" >> "$LOG_FILE" send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Required command not found at '$cmd'" "high" From cc25372d90819988f2882f57dcd7b20e7d65501f Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 14:08:37 +0100 Subject: [PATCH 12/22] more more fixing --- setup_harden_debian_ubuntu.sh | 64 ++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index f220a90..15c7912 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1045,28 +1045,60 @@ setup_backup() { exit 1 fi - # Optional SSH key copy attempt - if confirm "Attempt to copy SSH key to the backup destination now? (Requires password)"; then - if ssh-copy-id -p "$BACKUP_PORT" -s "$BACKUP_DEST" 2>/dev/null; then - print_success "SSH key copied successfully." - else - print_warning "SSH key copy failed. You must manually copy the key later." - fi - fi + # Handle SSH key copying options + echo -e "${CYAN}Choose how to copy the root SSH key to the backup destination:${NC}" + echo -e " 1) Automate with password (requires sshpass)" + echo -e " 2) Manual copy (run ssh-copy-id later)" + echo -e " 3) Skip (test connection or copy manually later)" + read -rp "$(echo -e "${CYAN}Enter choice (1-3): ${NC}")" KEY_COPY_CHOICE + case "$KEY_COPY_CHOICE" in + 1) + # Ensure sshpass is installed + if ! command -v sshpass >/dev/null 2>&1; then + print_info "Installing sshpass for automated key copying..." + if ! apt-get install -y -qq sshpass; then + print_error "Failed to install sshpass. Falling back to manual copy instructions." + KEY_COPY_CHOICE=2 + else + print_success "sshpass installed." + fi + fi + if [[ "$KEY_COPY_CHOICE" == "1" ]]; then + read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD + echo + print_info "Attempting automated SSH key copy..." + if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" -s "$BACKUP_DEST" 2>/dev/null; then + print_success "SSH key copied successfully." + else + print_error "Automated SSH key copy failed." + print_warning "Falling back to manual copy instructions." + KEY_COPY_CHOICE=2 + fi + fi + ;; + 2) + print_info "Manual SSH key copy selected." + ;; + 3|*) + print_info "Skipping SSH key copy." + ;; + esac - # Display SSH key copy instructions - print_warning "ACTION REQUIRED: If not already done, copy the root SSH key to the backup destination to enable backups." - echo -e "${YELLOW}Root public key:${NC}" - cat "$ROOT_SSH_KEY.pub" - echo -e "${CYAN}Run this command on your local machine or another terminal:${NC}" - echo -e " ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" - print_info "You can copy the SSH key later, but the backup cron job will fail until this is done." + # Display SSH key copy instructions if not automated + if [[ "$KEY_COPY_CHOICE" != "1" || ! -f /root/.ssh/known_hosts || ! grep -q "$BACKUP_DEST" /root/.ssh/known_hosts ]]; then + print_warning "ACTION REQUIRED: If not already done, copy the root SSH key to the backup destination to enable backups." + echo -e "${YELLOW}Root public key:${NC}" + cat "$ROOT_SSH_KEY.pub" + echo -e "${CYAN}Run this command on your local machine or another terminal:${NC}" + echo -e " ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" + print_info "You can copy the SSH key later, but the backup cron job will fail until this is done." + fi # Optional SSH connection test if confirm "Test SSH connection to the backup destination (optional)?"; then print_info "Testing connection (timeout: 5 seconds)..." DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2) - if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=5 "$BACKUP_DEST" exit 2>/dev/null; then + if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=5 "$BACKUP_DEST" true 2>/dev/null; then print_success "SSH connection successful!" else print_error "SSH connection failed." From cd85444ff1511aee0f385130ac15510ad6f92368 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 14:51:26 +0100 Subject: [PATCH 13/22] more fixing again --- setup_harden_debian_ubuntu.sh | 563 ++++++++++++---------------------- 1 file changed, 202 insertions(+), 361 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 15c7912..1efdadf 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -937,432 +937,273 @@ setup_backup() { if ! confirm "Configure rsync-based backups to a remote SSH server?"; then print_info "Skipping backup configuration." + log "Backup configuration skipped by user." return 0 fi - # Validate USERNAME - if [[ -z "$USERNAME" ]]; then - print_error "USERNAME is not set. Please run user setup first." - exit 1 - fi - if ! id "$USERNAME" >/dev/null 2>&1; then - print_error "Invalid USERNAME '$USERNAME'. User does not exist. Please run user setup first." - exit 1 + # --- 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 fi - print_warning "The backup cron job will run as root to ensure access to all files in /home/$USERNAME." - print_info "This requires copying the root SSH key to the remote server after script completion." - - # Generate SSH key for root (if not exists) local ROOT_SSH_DIR="/root/.ssh" local ROOT_SSH_KEY="$ROOT_SSH_DIR/id_ed25519" + local BACKUP_SCRIPT_PATH="/root/run_backup.sh" + local EXCLUDE_FILE_PATH="/root/rsync_exclude.txt" + local CRON_MARKER="#-*- managed by setup_harden script -*-" + + # --- Generate SSH Key for Root --- if [[ ! -f "$ROOT_SSH_KEY" ]]; then - print_info "Generating SSH key for root..." - mkdir -p "$ROOT_SSH_DIR" - chmod 700 "$ROOT_SSH_DIR" - ssh-keygen -t ed25519 -f "$ROOT_SSH_KEY" -N "" -q || { - print_error "Failed to generate SSH key." - exit 1 - } + 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 chown -R root:root "$ROOT_SSH_DIR" - print_success "Root SSH key generated." + print_success "Root SSH key generated at $ROOT_SSH_KEY" + log "Generated root SSH key for backups." else - print_info "Root SSH key already exists at $ROOT_SSH_KEY." + print_info "Existing root SSH key found at $ROOT_SSH_KEY." fi - # Clean up stale backup artifacts from previous runs - local BACKUP_SCRIPT="/root/backup.sh" - local EXCLUDE_FILE="/root/backup_exclude.txt" - local CRON_FILE=$(mktemp) - if [[ -f "$BACKUP_SCRIPT" ]]; then - print_info "Found existing backup script at $BACKUP_SCRIPT. It will be replaced." - rm -f "$BACKUP_SCRIPT" || { - print_error "Failed to remove stale backup script." - exit 1 - } - fi - if [[ -f "$EXCLUDE_FILE" ]]; then - print_info "Found existing exclude file at $EXCLUDE_FILE. It will be replaced." - rm -f "$EXCLUDE_FILE" || { - print_error "Failed to remove stale exclude file." - exit 1 - } - fi - # Remove existing backup cron job - crontab -u root -l > "$CRON_FILE" 2>/dev/null || true - if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then - print_info "Removing existing backup cron job..." - sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE" - crontab -u root "$CRON_FILE" || { - print_error "Failed to update crontab." - exit 1 - } - fi - rm -f "$CRON_FILE" + # --- Collect Backup Destination Details --- + local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH + read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${NC}")" BACKUP_DEST + read -rp "$(echo -e "${CYAN}Enter destination SSH port (Hetzner uses 23) [22]: ${NC}")" BACKUP_PORT + read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/my_backups/): ${NC}")" REMOTE_BACKUP_PATH - # Ask for backup destination details with retry logic - local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_DIR - local retry_count=0 - local max_retries=3 - while true; do - if ! read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., user@host or u45555-sub4@u45555.your-storagebox.de, or press Enter to skip): ${NC}")" BACKUP_DEST; then - print_error "Failed to read backup destination input." - continue - fi - if [[ -z "$BACKUP_DEST" ]]; then - print_info "Backup destination not provided. Skipping backup configuration." - return 0 - fi - if [[ "$BACKUP_DEST" =~ ^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+$ ]]; then - break - else - print_error "Invalid backup destination format. Expected user@host." - (( retry_count++ )) - if [[ $retry_count -lt $max_retries ]]; then - print_info "Please try again ($retry_count/$max_retries attempts)." - else - if confirm "Retry again or skip backup configuration?" "y"; then - retry_count=0 - print_info "Resetting retries. Please enter a valid backup destination." - else - print_info "Skipping backup configuration." - return 0 - fi - fi - fi - done - - read -rp "$(echo -e "${CYAN}Enter SSH port for backup destination [22]: ${NC}")" BACKUP_PORT - read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/myvps_backup/): ${NC}")" REMOTE_BACKUP_DIR + # Validate inputs BACKUP_PORT=${BACKUP_PORT:-22} - REMOTE_BACKUP_DIR=${REMOTE_BACKUP_DIR:-/home/backup/} - if ! validate_backup_port "$BACKUP_PORT"; then - print_error "Invalid SSH port. Must be between 1 and 65535." - exit 1 + if [[ ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$ ]]; then + print_error "Invalid backup destination format. Expected user@host." + return 1 fi - if [[ ! "$REMOTE_BACKUP_DIR" =~ ^/[^[:space:]]*/$ ]]; then + if [[ ! "$REMOTE_BACKUP_PATH" =~ ^/[^[:space:]]*/$ ]]; then print_error "Invalid remote backup path. Must start and end with '/' and contain no spaces." - exit 1 + return 1 fi - - # Handle SSH key copying options - echo -e "${CYAN}Choose how to copy the root SSH key to the backup destination:${NC}" - echo -e " 1) Automate with password (requires sshpass)" - echo -e " 2) Manual copy (run ssh-copy-id later)" - echo -e " 3) Skip (test connection or copy manually later)" - read -rp "$(echo -e "${CYAN}Enter choice (1-3): ${NC}")" KEY_COPY_CHOICE - case "$KEY_COPY_CHOICE" in - 1) - # Ensure sshpass is installed - if ! command -v sshpass >/dev/null 2>&1; then - print_info "Installing sshpass for automated key copying..." - if ! apt-get install -y -qq sshpass; then - print_error "Failed to install sshpass. Falling back to manual copy instructions." - KEY_COPY_CHOICE=2 - else - print_success "sshpass installed." - fi - fi - if [[ "$KEY_COPY_CHOICE" == "1" ]]; then - read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD - echo - print_info "Attempting automated SSH key copy..." - if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" -s "$BACKUP_DEST" 2>/dev/null; then - print_success "SSH key copied successfully." - else - print_error "Automated SSH key copy failed." - print_warning "Falling back to manual copy instructions." - KEY_COPY_CHOICE=2 - fi - fi - ;; - 2) - print_info "Manual SSH key copy selected." - ;; - 3|*) - print_info "Skipping SSH key copy." - ;; - esac - - # Display SSH key copy instructions if not automated - if [[ "$KEY_COPY_CHOICE" != "1" || ! -f /root/.ssh/known_hosts || ! grep -q "$BACKUP_DEST" /root/.ssh/known_hosts ]]; then - print_warning "ACTION REQUIRED: If not already done, copy the root SSH key to the backup destination to enable backups." - echo -e "${YELLOW}Root public key:${NC}" - cat "$ROOT_SSH_KEY.pub" - echo -e "${CYAN}Run this command on your local machine or another terminal:${NC}" - echo -e " ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" - print_info "You can copy the SSH key later, but the backup cron job will fail until this is done." + if ! [[ "$BACKUP_PORT" =~ ^[0-9]+$ && "$BACKUP_PORT" -ge 1 && "$BACKUP_PORT" -le 65535 ]]; then + print_error "Invalid SSH port. Must be between 1 and 65535." + return 1 fi + print_info "Backup target set to: ${BACKUP_DEST}:${REMOTE_BACKUP_PATH} on port ${BACKUP_PORT}" - # Optional SSH connection test - if confirm "Test SSH connection to the backup destination (optional)?"; then - print_info "Testing connection (timeout: 5 seconds)..." - DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2) - if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=5 "$BACKUP_DEST" true 2>/dev/null; then - print_success "SSH connection successful!" - else - print_error "SSH connection failed." - print_info "Verify the following:" - print_info " 1. The key was copied: ${YELLOW}ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST${NC}" - if command -v nc >/dev/null 2>&1; then - print_info " 2. Port $BACKUP_PORT is open: ${YELLOW}nc -zv $DEST_HOST $BACKUP_PORT${NC}" + # --- Handle SSH Key Copy --- + echo -e "${CYAN}Choose how to copy the root SSH key:${NC}" + echo -e " 1) Automate with password (requires sshpass, less secure)" + echo -e " 2) Manual copy (recommended)" + read -rp "$(echo -e "${CYAN}Enter choice (1-2) [2]: ${NC}")" KEY_COPY_CHOICE + 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..." + apt-get install -y -qq sshpass || { + print_warning "Failed to install sshpass. Falling back to manual copy." + KEY_COPY_CHOICE=2 + } + fi + if [[ "$KEY_COPY_CHOICE" == "1" ]]; then + read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD + echo + if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" "$BACKUP_DEST" 2>/dev/null; then + print_success "SSH key copied successfully." else - print_info " 2. Port $BACKUP_PORT is open: ${YELLOW}Contact your network admin or try 'telnet $DEST_HOST $BACKUP_PORT'${NC}" + print_error "Automated SSH key copy failed. Please copy manually." + KEY_COPY_CHOICE=2 fi fi fi + if [[ "$KEY_COPY_CHOICE" == "2" ]]; then + print_warning "ACTION REQUIRED: Copy the root SSH key to the backup destination." + echo -e "This will allow the root user to connect without a password for automated backups." + echo -e "${YELLOW}The root user's public key is:${NC}" + cat "${ROOT_SSH_KEY}.pub" + echo + echo -e "${YELLOW}Run the following command from this server's terminal to copy the key:${NC}" + echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" \"${BACKUP_DEST}\"${NC}" + echo + fi - # Create exclude file - print_info "Creating rsync exclude file at $EXCLUDE_FILE..." - cat > "$EXCLUDE_FILE" </dev/null; then + print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open." + print_info " - Copy key: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" \"$BACKUP_DEST\"" + print_info " - Check port: nc -zv $(echo \"$BACKUP_DEST\" | cut -d'@' -f2) \"$BACKUP_PORT\"" + else + print_success "SSH connection to backup destination successful!" + fi + fi + + # --- Create Exclude File --- + print_info "Creating rsync exclude file at $EXCLUDE_FILE_PATH..." + tee "$EXCLUDE_FILE_PATH" > /dev/null <<'EOF' +# Default Exclusions .cache/ -.cloud-locale-test.skip -.config/ -.lesshst .docker/ .local/ -.profile -.selected_editor -.ssh/ -.sudo_as_admin_successful -.wget-hsts -.wg-easy/ -*~ +.npm/ +.vscode-server/ +*.log *.tmp +node_modules/ +.bash_history +.wget-hsts EOF - if confirm "Add additional files/directories to exclude from backup?"; then - read -rp "$(echo -e "${CYAN}Enter directories/files to exclude (space-separated, e.g., .cache/ .log): ${NC}")" EXCLUDE_ITEMS - for item in $EXCLUDE_ITEMS; do - echo "$item" >> "$EXCLUDE_FILE" + if confirm "Add more directories/files to the exclude list?"; then + # Use read -a to handle spaces in paths correctly + read -rp "$(echo -e "${CYAN}Enter items separated by spaces (e.g., Videos/ 'My Documents/'): ${NC}")" -a extra_excludes + for item in "${extra_excludes[@]}"; do + echo "$item" >> "$EXCLUDE_FILE_PATH" done fi - chmod 600 "$EXCLUDE_FILE" || { - print_error "Failed to set permissions on exclude file." - exit 1 - } + chmod 600 "$EXCLUDE_FILE_PATH" print_success "Rsync exclude file created." - # Ask for cron schedule - print_info "Configuring cron schedule for backups..." - read -rp "$(echo -e "${CYAN}Enter cron schedule (e.g., '0 3 * * *' for daily at 3 AM): ${NC}")" CRON_SCHEDULE - CRON_SCHEDULE=${CRON_SCHEDULE:-0 3 * * *} - if ! echo "$CRON_SCHEDULE" | grep -qE '^((\*|[0-9,-]+(/[0-9]+)?)\s*){5}$'; then - print_error "Invalid cron expression. Using default daily at 3 AM." - CRON_SCHEDULE="0 3 * * *" + # --- Collect Cron Schedule --- + local CRON_SCHEDULE + print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help." + read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [5 3 * * *]: ${NC}")" CRON_SCHEDULE + CRON_SCHEDULE=${CRON_SCHEDULE:-"5 3 * * *"} + if ! echo "$CRON_SCHEDULE" | grep -qE '^(((\*\/)?[0-9,-]+|\*)\s){4}((\*\/)?[0-9,-]+|\*)$'; then + print_error "Invalid cron expression. Using default: 5 3 * * *" + CRON_SCHEDULE="5 3 * * *" fi - # Ask for notification preference - local NOTIFICATION_SETUP="none" NTFY_URL NTFY_TOPIC NTFY_TOKEN DISCORD_WEBHOOK - if confirm "Enable backup notifications?"; then - echo -e "${CYAN}Choose notification method:${NC}" - echo -e " 1) ntfy" - echo -e " 2) Discord" - echo -e " 3) None" - read -rp "$(echo -e "${CYAN}Enter choice (1-3): ${NC}")" NOTIFICATION_CHOICE - case "$NOTIFICATION_CHOICE" in - 1) - read -rp "$(echo -e "${CYAN}Enter ntfy URL (e.g., https://ntfy.sh): ${NC}")" NTFY_URL - read -rp "$(echo -e "${CYAN}Enter ntfy topic: ${NC}")" NTFY_TOPIC - read -rp "$(echo -e "${CYAN}Enter ntfy token (optional, press Enter to skip): ${NC}")" NTFY_TOKEN - NTFY_URL=${NTFY_URL:-https://ntfy.sh} - NTFY_TOPIC=${NTFY_TOPIC:-vps-backups} - NOTIFICATION_SETUP="ntfy" - ;; - 2) - read -rp "$(echo -e "${CYAN}Enter Discord webhook URL: ${NC}")" DISCORD_WEBHOOK - if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then - print_error "Invalid Discord webhook URL." - exit 1 - fi - NOTIFICATION_SETUP="discord" - ;; - *) NOTIFICATION_SETUP="none" ;; - esac + # --- Collect Notification Details --- + local NOTIFICATION_SETUP="none" NTFY_URL="" NTFY_TOKEN="" DISCORD_WEBHOOK="" + if confirm "Enable backup status notifications?"; then + echo -e "${CYAN}Select notification method: 1) ntfy.sh 2) Discord [1]: ${NC}" + read -r n_choice + if [[ "$n_choice" == "2" ]]; then + NOTIFICATION_SETUP="discord" + read -rp "$(echo -e "${CYAN}Enter Discord Webhook URL: ${NC}")" DISCORD_WEBHOOK + if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then + print_error "Invalid Discord webhook URL." + return 1 + fi + else + NOTIFICATION_SETUP="ntfy" + read -rp "$(echo -e "${CYAN}Enter ntfy URL/topic (e.g., https://ntfy.sh/my-backups): ${NC}")" NTFY_URL + read -rp "$(echo -e "${CYAN}Enter ntfy Access Token (optional): ${NC}")" NTFY_TOKEN + if [[ ! "$NTFY_URL" =~ ^https?:// ]]; then + print_error "Invalid ntfy URL. Must start with http:// or https://." + return 1 + fi + fi fi - # Create backup script - print_info "Creating backup script at $BACKUP_SCRIPT..." - cat > "$BACKUP_SCRIPT" < /dev/null <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <> "$BACKUP_SCRIPT" <<'EOF' -# Notification function + # Use a quoted heredoc to write the script logic literally, preventing variable expansion + tee -a "$BACKUP_SCRIPT_PATH" > /dev/null <<'EOF' + +# --- SCRIPT LOGIC --- send_notification() { - local title="$1" + local status="$1" local message="$2" - local priority="${3:-default}" - local color=65280 # Green for success - if [[ "$title" == *"FAILED"* ]]; then - color=16711680 # Red for failure + local title + local color + + if [[ "$status" == "SUCCESS" ]]; then + title="✅ Backup SUCCESS: $HOSTNAME" + color=3066993 # Green + else + title="❌ Backup FAILED: $HOSTNAME" + color=15158332 # Red fi -EOF + if [[ "$NOTIFICATION_SETUP" == "ntfy" ]]; then - cat >> "$BACKUP_SCRIPT" < /dev/null 2>> "$LOG_FILE" -EOF + curl -s -H "Title: $title" \ + ${NTFY_TOKEN:+-H "Authorization: Bearer $NTFY_TOKEN"} \ + -d "$message" "$NTFY_URL" > /dev/null 2>&1 elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then - cat >> "$BACKUP_SCRIPT" < /dev/null 2>> "$LOG_FILE" -EOF - else - cat >> "$BACKUP_SCRIPT" <<'EOF' - : # No notifications configured -EOF - fi - cat >> "$BACKUP_SCRIPT" <<'EOF' -} - -# Format backup stats -format_backup_stats() { - local stats_line - stats_line=$("$GREP_CMD" 'Total transferred file size' "$LOG_FILE" | tail -n 1) - if [ -n "$stats_line" ]; then - local bytes - bytes=$(echo "$stats_line" | "$AWK_CMD" '{gsub(/,/, ""); print $5}') - if [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 ]]; then - local human_readable - human_readable=$("$NUMFMT_CMD" --to=iec-i --suffix=B --format="%.2f" "$bytes") - printf "Data Transferred: $human_readable" - else - printf "Data Transferred: 0 B (No changes)" - fi - else - printf "See log for statistics." + # Escape JSON special characters in the message + local escaped_message + escaped_message=$(echo "$message" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g' | sed ':a;N;$!ba;s/\n/\\n/g') + local json_payload + json_payload=$(printf '{"embeds": [{"title": "%s", "description": "%s", "color": %d}]}' "$title" "$escaped_message" "$color") + curl -s -H "Content-Type: application/json" -d "$json_payload" "$DISCORD_WEBHOOK" > /dev/null 2>&1 fi } -# Dependency check -EOF - if [[ "$NOTIFICATION_SETUP" != "none" ]]; then - cat >> "$BACKUP_SCRIPT" <<'EOF' -if [[ -n "${CURL_CMD:-}" ]] && ! command -v "$CURL_CMD" &>/dev/null; then - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: curl not found" >> "$LOG_FILE" - send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "curl not found" "high" - exit 10 -fi -EOF - fi - cat >> "$BACKUP_SCRIPT" <<'EOF' -for cmd in "$RSYNC_CMD" "$NC_CMD" "$AWK_CMD" "$NUMFMT_CMD" "$GREP_CMD" "$HOSTNAME_CMD" "$DATE_CMD" "$STAT_CMD" "$MV_CMD" "$TOUCH_CMD"; do +# --- DEPENDENCY CHECKS --- +# Corrected dependency check loop +for cmd in rsync curl numfmt awk flock; do if ! command -v "$cmd" &>/dev/null; then - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Required command not found at '$cmd'" >> "$LOG_FILE" - send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Required command not found at '$cmd'" "high" + echo "FATAL: Required command '$cmd' not found. Please install it." >> "$LOG_FILE" + send_notification "FAILURE" "FATAL: Required command '$cmd' not found." exit 10 fi done -# Pre-flight checks -if [[ ! -f "$EXCLUDE_FILE" ]]; then - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Exclude file not found at $EXCLUDE_FILE" >> "$LOG_FILE" - send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Exclude file not found at $EXCLUDE_FILE" "high" - exit 3 -fi -if [[ "$LOCAL_DIR" != */ ]]; then - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: LOCAL_DIR must end with a trailing slash" >> "$LOG_FILE" - send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "LOCAL_DIR must end with a trailing slash" "high" - exit 2 -fi +# Ensure single instance +exec 200>"$LOCK_FILE" +flock -n 200 || { echo "Backup already running."; exit 1; } # Log rotation -if [ -f "$LOG_FILE" ] && [ "$("$STAT_CMD" -c%s "$LOG_FILE")" -gt "$MAX_LOG_SIZE" ]; then - "$MV_CMD" "$LOG_FILE" "${LOG_FILE}.$("$DATE_CMD" +%Y%m%d_%H%M%S)" - "$TOUCH_CMD" "$LOG_FILE" +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 -# Network connectivity check -DEST_HOST=$(echo "$BACKUP_DEST" | cut -d'@' -f2) -if ! "$NC_CMD" -z -w 5 "$DEST_HOST" "$SSH_PORT"; then - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FATAL: Cannot reach $DEST_HOST on port $SSH_PORT" >> "$LOG_FILE" - send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "Cannot reach $DEST_HOST on port $SSH_PORT" "high" - exit 4 -fi +echo "--- Starting Backup at $(date) ---" >> "$LOG_FILE" -# Rsync backup -echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] Starting rsync backup for $("$HOSTNAME_CMD")" >> "$LOG_FILE" -if LC_ALL=C "$RSYNC_CMD" -avz --stats --delete --partial --timeout=60 --exclude-from="$EXCLUDE_FILE" -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "$BACKUP_DEST:$REMOTE_DIR" >> "$LOG_FILE" 2>&1; then - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] SUCCESS: rsync completed successfully." >> "$LOG_FILE" - BACKUP_STATS=$(format_backup_stats) - send_notification "✅ Backup SUCCESS: $("$HOSTNAME_CMD")" "rsync backup completed successfully.\n\n$BACKUP_STATS" +# Run rsync +# The '-R' option preserves the full path from the source +rsync_output=$(rsync -avzR --delete --stats --exclude-from="$EXCLUDE_FILE" \ + -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "${REMOTE_DEST}:${REMOTE_PATH}" 2>&1) + +rsync_exit_code=$? +echo "$rsync_output" >> "$LOG_FILE" + +# Check status and send notification +if [[ $rsync_exit_code -eq 0 ]]; then + data_transferred=$(echo "$rsync_output" | grep 'Total transferred file size' | awk '{print $5}' | sed 's/,//g') + human_readable=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$data_transferred") + message="Backup completed successfully.\\nData Transferred: ${human_readable}" + send_notification "SUCCESS" "$message" + echo "--- Backup SUCCEEDED at $(date) ---" >> "$LOG_FILE" else - EXIT_CODE=$? - echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] FAILED: rsync exited with status code: $EXIT_CODE" >> "$LOG_FILE" - send_notification "❌ Backup FAILED: $("$HOSTNAME_CMD")" "rsync failed with exit code $EXIT_CODE. Check log: $LOG_FILE" "high" - exit $EXIT_CODE + message="rsync failed with exit code ${rsync_exit_code}. Check log for details." + send_notification "FAILURE" "$message" + echo "--- Backup FAILED at $(date) ---" >> "$LOG_FILE" fi -echo "[$("$DATE_CMD" '+%Y-%m-%d %H:%M:%S')] Run Finished" >> "$LOG_FILE" EOF - chmod 700 "$BACKUP_SCRIPT" || { - print_error "Failed to set permissions on backup script." - exit 1 - } - print_success "Backup script created at $BACKUP_SCRIPT." - # Add to crontab - print_info "Adding cron job for root..." - CRON_FILE=$(mktemp) - crontab -u root -l > "$CRON_FILE" 2>/dev/null || true - if grep -q "$BACKUP_SCRIPT" "$CRON_FILE"; then - print_info "Cron job for $BACKUP_SCRIPT already exists. Updating schedule..." - sed -i "\|$BACKUP_SCRIPT|d" "$CRON_FILE" - fi - echo "$CRON_SCHEDULE $BACKUP_SCRIPT" >> "$CRON_FILE" - crontab -u root "$CRON_FILE" || { - print_error "Failed to update crontab." - exit 1 - } - rm -f "$CRON_FILE" - print_success "Cron job added for root: $CRON_SCHEDULE $BACKUP_SCRIPT" + chmod 700 "$BACKUP_SCRIPT_PATH" + print_success "Backup script created." + + # --- Configure Cron Job --- + print_info "Configuring root cron job..." + # This robustly removes the old job (if any) and adds the new one + (crontab -u root -l 2>/dev/null | grep -v "$CRON_MARKER"; echo "$CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER") | crontab -u root - + + print_success "Backup cron job scheduled: $CRON_SCHEDULE" log "Backup configuration completed." } From b99b6a1f5bf180dd4c478300355aed5e5c47ce89 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 15:25:37 +0100 Subject: [PATCH 14/22] backup function fix --- setup_harden_debian_ubuntu.sh | 45 ++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 1efdadf..da98e91 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,9 +1,9 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4-rc | 2025-06-28 +# Version: 4-rc1 | 2025-06-28 # Changelog: -# - v4.1: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. +# - v4.0: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. # - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. # - v4.0: Enhanced generate_summary to include backup details (script path, cron schedule, notifications). # - v4.0: Tested on Debian 12, Ubuntu 20.04, 22.04, 24.04, and 24.10 (experimental) at DigitalOcean, Oracle Cloud, Netcup, Hetzner, and local VMs. @@ -83,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4-rc | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4-rc1 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo @@ -967,7 +967,7 @@ setup_backup() { fi # --- Collect Backup Destination Details --- - local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH + local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH SSH_COPY_ID_FLAGS="" read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${NC}")" BACKUP_DEST read -rp "$(echo -e "${CYAN}Enter destination SSH port (Hetzner uses 23) [22]: ${NC}")" BACKUP_PORT read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/my_backups/): ${NC}")" REMOTE_BACKUP_PATH @@ -988,6 +988,12 @@ setup_backup() { fi print_info "Backup target set to: ${BACKUP_DEST}:${REMOTE_BACKUP_PATH} on port ${BACKUP_PORT}" + # --- FIX: Hetzner Specific Handling --- + if confirm "Is this backup destination a Hetzner Storage Box (requires special handling)?"; then + SSH_COPY_ID_FLAGS="-s" # Add the -s flag required by Hetzner + print_info "Hetzner Storage Box mode enabled. Using '-s' for ssh-copy-id." + fi + # --- Handle SSH Key Copy --- echo -e "${CYAN}Choose how to copy the root SSH key:${NC}" echo -e " 1) Automate with password (requires sshpass, less secure)" @@ -1005,7 +1011,7 @@ setup_backup() { if [[ "$KEY_COPY_CHOICE" == "1" ]]; then read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD echo - if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" "$BACKUP_DEST" 2>/dev/null; then + if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" $SSH_COPY_ID_FLAGS "$BACKUP_DEST"; then print_success "SSH key copied successfully." else print_error "Automated SSH key copy failed. Please copy manually." @@ -1020,19 +1026,25 @@ setup_backup() { cat "${ROOT_SSH_KEY}.pub" echo echo -e "${YELLOW}Run the following command from this server's terminal to copy the key:${NC}" - echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" \"${BACKUP_DEST}\"${NC}" + echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" ${SSH_COPY_ID_FLAGS} \"${BACKUP_DEST}\"${NC}" echo fi # --- Test SSH Connection --- if confirm "Test SSH connection to the backup destination (recommended)?"; then print_info "Testing connection (timeout: 10 seconds)..." - if ! ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=10 "$BACKUP_DEST" true 2>/dev/null; then - print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open." - print_info " - Copy key: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" \"$BACKUP_DEST\"" - print_info " - Check port: nc -zv $(echo \"$BACKUP_DEST\" | cut -d'@' -f2) \"$BACKUP_PORT\"" - else + # For Hetzner, test with sftp. For others, test with ssh. + 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="echo 'exit' | sftp -P \"$BACKUP_PORT\" -o BatchMode=no -o ConnectTimeout=10 \"$BACKUP_DEST\"" + fi + + if eval "$test_command" 2>/dev/null; then print_success "SSH connection to backup destination successful!" + else + print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open." + print_info " - Copy key command was: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" $SSH_COPY_ID_FLAGS \"$BACKUP_DEST\"" + print_info " - Check port: nc -zv $(echo \"$BACKUP_DEST\" | cut -d'@' -f2) \"$BACKUP_PORT\"" fi fi @@ -1052,7 +1064,6 @@ node_modules/ .wget-hsts EOF if confirm "Add more directories/files to the exclude list?"; then - # Use read -a to handle spaces in paths correctly read -rp "$(echo -e "${CYAN}Enter items separated by spaces (e.g., Videos/ 'My Documents/'): ${NC}")" -a extra_excludes for item in "${extra_excludes[@]}"; do echo "$item" >> "$EXCLUDE_FILE_PATH" @@ -1118,7 +1129,6 @@ NTFY_TOKEN="${NTFY_TOKEN}" DISCORD_WEBHOOK="${DISCORD_WEBHOOK}" EOF - # Use a quoted heredoc to write the script logic literally, preventing variable expansion tee -a "$BACKUP_SCRIPT_PATH" > /dev/null <<'EOF' # --- SCRIPT LOGIC --- @@ -1141,7 +1151,6 @@ send_notification() { ${NTFY_TOKEN:+-H "Authorization: Bearer $NTFY_TOKEN"} \ -d "$message" "$NTFY_URL" > /dev/null 2>&1 elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then - # Escape JSON special characters in the message local escaped_message escaped_message=$(echo "$message" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g' | sed ':a;N;$!ba;s/\n/\\n/g') local json_payload @@ -1151,7 +1160,6 @@ send_notification() { } # --- DEPENDENCY CHECKS --- -# Corrected dependency check loop for cmd in rsync curl numfmt awk flock; do if ! command -v "$cmd" &>/dev/null; then echo "FATAL: Required command '$cmd' not found. Please install it." >> "$LOG_FILE" @@ -1174,7 +1182,6 @@ fi echo "--- Starting Backup at $(date) ---" >> "$LOG_FILE" # Run rsync -# The '-R' option preserves the full path from the source rsync_output=$(rsync -avzR --delete --stats --exclude-from="$EXCLUDE_FILE" \ -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "${REMOTE_DEST}:${REMOTE_PATH}" 2>&1) @@ -1184,7 +1191,7 @@ echo "$rsync_output" >> "$LOG_FILE" # Check status and send notification if [[ $rsync_exit_code -eq 0 ]]; then data_transferred=$(echo "$rsync_output" | grep 'Total transferred file size' | awk '{print $5}' | sed 's/,//g') - human_readable=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$data_transferred") + human_readable=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$data_transferred" 2>/dev/null || echo "0 B") message="Backup completed successfully.\\nData Transferred: ${human_readable}" send_notification "SUCCESS" "$message" echo "--- Backup SUCCEEDED at $(date) ---" >> "$LOG_FILE" @@ -1200,8 +1207,8 @@ EOF # --- Configure Cron Job --- print_info "Configuring root cron job..." - # This robustly removes the old job (if any) and adds the new one - (crontab -u root -l 2>/dev/null | grep -v "$CRON_MARKER"; echo "$CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER") | crontab -u root - + # FIX: Make crontab -l command safe for set -e + (crontab -u root -l 2>/dev/null || true | grep -v "$CRON_MARKER"; echo "$CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER") | crontab -u root - print_success "Backup cron job scheduled: $CRON_SCHEDULE" log "Backup configuration completed." From 98b5230546d70a48c5020b71944f8b77ae3d2160 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 16:05:32 +0100 Subject: [PATCH 15/22] improving hetzner backup settings --- setup_harden_debian_ubuntu.sh | 189 +++++++++++----------------------- 1 file changed, 61 insertions(+), 128 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index da98e91..f6d0656 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -966,51 +966,47 @@ setup_backup() { print_info "Existing root SSH key found at $ROOT_SSH_KEY." fi - # --- Collect Backup Destination Details --- + # --- Collect Backup Destination Details with Retry Loops --- local BACKUP_DEST BACKUP_PORT REMOTE_BACKUP_PATH SSH_COPY_ID_FLAGS="" - read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${NC}")" BACKUP_DEST - read -rp "$(echo -e "${CYAN}Enter destination SSH port (Hetzner uses 23) [22]: ${NC}")" BACKUP_PORT - read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/my_backups/): ${NC}")" REMOTE_BACKUP_PATH - # Validate inputs - BACKUP_PORT=${BACKUP_PORT:-22} - if [[ ! "$BACKUP_DEST" =~ ^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$ ]]; then - print_error "Invalid backup destination format. Expected user@host." - return 1 - fi - if [[ ! "$REMOTE_BACKUP_PATH" =~ ^/[^[:space:]]*/$ ]]; then - print_error "Invalid remote backup path. Must start and end with '/' and contain no spaces." - return 1 - fi - if ! [[ "$BACKUP_PORT" =~ ^[0-9]+$ && "$BACKUP_PORT" -ge 1 && "$BACKUP_PORT" -le 65535 ]]; then - print_error "Invalid SSH port. Must be between 1 and 65535." - return 1 - fi + while true; do + read -rp "$(echo -e "${CYAN}Enter backup destination (e.g., u12345@u12345.your-storagebox.de): ${NC}")" BACKUP_DEST + 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 + read -rp "$(echo -e "${CYAN}Enter destination SSH port (Hetzner uses 23) [22]: ${NC}")" BACKUP_PORT + 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 + read -rp "$(echo -e "${CYAN}Enter remote backup path (e.g., /home/my_backups/): ${NC}")" REMOTE_BACKUP_PATH + 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 + print_info "Backup target set to: ${BACKUP_DEST}:${REMOTE_BACKUP_PATH} on port ${BACKUP_PORT}" - # --- FIX: Hetzner Specific Handling --- - if confirm "Is this backup destination a Hetzner Storage Box (requires special handling)?"; then - SSH_COPY_ID_FLAGS="-s" # Add the -s flag required by Hetzner + # --- 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" print_info "Hetzner Storage Box mode enabled. Using '-s' for ssh-copy-id." fi # --- Handle SSH Key Copy --- + # (Your existing code for this section is excellent and remains unchanged) echo -e "${CYAN}Choose how to copy the root SSH key:${NC}" - echo -e " 1) Automate with password (requires sshpass, less secure)" + echo -e " 1) Automate with password (requires sshpass, password stored briefly in memory)" echo -e " 2) Manual copy (recommended)" read -rp "$(echo -e "${CYAN}Enter choice (1-2) [2]: ${NC}")" KEY_COPY_CHOICE 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..." - apt-get install -y -qq sshpass || { - print_warning "Failed to install sshpass. Falling back to manual copy." - KEY_COPY_CHOICE=2 - } + apt-get install -y -qq sshpass || { print_warning "Failed to install sshpass. Falling back to manual copy."; KEY_COPY_CHOICE=2; } fi if [[ "$KEY_COPY_CHOICE" == "1" ]]; then - read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD - echo + read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD; echo if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" $SSH_COPY_ID_FLAGS "$BACKUP_DEST"; then print_success "SSH key copied successfully." else @@ -1021,34 +1017,25 @@ setup_backup() { fi if [[ "$KEY_COPY_CHOICE" == "2" ]]; then print_warning "ACTION REQUIRED: Copy the root SSH key to the backup destination." - echo -e "This will allow the root user to connect without a password for automated backups." - echo -e "${YELLOW}The root user's public key is:${NC}" - cat "${ROOT_SSH_KEY}.pub" - echo + echo -e "${YELLOW}The root user's public key is:${NC}"; cat "${ROOT_SSH_KEY}.pub"; echo echo -e "${YELLOW}Run the following command from this server's terminal to copy the key:${NC}" - echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" ${SSH_COPY_ID_FLAGS} \"${BACKUP_DEST}\"${NC}" - echo + echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" ${SSH_COPY_ID_FLAGS} \"${BACKUP_DEST}\"${NC}"; echo fi - # --- Test SSH Connection --- + # --- SSH Connection Test --- if confirm "Test SSH connection to the backup destination (recommended)?"; then - print_info "Testing connection (timeout: 10 seconds)..." - # For Hetzner, test with sftp. For others, test with ssh. - 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="echo 'exit' | sftp -P \"$BACKUP_PORT\" -o BatchMode=no -o ConnectTimeout=10 \"$BACKUP_DEST\"" - fi - - if eval "$test_command" 2>/dev/null; then + print_info "Testing SSH connection (timeout: 10 seconds)..." + if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=10 "$BACKUP_DEST" true 2>/dev/null; then print_success "SSH connection to backup destination successful!" else print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open." - print_info " - Copy key command was: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" $SSH_COPY_ID_FLAGS \"$BACKUP_DEST\"" + print_info " - Copy key: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" $SSH_COPY_ID_FLAGS \"$BACKUP_DEST\"" print_info " - Check port: nc -zv $(echo \"$BACKUP_DEST\" | cut -d'@' -f2) \"$BACKUP_PORT\"" + print_info " - Ensure key is in ~/.ssh/authorized_keys on the backup server." fi fi - # --- Create Exclude File --- + # (Your existing code for this section is excellent and remains unchanged) print_info "Creating rsync exclude file at $EXCLUDE_FILE_PATH..." tee "$EXCLUDE_FILE_PATH" > /dev/null <<'EOF' # Default Exclusions @@ -1065,43 +1052,35 @@ node_modules/ EOF if confirm "Add more directories/files to the exclude list?"; then read -rp "$(echo -e "${CYAN}Enter items separated by spaces (e.g., Videos/ 'My Documents/'): ${NC}")" -a extra_excludes - for item in "${extra_excludes[@]}"; do - echo "$item" >> "$EXCLUDE_FILE_PATH" - done + for item in "${extra_excludes[@]}"; do echo "$item" >> "$EXCLUDE_FILE_PATH"; done fi chmod 600 "$EXCLUDE_FILE_PATH" print_success "Rsync exclude file created." + # --- Collect Cron Schedule --- local CRON_SCHEDULE - print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help." - read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [5 3 * * *]: ${NC}")" CRON_SCHEDULE - CRON_SCHEDULE=${CRON_SCHEDULE:-"5 3 * * *"} - if ! echo "$CRON_SCHEDULE" | grep -qE '^(((\*\/)?[0-9,-]+|\*)\s){4}((\*\/)?[0-9,-]+|\*)$'; then - print_error "Invalid cron expression. Using default: 5 3 * * *" - CRON_SCHEDULE="5 3 * * *" - fi + while true; do + print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help." + read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [5 3 * * *]: ${NC}")" CRON_SCHEDULE + CRON_SCHEDULE=${CRON_SCHEDULE:-"5 3 * * *"} + # More robust cron validation + if [[ $CRON_SCHEDULE =~ ^(((\*\/)?[0-9,-]+|\*|([a-zA-Z]{3,3}))\s*){5}$ ]]; then break; else print_error "Invalid cron expression. Please try again."; fi + done # --- Collect Notification Details --- local NOTIFICATION_SETUP="none" NTFY_URL="" NTFY_TOKEN="" DISCORD_WEBHOOK="" if confirm "Enable backup status notifications?"; then - echo -e "${CYAN}Select notification method: 1) ntfy.sh 2) Discord [1]: ${NC}" - read -r n_choice + echo -e "${CYAN}Select notification method: 1) ntfy.sh 2) Discord [1]: ${NC}"; read -r n_choice if [[ "$n_choice" == "2" ]]; then NOTIFICATION_SETUP="discord" read -rp "$(echo -e "${CYAN}Enter Discord Webhook URL: ${NC}")" DISCORD_WEBHOOK - if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then - print_error "Invalid Discord webhook URL." - return 1 - fi + if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then print_error "Invalid Discord webhook URL."; return 1; fi else NOTIFICATION_SETUP="ntfy" read -rp "$(echo -e "${CYAN}Enter ntfy URL/topic (e.g., https://ntfy.sh/my-backups): ${NC}")" NTFY_URL read -rp "$(echo -e "${CYAN}Enter ntfy Access Token (optional): ${NC}")" NTFY_TOKEN - if [[ ! "$NTFY_URL" =~ ^https?:// ]]; then - print_error "Invalid ntfy URL. Must start with http:// or https://." - return 1 - fi + if [[ ! "$NTFY_URL" =~ ^https?:// ]]; then print_error "Invalid ntfy URL."; return 1; fi fi fi @@ -1110,10 +1089,7 @@ EOF tee "$BACKUP_SCRIPT_PATH" > /dev/null < /dev/null <<'EOF' - -# --- SCRIPT LOGIC --- +# --- BACKUP SCRIPT LOGIC --- send_notification() { - local status="$1" - local message="$2" - local title - local color - - if [[ "$status" == "SUCCESS" ]]; then - title="✅ Backup SUCCESS: $HOSTNAME" - color=3066993 # Green - else - title="❌ Backup FAILED: $HOSTNAME" - color=15158332 # Red - fi - + 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 if [[ "$NOTIFICATION_SETUP" == "ntfy" ]]; then - curl -s -H "Title: $title" \ - ${NTFY_TOKEN:+-H "Authorization: Bearer $NTFY_TOKEN"} \ - -d "$message" "$NTFY_URL" > /dev/null 2>&1 + curl -s -H "Title: $title" ${NTFY_TOKEN:+-H "Authorization: Bearer $NTFY_TOKEN"} -d "$message" "$NTFY_URL" > /dev/null 2>&1 elif [[ "$NOTIFICATION_SETUP" == "discord" ]]; then - local escaped_message - escaped_message=$(echo "$message" | sed 's/"/\\"/g' | sed 's/\\/\\\\/g' | sed ':a;N;$!ba;s/\n/\\n/g') - local json_payload - json_payload=$(printf '{"embeds": [{"title": "%s", "description": "%s", "color": %d}]}' "$title" "$escaped_message" "$color") + 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") curl -s -H "Content-Type: application/json" -d "$json_payload" "$DISCORD_WEBHOOK" > /dev/null 2>&1 fi } - -# --- DEPENDENCY CHECKS --- -for cmd in rsync curl numfmt awk flock; do - if ! command -v "$cmd" &>/dev/null; then - echo "FATAL: Required command '$cmd' not found. Please install it." >> "$LOG_FILE" - send_notification "FAILURE" "FATAL: Required command '$cmd' not found." - exit 10 - fi -done - -# Ensure single instance -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 - +# --- DEPENDENCY & LOCKING --- +for cmd in rsync flock; do if ! command -v "$cmd" &>/dev/null; then send_notification "FAILURE" "FATAL: '$cmd' not found."; exit 10; fi; done +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 echo "--- Starting Backup at $(date) ---" >> "$LOG_FILE" - -# Run rsync -rsync_output=$(rsync -avzR --delete --stats --exclude-from="$EXCLUDE_FILE" \ - -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "${REMOTE_DEST}:${REMOTE_PATH}" 2>&1) - -rsync_exit_code=$? -echo "$rsync_output" >> "$LOG_FILE" - -# Check status and send notification +# --- RSYNC COMMAND --- +rsync_output=$(rsync -avz --delete --stats --exclude-from="$EXCLUDE_FILE" -e "ssh -p $SSH_PORT" "$LOCAL_DIR" "${REMOTE_DEST}:${REMOTE_PATH}" 2>&1) +rsync_exit_code=$?; echo "$rsync_output" >> "$LOG_FILE" +# --- NOTIFICATION --- if [[ $rsync_exit_code -eq 0 ]]; then data_transferred=$(echo "$rsync_output" | grep 'Total transferred file size' | awk '{print $5}' | sed 's/,//g') human_readable=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$data_transferred" 2>/dev/null || echo "0 B") - message="Backup completed successfully.\\nData Transferred: ${human_readable}" + message="Backup completed successfully.\nData Transferred: ${human_readable}" send_notification "SUCCESS" "$message" - echo "--- Backup SUCCEEDED at $(date) ---" >> "$LOG_FILE" else message="rsync failed with exit code ${rsync_exit_code}. Check log for details." send_notification "FAILURE" "$message" - echo "--- Backup FAILED at $(date) ---" >> "$LOG_FILE" fi EOF - chmod 700 "$BACKUP_SCRIPT_PATH" print_success "Backup script created." # --- Configure Cron Job --- print_info "Configuring root cron job..." - # FIX: Make crontab -l command safe for set -e (crontab -u root -l 2>/dev/null || true | grep -v "$CRON_MARKER"; echo "$CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER") | crontab -u root - - print_success "Backup cron job scheduled: $CRON_SCHEDULE" log "Backup configuration completed." } From 66ddd9b21c777bc9af9df4001bb02caec5538b44 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 16:06:25 +0100 Subject: [PATCH 16/22] improving hetzner backup settings --- setup_harden_debian_ubuntu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index f6d0656..c01655b 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,7 +1,7 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4-rc1 | 2025-06-28 +# Version: 4-rc2 | 2025-06-28 # Changelog: # - v4.0: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. # - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. @@ -83,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4-rc1 | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4-rc2 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo From b330d0234485dfd0ad0884e758b6235c30b9f150 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 16:30:39 +0100 Subject: [PATCH 17/22] still stuck at Hetzner fix --- setup_harden_debian_ubuntu.sh | 36 +++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index c01655b..b1bd427 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -994,7 +994,6 @@ setup_backup() { fi # --- Handle SSH Key Copy --- - # (Your existing code for this section is excellent and remains unchanged) echo -e "${CYAN}Choose how to copy the root SSH key:${NC}" echo -e " 1) Automate with password (requires sshpass, password stored briefly in memory)" echo -e " 2) Manual copy (recommended)" @@ -1003,39 +1002,58 @@ setup_backup() { if [[ "$KEY_COPY_CHOICE" == "1" ]]; then if ! command -v sshpass >/dev/null 2>&1; then print_info "Installing sshpass for automated key copying..." - apt-get install -y -qq sshpass || { print_warning "Failed to install sshpass. Falling back to manual copy."; KEY_COPY_CHOICE=2; } + apt-get update -qq && apt-get install -y -qq sshpass || { print_warning "Failed to install sshpass. Falling back to manual copy."; KEY_COPY_CHOICE=2; } fi if [[ "$KEY_COPY_CHOICE" == "1" ]]; then read -sp "$(echo -e "${CYAN}Enter password for $BACKUP_DEST: ${NC}")" BACKUP_PASSWORD; echo - if SSHPASS="$BACKUP_PASSWORD" sshpass -e ssh-copy-id -p "$BACKUP_PORT" -i "$ROOT_SSH_KEY.pub" $SSH_COPY_ID_FLAGS "$BACKUP_DEST"; then + # 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 print_success "SSH key copied successfully." else - print_error "Automated SSH key copy failed. Please copy manually." + 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." KEY_COPY_CHOICE=2 fi fi fi if [[ "$KEY_COPY_CHOICE" == "2" ]]; then print_warning "ACTION REQUIRED: Copy the root SSH key to the backup destination." + echo -e "This will allow the root user to connect without a password for automated backups." echo -e "${YELLOW}The root user's public key is:${NC}"; cat "${ROOT_SSH_KEY}.pub"; echo echo -e "${YELLOW}Run the following command from this server's terminal to copy the key:${NC}" echo -e "${CYAN}ssh-copy-id -p \"${BACKUP_PORT}\" -i \"${ROOT_SSH_KEY}.pub\" ${SSH_COPY_ID_FLAGS} \"${BACKUP_DEST}\"${NC}"; echo + 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 fi # --- SSH Connection Test --- if confirm "Test SSH connection to the backup destination (recommended)?"; then print_info "Testing SSH connection (timeout: 10 seconds)..." - if ssh -p "$BACKUP_PORT" -o BatchMode=yes -o ConnectTimeout=10 "$BACKUP_DEST" true 2>/dev/null; then + 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 print_success "SSH connection to backup destination successful!" else print_error "SSH connection test failed. Please ensure the key was copied correctly and the port is open." print_info " - Copy key: ssh-copy-id -p \"$BACKUP_PORT\" -i \"$ROOT_SSH_KEY.pub\" $SSH_COPY_ID_FLAGS \"$BACKUP_DEST\"" print_info " - Check port: nc -zv $(echo \"$BACKUP_DEST\" | cut -d'@' -f2) \"$BACKUP_PORT\"" print_info " - Ensure key is in ~/.ssh/authorized_keys on the backup server." + 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 fi fi + # --- Create Exclude File --- - # (Your existing code for this section is excellent and remains unchanged) print_info "Creating rsync exclude file at $EXCLUDE_FILE_PATH..." tee "$EXCLUDE_FILE_PATH" > /dev/null <<'EOF' # Default Exclusions @@ -1057,15 +1075,13 @@ EOF chmod 600 "$EXCLUDE_FILE_PATH" print_success "Rsync exclude file created." - # --- Collect Cron Schedule --- local CRON_SCHEDULE while true; do print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help." read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [5 3 * * *]: ${NC}")" CRON_SCHEDULE CRON_SCHEDULE=${CRON_SCHEDULE:-"5 3 * * *"} - # More robust cron validation - if [[ $CRON_SCHEDULE =~ ^(((\*\/)?[0-9,-]+|\*|([a-zA-Z]{3,3}))\s*){5}$ ]]; then break; else print_error "Invalid cron expression. Please try again."; fi + if [[ $CRON_SCHEDULE =~ ^((\*|[0-9,-]+|[a-zA-Z]{3})\s*){5}$ ]]; then break; else print_error "Invalid cron expression. Please try again."; fi done # --- Collect Notification Details --- @@ -1118,7 +1134,7 @@ send_notification() { fi } # --- DEPENDENCY & LOCKING --- -for cmd in rsync flock; do if ! command -v "$cmd" &>/dev/null; then send_notification "FAILURE" "FATAL: '$cmd' not found."; exit 10; fi; done +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 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 From 4b2862329065a1acd493f7d0516f1b646ea46dd0 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 16:55:55 +0100 Subject: [PATCH 18/22] rsync fix --- setup_harden_debian_ubuntu.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index b1bd427..5cba1ca 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,7 +1,7 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4-rc2 | 2025-06-28 +# Version: 4-rc3 | 2025-06-28 # Changelog: # - v4.0: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. # - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. @@ -83,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4-rc2 | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4-rc3 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo @@ -1076,13 +1076,13 @@ EOF print_success "Rsync exclude file created." # --- Collect Cron Schedule --- - local CRON_SCHEDULE - while true; do - print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help." - read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [5 3 * * *]: ${NC}")" CRON_SCHEDULE - CRON_SCHEDULE=${CRON_SCHEDULE:-"5 3 * * *"} - if [[ $CRON_SCHEDULE =~ ^((\*|[0-9,-]+|[a-zA-Z]{3})\s*){5}$ ]]; then break; else print_error "Invalid cron expression. Please try again."; fi - done + local CRON_SCHEDULE="5 3 * * *" + print_info "Enter a cron schedule for the backup. Use https://crontab.guru for help." + read -rp "$(echo -e "${CYAN}Enter schedule (default: daily at 3:05 AM) [${CRON_SCHEDULE}]: ${NC}")" input + 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 # --- Collect Notification Details --- local NOTIFICATION_SETUP="none" NTFY_URL="" NTFY_TOKEN="" DISCORD_WEBHOOK="" From ed1075b8c722f4681154cf7c91a845fb83134560 Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 17:42:48 +0100 Subject: [PATCH 19/22] fix summary output --- setup_harden_debian_ubuntu.sh | 105 +++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 5cba1ca..223b157 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1091,18 +1091,26 @@ EOF if [[ "$n_choice" == "2" ]]; then NOTIFICATION_SETUP="discord" read -rp "$(echo -e "${CYAN}Enter Discord Webhook URL: ${NC}")" DISCORD_WEBHOOK - if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then print_error "Invalid Discord webhook URL."; return 1; fi + if [[ ! "$DISCORD_WEBHOOK" =~ ^https://discord.com/api/webhooks/ ]]; then + print_error "Invalid Discord webhook URL." + log "Invalid Discord webhook URL provided." + return 1 + fi else NOTIFICATION_SETUP="ntfy" read -rp "$(echo -e "${CYAN}Enter ntfy URL/topic (e.g., https://ntfy.sh/my-backups): ${NC}")" NTFY_URL read -rp "$(echo -e "${CYAN}Enter ntfy Access Token (optional): ${NC}")" NTFY_TOKEN - if [[ ! "$NTFY_URL" =~ ^https?:// ]]; then print_error "Invalid ntfy URL."; return 1; fi + if [[ ! "$NTFY_URL" =~ ^https?:// ]]; then + print_error "Invalid ntfy URL." + log "Invalid ntfy URL provided." + return 1 + fi fi fi # --- Generate the Backup Script --- print_info "Generating the backup script at $BACKUP_SCRIPT_PATH..." - tee "$BACKUP_SCRIPT_PATH" > /dev/null < /dev/null < /dev/null <<'EOF' + 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' # --- BACKUP SCRIPT LOGIC --- send_notification() { local status="$1" message="$2" title color @@ -1153,12 +1166,51 @@ else send_notification "FAILURE" "$message" fi EOF - chmod 700 "$BACKUP_SCRIPT_PATH" + 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 print_success "Backup script created." # --- Configure Cron Job --- print_info "Configuring root cron job..." - (crontab -u root -l 2>/dev/null || true | grep -v "$CRON_MARKER"; echo "$CRON_SCHEDULE $BACKUP_SCRIPT_PATH $CRON_MARKER") | crontab -u root - + # 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" print_success "Backup cron job scheduled: $CRON_SCHEDULE" log "Backup configuration completed." } @@ -1336,6 +1388,7 @@ final_cleanup() { log "Final system cleanup completed." } +```bash generate_summary() { print_section "Setup Complete!" print_info "Checking critical services..." @@ -1372,26 +1425,26 @@ generate_summary() { echo -e " Hostname: $SERVER_NAME" echo -e " SSH Port: $SSH_PORT" echo -e " Server IP: $SERVER_IP" - if [[ -f /root/backup.sh ]]; then - local CRON_SCHEDULE=$(crontab -u root -l 2>/dev/null | grep -F "/root/backup.sh" | awk '{print $1, $2, $3, $4, $5}' || echo "Not configured") - local NOTIFICATION_STATUS="None" - local BACKUP_DEST=$(grep "^BACKUP_DEST=" /root/backup.sh | cut -d'"' -f2 || echo "Unknown") - local BACKUP_PORT=$(grep "^SSH_PORT=" /root/backup.sh | cut -d'"' -f2 || echo "Unknown") - local REMOTE_BACKUP_DIR=$(grep "^REMOTE_DIR=" /root/backup.sh | cut -d'"' -f2 || echo "Unknown") - if grep -q "NTFY_URL" /root/backup.sh; then + if [[ -f /root/run_backup.sh ]]; then + local 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") + local NOTIFICATION_STATUS="None" + local BACKUP_DEST=$(grep "^REMOTE_DEST=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown") + local BACKUP_PORT=$(grep "^SSH_PORT=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown") + local REMOTE_BACKUP_PATH=$(grep "^REMOTE_PATH=" /root/run_backup.sh | cut -d'"' -f2 || echo "Unknown") + if grep -q "NTFY_URL=" /root/run_backup.sh && ! grep -q 'NTFY_URL=""' /root/run_backup.sh; then NOTIFICATION_STATUS="ntfy" - elif grep -q "DISCORD_WEBHOOK" /root/backup.sh; then + elif grep -q "DISCORD_WEBHOOK=" /root/run_backup.sh && ! grep -q 'DISCORD_WEBHOOK=""' /root/run_backup.sh; then NOTIFICATION_STATUS="Discord" - fi - echo -e " Remote Backup: Enabled" - echo -e " - Backup Script: /root/backup.sh" - echo -e " - Destination: $BACKUP_DEST" - echo -e " - SSH Port: $BACKUP_PORT" - echo -e " - Remote Path: $REMOTE_BACKUP_DIR" - echo -e " - Cron Schedule: $CRON_SCHEDULE" - echo -e " - Notifications: $NOTIFICATION_STATUS" + fi + echo -e " Remote Backup: Enabled" + echo -e " - Backup Script: /root/run_backup.sh" + echo -e " - Destination: $BACKUP_DEST" + echo -e " - SSH Port: $BACKUP_PORT" + echo -e " - Remote Path: $REMOTE_BACKUP_PATH" + echo -e " - Cron Schedule: $CRON_SCHEDULE" + echo -e " - Notifications: $NOTIFICATION_STATUS" else - echo -e " Remote Backup: Not configured" + echo -e " Remote Backup: Not configured" fi echo echo -e "${PURPLE}Log File: ${LOG_FILE}${NC}" @@ -1410,12 +1463,12 @@ generate_summary() { if command -v tailscale >/dev/null 2>&1; then echo -e " - Tailscale status: tailscale status" fi - if [[ -f /root/backup.sh ]]; then + if [[ -f /root/run_backup.sh ]]; then echo -e " - Remote Backup:" echo -e " - Verify SSH key: cat /root/.ssh/id_ed25519.pub" echo -e " - Copy key if needed: ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" - echo -e " - Test backup: sudo /root/backup.sh" - echo -e " - Check logs: sudo less /var/log/backup_*.log" + echo -e " - Test backup: sudo /root/run_backup.sh" + echo -e " - Check logs: sudo less /var/log/backup_rsync.log" fi print_warning "\nACTION REQUIRED: If remote backup is enabled, ensure the root SSH key is copied to the destination server." print_warning "A reboot is required to apply all changes cleanly." From 0d5c96169d5ad44c234ddba09d58dde892967bf2 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Sat, 28 Jun 2025 17:50:50 +0100 Subject: [PATCH 20/22] Update setup_harden_debian_ubuntu.sh --- setup_harden_debian_ubuntu.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 223b157..082bcff 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,7 +1,7 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4-rc3 | 2025-06-28 +# Version: 4-rc4 | 2025-06-28 # Changelog: # - v4.0: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. # - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. @@ -83,7 +83,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4-rc3 | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4-rc4 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo @@ -1388,7 +1388,6 @@ final_cleanup() { log "Final system cleanup completed." } -```bash generate_summary() { print_section "Setup Complete!" print_info "Checking critical services..." From 424977bab2a8c2ed350dac8f5a0452197ac2cb81 Mon Sep 17 00:00:00 2001 From: buildplan <170122315+buildplan@users.noreply.github.com> Date: Sat, 28 Jun 2025 17:53:00 +0100 Subject: [PATCH 21/22] Update setup_harden_debian_ubuntu.sh From 6d5acd5ae3afa4bddf2e0b324507beeff5a62ebc Mon Sep 17 00:00:00 2001 From: Ali Date: Sat, 28 Jun 2025 18:48:26 +0100 Subject: [PATCH 22/22] v4 error free tested on D12 & U24.04 --- setup_harden_debian_ubuntu.sh | 57 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/setup_harden_debian_ubuntu.sh b/setup_harden_debian_ubuntu.sh index 082bcff..ef85b9c 100644 --- a/setup_harden_debian_ubuntu.sh +++ b/setup_harden_debian_ubuntu.sh @@ -1,21 +1,20 @@ #!/bin/bash # Debian 12 and Ubuntu Server Hardening Interactive Script -# Version: 4-rc4 | 2025-06-28 +# Version: 4.0 | 2025-06-28 # Changelog: -# - v4.0: Generalized backup configuration to support any rsync-compatible SSH destination, renamed setup_hetzner_backup to setup_backup. -# - v4.0: Added Hetzner Storage Box backup configuration with root SSH key automation, cron job scheduling, ntfy/Discord notifications, and exclude file defaults. -# - v4.0: Enhanced generate_summary to include backup details (script path, cron schedule, notifications). -# - v4.0: Tested on Debian 12, Ubuntu 20.04, 22.04, 24.04, and 24.10 (experimental) at DigitalOcean, Oracle Cloud, Netcup, Hetzner, and local VMs. +# - 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 # # Description: # This script provisions and hardens a fresh Debian 12 or Ubuntu server with essential security # configurations, user management, SSH hardening, firewall setup, and optional features -# like Docker and Tailscale. It is designed to be idempotent, safe, and suitable for -# production environments. +# like Docker and Tailscale and automated backups to Hetzner storage box or any rsync location. +# It is designed to be idempotent, safe. +# README at GitHub: https://github.com/buildplan/setup_harden_server # # Prerequisites: -# - Run as root on a fresh Debian 12 or Ubuntu server (e.g., sudo ./setup_harden_debian_ubuntu.sh). +# - Run as root on a fresh Debian 12 or Ubuntu server (e.g., sudo ./setup_harden_debian_ubuntu.sh or run as root ./setup_harden_debian_ubuntu.sh). # - Internet connectivity is required for package installation. # # Usage: @@ -24,7 +23,7 @@ # Run it: sudo ./setup_harden_debian_ubuntu.sh [--quiet] # # Options: -# --quiet: Suppress non-critical output for automation. +# --quiet: Suppress non-critical output for automation. (Not recommended always best to review all the options) # # Notes: # - The script creates a log file in /var/log/setup_harden_debian_ubuntu_*.log. @@ -83,7 +82,7 @@ print_header() { echo -e "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}" - echo -e "${CYAN}║ v4-rc4 | 2025-06-28 ║${NC}" + echo -e "${CYAN}║ v4.0 | 2025-06-28 ║${NC}" echo -e "${CYAN}║ ║${NC}" echo -e "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}" echo @@ -1436,12 +1435,12 @@ generate_summary() { NOTIFICATION_STATUS="Discord" fi echo -e " Remote Backup: Enabled" - echo -e " - Backup Script: /root/run_backup.sh" - echo -e " - Destination: $BACKUP_DEST" - echo -e " - SSH Port: $BACKUP_PORT" - echo -e " - Remote Path: $REMOTE_BACKUP_PATH" - echo -e " - Cron Schedule: $CRON_SCHEDULE" - echo -e " - Notifications: $NOTIFICATION_STATUS" + echo -e " - Backup Script: /root/run_backup.sh" + echo -e " - Destination: $BACKUP_DEST" + echo -e " - SSH Port: $BACKUP_PORT" + echo -e " - Remote Path: $REMOTE_BACKUP_PATH" + echo -e " - Cron Schedule: $CRON_SCHEDULE" + echo -e " - Notifications: $NOTIFICATION_STATUS" else echo -e " Remote Backup: Not configured" fi @@ -1450,30 +1449,30 @@ generate_summary() { echo -e "${PURPLE}Backups: ${BACKUP_DIR}${NC}" echo echo -e "${CYAN}Post-Reboot Verification Steps:${NC}" - echo -e " - SSH access: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" - echo -e " - Firewall rules: sudo ufw status verbose" - echo -e " - Time sync: chronyc tracking" - echo -e " - Fail2Ban status: sudo fail2ban-client status sshd" - echo -e " - Swap status: sudo swapon --show && free -h" - echo -e " - Hostname: hostnamectl" + echo -e " - SSH access: ssh -p $SSH_PORT $USERNAME@$SERVER_IP" + echo -e " - Firewall rules: sudo ufw status verbose" + echo -e " - Time sync: chronyc tracking" + echo -e " - Fail2Ban status: sudo fail2ban-client status sshd" + echo -e " - Swap status: sudo swapon --show && free -h" + echo -e " - Hostname: hostnamectl" if command -v docker >/dev/null 2>&1; then - echo -e " - Docker status: docker ps" + echo -e " - Docker status: docker ps" fi if command -v tailscale >/dev/null 2>&1; then - echo -e " - Tailscale status: tailscale status" + echo -e " - Tailscale status: tailscale status" fi if [[ -f /root/run_backup.sh ]]; then echo -e " - Remote Backup:" - echo -e " - Verify SSH key: cat /root/.ssh/id_ed25519.pub" - echo -e " - Copy key if needed: ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" - echo -e " - Test backup: sudo /root/run_backup.sh" - echo -e " - Check logs: sudo less /var/log/backup_rsync.log" + echo -e " - Verify SSH key: cat /root/.ssh/id_ed25519.pub" + echo -e " - Copy key if needed: ssh-copy-id -p $BACKUP_PORT -s $BACKUP_DEST" + echo -e " - Test backup: sudo /root/run_backup.sh" + echo -e " - Check logs: sudo less /var/log/backup_rsync.log" fi print_warning "\nACTION REQUIRED: If remote backup is enabled, ensure the root SSH key is copied to the destination server." print_warning "A reboot is required to apply all changes cleanly." if [[ $VERBOSE == true ]]; then if confirm "Reboot now?" "y"; then - print_info "Rebooting now..." + print_info "Rebooting now, Bye!..." sleep 3 reboot else