Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

-----

**Version:** v0.79.1
**Version:** v0.80.0

**Last Updated:** 2026-01-13
**Last Updated:** 2026-01-19

**Compatible With:**

Expand All @@ -27,6 +27,7 @@ This script automates the initial setup and security hardening of a fresh Debian
* **Secure User Management**: Creates a new `sudo` user and disables root SSH access. Optionally installs a custom .bashrc for enhanced terminal experience.
* **SSH Hardening**: Configures a custom SSH port, enforces key-based authentication, and applies security best practices.
* **Firewall Configuration**: Sets up UFW with secure defaults and customizable rules.
* **2FA Configuration**: Optionally enhance server security with 2FA, Sets up 2FA and shows QR code to add to any TOTP authenticator.
* **Intrusion Prevention**: Installs and configures **Fail2Ban** to block malicious IPs.
* **Kernel Hardening**: Optionally applies a set of recommended `sysctl` security settings to harden the kernel against common network and memory-related threats.
* **Automated Security Updates**: Enables `unattended-upgrades` for automatic security patches.
Expand Down Expand Up @@ -87,12 +88,12 @@ sha256sum du_setup.sh

Compare the output hash to the one below. They must match exactly.

`3d9a348574eb6165fd24048a2b7981f897da9c75db5758e89a92451ed262e830`
`82c297bc94d295320132fe9630fe085d67bb0f2978bc2c21154810f75337fd73`

Or echo the hash to check, it should output: `du_setup.sh: OK`

```bash
echo 3d9a348574eb6165fd24048a2b7981f897da9c75db5758e89a92451ed262e830 du_setup.sh | sha256sum --check
echo 82c297bc94d295320132fe9630fe085d67bb0f2978bc2c21154810f75337fd73 du_setup.sh | sha256sum --check
```

### 3. Run the Script
Expand Down Expand Up @@ -133,7 +134,8 @@ sudo -E ./du_setup.sh --quiet
| **Setup User Creation & Management** | Creates or uses an existing admin user with optional SSH key setup and strong password enforcement. Includes marker file for cleanup exclusion. |
| **SSH Hardening and Rollback** | Disables root login, configures key-based authentication, sets custom SSH port, and supports rollback of SSH configuration if connectivity fails. |
| **Firewall Setup** | Configures UFW to deny incoming traffic by default, allowing specific user-defined ports. |
| **Fail2Ban Setup** | Configures Fail2Ban to monitor SSH and UFW logs, blocking suspicious IPs. |
| **Fail2Ban/CrowdSec Setup** | Configures Fail2Ban or CrowdSec to monitor SSH and UFW logs, blocking suspicious IPs. |
| **2FA Setup** | Configures TOTP, shows QR code in the terminal to be scanned with any OTP app and inculdes rollback in case it any issues with setup. |
| **Auto-Updates Setup** | Enables and configures `unattended-upgrades` for automatic security patches. |
| **Time Sync Setup** | Ensures `chrony` is active for accurate network time synchronization. |
| **Kernel and Sysctl Hardening** | Optional improvements to kernel parameters to mitigate common network attacks and improve system hardening. |
Expand Down
269 changes: 212 additions & 57 deletions du_setup.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/bin/bash

# Debian and Ubuntu Server Hardening Interactive Script
# Version: 0.79.1 | 2026-01-13
# Version: 0.80.0 | 2026-01-19
# Changelog:
# - v0.80.0: Added 2FA, optionally set 2FA for SSH Login.
# - v0.79.1: Added CrowdSec collections install to CrowdSec setup.
# - v0.79.0: Added CrowdSec, now you can choose between fail2ban and CrowdSec for system level firewall.
# - v0.78.5: Switched to using nano as the default editor in .bashrc.
# - v0.78.4: Improved configure_swap to detect swap partitions vs files.
Expand Down Expand Up @@ -97,7 +99,7 @@
set -euo pipefail

# --- Update Configuration ---
CURRENT_VERSION="0.79.1"
CURRENT_VERSION="0.80.0"
SCRIPT_URL="https://raw.githubusercontent.com/buildplan/du_setup/refs/heads/main/du_setup.sh"
CHECKSUM_URL="${SCRIPT_URL}.sha256"

Expand Down Expand Up @@ -157,6 +159,7 @@ FAILED_SERVICES=()
PREVIOUS_SSH_PORT=""

IDS_INSTALLED=""
TWO_FACTOR_ENABLED="false"

# --- --help ---
show_usage() {
Expand Down Expand Up @@ -255,7 +258,7 @@ print_header() {
printf '%s\n' "${CYAN}╔═════════════════════════════════════════════════════════════════╗${NC}"
printf '%s\n' "${CYAN}║ ║${NC}"
printf '%s\n' "${CYAN}║ DEBIAN/UBUNTU SERVER SETUP AND HARDENING SCRIPT ║${NC}"
printf '%s\n' "${CYAN}║ v0.79.1 | 2026-01-13 ║${NC}"
printf '%s\n' "${CYAN}║ v0.80.0 | 2026-01-19 ║${NC}"
printf '%s\n' "${CYAN}║ ║${NC}"
printf '%s\n' "${CYAN}╚═════════════════════════════════════════════════════════════════╝${NC}"
printf '\n'
Expand Down Expand Up @@ -3263,6 +3266,53 @@ cleanup_and_exit() {
exit $exit_code
}

show_connection_options() {
local port="$1"
local public_ip="$2"

local TS_IP=""
if command -v tailscale >/dev/null 2>&1 && tailscale ip >/dev/null 2>&1; then
TS_IP=$(tailscale ip -4 2>/dev/null)
fi

printf "\n"

# 1. Public IP (Internet)
if [[ -n "$public_ip" && "$public_ip" != "Unknown" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Public (Internet):" "$port" "$USERNAME" "$public_ip"
fi

# 2. Internal/LAN IPs
local found_internal=false
while read -r ip_addr; do
local clean_ip="${ip_addr%/*}"
if [[ -n "$clean_ip" && "$clean_ip" != "127.0.0.1" && "$clean_ip" != "$public_ip" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Internal/Private:" "$port" "$USERNAME" "$clean_ip"
found_internal=true
fi
done < <(ip -4 -o addr show scope global | awk '{print $4}')

# show the detected local IP from route (Home VM scenario)
if [[ "$found_internal" == false && "$public_ip" == "Unknown" ]]; then
local fallback_ip
fallback_ip=$(ip -4 route get 8.8.8.8 2>/dev/null | head -1 | awk '{print $7}')
if [[ -n "$fallback_ip" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Local (LAN):" "$port" "$USERNAME" "$fallback_ip"
fi
fi

# 3. IPv6
if [[ -n "$SERVER_IP_V6" && "$SERVER_IP_V6" != "Not available" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "IPv6:" "$port" "$USERNAME" "$SERVER_IP_V6"
fi

# 4. Tailscale IP (VPN)
if [[ -n "$TS_IP" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Tailscale (VPN):" "$port" "$USERNAME" "$TS_IP"
fi
printf "\n"
}

configure_ssh() {
trap cleanup_and_exit ERR

Expand Down Expand Up @@ -3323,60 +3373,6 @@ configure_ssh() {
print_warning "SSH Key Authentication Required for Next Steps!"
printf '%s\n' "${CYAN}Test SSH access from a SEPARATE terminal now.${NC}"

# --- Connection Display Function ---
show_connection_options() {
local port="$1"
local public_ip="$2"

local TS_IP=""
if command -v tailscale >/dev/null 2>&1 && tailscale ip >/dev/null 2>&1; then
TS_IP=$(tailscale ip -4 2>/dev/null)
fi

printf "\n"

# 1. Public IP (Internet)
# Only show if valid and not "Unknown"
if [[ -n "$public_ip" && "$public_ip" != "Unknown" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Public (Internet):" "$port" "$USERNAME" "$public_ip"
fi

# 2. Internal/LAN IPs
# scan all interfaces. exclude the Public IP (already shown) and Loopback.
local found_internal=false
while read -r ip_addr; do
# Remove subnet mask if present
local clean_ip="${ip_addr%/*}"

# Skip if empty, loopback, or matches the Public IP we just displayed
if [[ -n "$clean_ip" && "$clean_ip" != "127.0.0.1" && "$clean_ip" != "$public_ip" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Internal/Private:" "$port" "$USERNAME" "$clean_ip"
found_internal=true
fi
done < <(ip -4 -o addr show scope global | awk '{print $4}')

# Fallback: If we found NO internal IPs and NO Public IP (local VM offline?),
# show the detected local IP from route (Home VM scenario)
if [[ "$found_internal" == false && "$public_ip" == "Unknown" ]]; then
local fallback_ip
fallback_ip=$(ip -4 route get 8.8.8.8 2>/dev/null | head -1 | awk '{print $7}')
if [[ -n "$fallback_ip" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Local (LAN):" "$port" "$USERNAME" "$fallback_ip"
fi
fi

# 3. IPv6
if [[ -n "$SERVER_IP_V6" && "$SERVER_IP_V6" != "Not available" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "IPv6:" "$port" "$USERNAME" "$SERVER_IP_V6"
fi

# 4. Tailscale IP (VPN)
if [[ -n "$TS_IP" ]]; then
printf " %-20s ${CYAN}ssh -p %s %s@%s${NC}\n" "Tailscale (VPN):" "$port" "$USERNAME" "$TS_IP"
fi
printf "\n"
}

# Show options for CURRENT port
show_connection_options "$CURRENT_SSH_PORT" "$SERVER_IP_V4"

Expand Down Expand Up @@ -3680,6 +3676,157 @@ rollback_ssh_changes() {
return 0
}

configure_2fa() {
print_section "Two-Factor Authentication (2FA) Setup"
print_info "2FA adds an extra layer of security by requiring a time-based code (TOTP) along with your SSH key."
print_info "Note: This will be configured specifically for the user '$USERNAME'."

if ! confirm "Setup 2FA (Google Authenticator) for user '$USERNAME'?"; then
print_info "Skipping 2FA setup."
return 0
fi

# 1. Install Dependencies
print_info "Installing libpam-google-authenticator and qrencode..."
if ! apt-get update -qq || ! apt-get install -y -qq libpam-google-authenticator qrencode; then
print_error "Failed to install required packages. Aborting 2FA setup."
return 1
fi

local USER_HOME
USER_HOME=$(getent passwd "$USERNAME" | cut -d: -f6)
local GA_FILE="$USER_HOME/.google_authenticator"
local SETUP_SUCCESS=false

# 2. Generate Secret
if [[ -f "$GA_FILE" ]]; then
print_warning "2FA configuration already exists for $USERNAME."
if ! confirm "Regenerate secret (this will invalidate old codes)?"; then
print_info "Keeping existing 2FA configuration."
SETUP_SUCCESS=true
else
rm -f "$GA_FILE"
fi
fi

if [[ "$SETUP_SUCCESS" == "false" ]]; then
print_info "Generating 2FA secret for $USERNAME..."

# Run google-authenticator non-interactively
# -t: time-based, -d: disallow reuse, -f: force to file, -r 3 -R 30: rate limit, -w 3: window size
if ! sudo -u "$USERNAME" google-authenticator -t -d -f -r 3 -R 30 -w 3 -q; then
print_error "Failed to generate 2FA configuration."
return 1
fi

# Extract secret for display
local SECRET
SECRET=$(head -n 1 "$GA_FILE")
local QR_LABEL="${USERNAME}@${SERVER_NAME}"
local QR_URL; QR_URL="otpauth://totp/${QR_LABEL}?secret=${SECRET}&issuer=$(hostname)"

print_section "ACTION REQUIRED: Setup Authenticator App"
printf '%s\n' "${YELLOW}1. Open your Authenticator App (Google Auth, Authy, etc.)${NC}"
printf '%s\n' "${YELLOW}2. Scan the QR Code below:${NC}"
printf '\n'

# Display QR Code in terminal
qrencode -t ANSIUTF8 "$QR_URL"

printf '\n'
printf " %s: %s\n" "${CYAN}Secret Key (if QR fails)${NC}" "${BOLD}$SECRET${NC}"
printf '\n'
printf '%s\n' "${RED}SAVE THESE EMERGENCY SCRATCH CODES:${NC}"
grep -E '^[0-9]{8}$' "$GA_FILE"
printf '\n'

if ! confirm "Have you saved the secret/codes and set up your app?"; then
print_warning "Aborting 2FA setup to prevent lockout."
rm -f "$GA_FILE"
return 1
fi
SETUP_SUCCESS=true
fi

# 3. Configure PAM
local PAM_FILE="/etc/pam.d/sshd"
print_info "Configuring PAM ($PAM_FILE)..."
if ! grep -q "pam_google_authenticator.so" "$PAM_FILE"; then
cp "$PAM_FILE" "${PAM_FILE}.backup_$(date +%Y%m%d_%H%M%S)"
# Prepend to ensure it runs
sed -i '1i auth required pam_google_authenticator.so nullok' "$PAM_FILE"
print_success "Updated PAM configuration."
else
print_info "PAM already configured."
fi

# 4. Configure SSH
print_info "Configuring SSH to enforce 2FA for '$USERNAME'..."
local SSH_DROPIN_DIR="/etc/ssh/sshd_config.d"
local SSH_2FA_CONF="$SSH_DROPIN_DIR/95-2fa-${USERNAME}.conf"
local USE_DROPIN=false

# Check if drop-in directory exists and is included (standard on Ubuntu 20.04+/Debian 12)
if [[ -d "$SSH_DROPIN_DIR" ]] && grep -q "^Include /etc/ssh/sshd_config.d/\*.conf" /etc/ssh/sshd_config; then
USE_DROPIN=true
fi

local CONFIG_CONTENT="Match User $USERNAME
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes
"

if [[ "$USE_DROPIN" == "true" ]]; then
echo "$CONFIG_CONTENT" > "$SSH_2FA_CONF"
print_success "Created SSH user config: $SSH_2FA_CONF"
else
# Fallback for older systems: Append to main config
print_warning "Drop-in config not supported. Appending to /etc/ssh/sshd_config."
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup_2fa
# Ensure global KbdInteractive is enabled if not already
if ! grep -q "^KbdInteractiveAuthentication yes" /etc/ssh/sshd_config && ! grep -q "^ChallengeResponseAuthentication yes" /etc/ssh/sshd_config; then
echo "KbdInteractiveAuthentication yes" >> /etc/ssh/sshd_config
fi
if ! grep -q "Match User $USERNAME" /etc/ssh/sshd_config; then
echo "$CONFIG_CONTENT" >> /etc/ssh/sshd_config
fi
fi

# 5. Restart and Verify
print_info "Restarting SSH service..."
if ! systemctl restart "$SSH_SERVICE"; then
print_error "Failed to restart SSH service. Reverting 2FA changes..."
if [[ "$USE_DROPIN" == "true" ]]; then rm -f "$SSH_2FA_CONF"; else cp /etc/ssh/sshd_config.backup_2fa /etc/ssh/sshd_config; fi
sed -i '/pam_google_authenticator.so/d' "$PAM_FILE"
return 1
fi

print_warning "CRITICAL VERIFICATION STEP"
print_info "Do NOT close this terminal."
print_info "Open a NEW terminal window and try to SSH in as $USERNAME."
print_info "You should be asked for your SSH Key passphrase (if set) FOLLOWED by the Verification Code."
print_info "With default PAM settings, you may ALSO be asked for $USERNAME password."

show_connection_options "$SSH_PORT" "$SERVER_IP_V4"

if confirm "Was the login successful?"; then
print_success "2FA setup verified and active."
TWO_FACTOR_ENABLED=true
log "2FA enabled for user $USERNAME."
else
print_error "Login verification failed. Reverting 2FA changes..."
if [[ "$USE_DROPIN" == "true" ]]; then
rm -f "$SSH_2FA_CONF"
else
# Basic cleanup for non-dropin
sed -i "/Match User $USERNAME/,+3d" /etc/ssh/sshd_config
fi
sed -i '/pam_google_authenticator.so/d' "$PAM_FILE"
systemctl restart "$SSH_SERVICE"
print_info "2FA changes reverted. Your SSH access should be standard."
fi
}

configure_firewall() {
print_section "Firewall Configuration (UFW)"
if ufw status | grep -q "Status: active"; then
Expand Down Expand Up @@ -5489,6 +5636,13 @@ generate_summary() {
printf " %-15s %s\n" "Server IPv6:" "$SERVER_IP_V6"
fi

# --- 2FA Status ---
if [[ "$TWO_FACTOR_ENABLED" == "true" ]]; then
printf " %-20s ${GREEN}Enabled (SSH Key + TOTP)${NC}\n" "2FA/MFA:"
else
printf " %-20s ${YELLOW}Disabled${NC}\n" "2FA/MFA:"
fi

# --- Kernel Hardening Status ---
if [[ -f /etc/sysctl.d/99-du-hardening.conf ]]; then
printf " %-20s${GREEN}Applied${NC}\n" "Kernel Hardening:"
Expand Down Expand Up @@ -5772,6 +5926,7 @@ main() {
;;
esac
configure_ssh
configure_2fa
configure_auto_updates
configure_time_sync
configure_kernel_hardening
Expand Down
2 changes: 1 addition & 1 deletion du_setup.sh.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3d9a348574eb6165fd24048a2b7981f897da9c75db5758e89a92451ed262e830 du_setup.sh
82c297bc94d295320132fe9630fe085d67bb0f2978bc2c21154810f75337fd73 du_setup.sh
Loading