From 202ef6a648c5f6e923c0275518ad9eaf53a5fa5e Mon Sep 17 00:00:00 2001 From: Pratik Tripathy <> Date: Thu, 9 Oct 2025 19:16:05 +0530 Subject: [PATCH] feat(firewalld,freebds+fail2ban): - Linux: ufw -> firewalld - installations: separate install list for linux & freebsd - firewall: `pf` for freebsd & `firewalld` for linux - firewall: allow ssh, http, https in; block all other - firewall freebsd: start pf & pflog services - fail2ban: Jails for: ssh, nginx-botsearch, nginx-http-auth, nginx-limit-req, haproxy-http-auth, recidive - fail2ban linux: action performed by `firewalld` - fail2ban freebsd: action performed by `pf` - chore: Hide command outputs from `if` conditions - refactor: `if` with `else` checks positive conditions - refactor: No explicit `return`s on success - refactor: No `exit`s - chore: Consistent log formatting with values inside `[]` - fix: Reverting success isn't operation succes --- init-linux-harden.sh | 756 ++++++++++++++++++++++++++++--------------- 1 file changed, 503 insertions(+), 253 deletions(-) diff --git a/init-linux-harden.sh b/init-linux-harden.sh index 0df9907..742eae0 100755 --- a/init-linux-harden.sh +++ b/init-linux-harden.sh @@ -1,10 +1,9 @@ #!/bin/sh SCRIPT_NAME=server-init-harden -SCRIPT_VERSION=2.2 +SCRIPT_VERSION=2.7 TIMESTAMP=$(date '+%Y-%m-%d_%H-%M-%S') LOG_FILE_NAME="${SCRIPT_NAME}_${TIMESTAMP}.log" -START_TIME=$(date +%s) USERNAME="" RESET_ROOT=false @@ -25,7 +24,7 @@ DESCRIPTION: - Generates secure SSH keys - Resets root password (optional) - Configures Fail2ban for intrusion prevention - - Sets up UFW firewall rules + - Sets up Firewalld firewall rules OPTIONS: -u USERNAME Create a new sudo user with the specified username @@ -34,7 +33,7 @@ OPTIONS: -h Display this help message EXAMPLES: - # Basic hardening (SSH, Fail2ban, UFW) + # Basic hardening (SSH, Fail2ban, Firewalld) $0 # Create new sudo user during hardening @@ -77,8 +76,8 @@ parse_and_validate_args() { USERNAME="$2" shift 2 else - console_log "ERROR" "Invalid username format. Must start with a letter and contain only alphanumeric characters, hyphens, or underscores." - file_log "ERROR" "Invalid username format. Must start with a letter and contain only alphanumeric characters, hyphens, or underscores." + console_log "ERROR" "Invalid username" + file_log "ERROR" "Invalid username. Must start with a letter and contain alphanumeric characters, hyphens, underscores." exit 1 fi ;; @@ -143,11 +142,7 @@ print_operation_details() { echo "Following system hardening operations will be performed:" if [ "$RESET_ROOT" = true ]; then - echo " [-r]: Existing root user's password will be re-created" - fi - - if [ "$SHOW_CREDENTIALS" = true ]; then - echo " [-s]: Passwords, keys are will be displayed on the screen" + echo " [-r]: root password will be reset" fi if [ -n "$USERNAME" ]; then @@ -160,8 +155,8 @@ print_operation_details() { echo " SSH: login to root account will be disabled" echo " SSH: login can only happen using generated SSH keys" echo " Software repository will be updated & required software will be installed" - echo " UFW: Firewall will be configured to only allow SSH, HTTP, HTTPS traffic into the server" - echo " Fail2Ban: Configured to automatically block repeat offender IPs" + echo " Firewalld: Firewall will be configured to only allow SSH, HTTP, HTTPS traffic into the server" + echo " Fail2ban: Configured to automatically block repeat offender IPs" } print_log_file_details() { @@ -288,13 +283,13 @@ reset_root_password() { else console_log "SUCCESS" "Root password reset" file_log "SUCCESS" "Root password reset" - log_credentials "New root password: $ROOT_PASSWORD" + log_credentials "New root password: [ $ROOT_PASSWORD ]" fi } revert_create_user() { - console_log "INFO" "Attempting to remove user $USERNAME" - file_log "INFO" "Attempting to remove user $USERNAME" + console_log "INFO" "Attempting to remove user [ $USERNAME ]" + file_log "INFO" "Attempting to remove user [ $USERNAME ]" # Check if the user exists before attempting to remove if id "$USERNAME" >/dev/null 2>&1; then @@ -304,21 +299,21 @@ revert_create_user() { file_log "INFO" "$output" if [ $command_status -eq 0 ]; then - console_log "SUCCESS" "User $USERNAME and home directory removed" - file_log "SUCCESS" "User $USERNAME and home directory removed" + console_log "INFO" "User [ $USERNAME ] and home directory removed" + file_log "INFO" "User [ $USERNAME ] and home directory removed" else - console_log "ERROR" "Failed to remove user $USERNAME" - file_log "ERROR" "Failed to remove user $USERNAME" + console_log "ERROR" "Failed to remove user [ $USERNAME ]" + file_log "ERROR" "Failed to remove user [ $USERNAME ]" fi else - console_log "WARNING" "No user $USERNAME found to remove" + console_log "WARNING" "No user [ $USERNAME ] found to remove" file_log "WARNING" "No user $USERNAME found to remove" fi } create_user() { - console_log "INFO" "Creating user $USERNAME..." - file_log "INFO" "Creating user $USERNAME" + console_log "INFO" "Creating user [ $USERNAME ]..." + file_log "INFO" "Creating user [ $USERNAME ]" # Generate a 15-character random password USER_PASSWORD=$(head -c 12 /dev/urandom | base64 | tr -dc "[:alnum:]" | head -c 15) @@ -335,21 +330,21 @@ create_user() { file_log "INFO" "$output" - if [ $command_status -ne 0 ]; then - console_log "ERROR" "Failed to create user: $USERNAME" - file_log "ERROR" "Failed to create user $USERNAME" + if [ $command_status -eq 0 ]; then + file_log "SUCCESS" "Created user [ $USERNAME ]" + console_log "SUCCESS" "Created user [ $USERNAME ]" + log_credentials "$USERNAME's password [ $USER_PASSWORD ]" + else + console_log "ERROR" "Failed to create user [ $USERNAME ]" + file_log "ERROR" "Failed to create user [ $USERNAME ]" revert_create_user return 1 - else - file_log "SUCCESS" "User created: $USERNAME" - console_log "SUCCESS" "User created: $USERNAME" - log_credentials "$USERNAME's password: $USER_PASSWORD" fi } user_privileged_access() { - file_log "INFO" "Granting privileged access (sudo) to $USERNAME" - console_log "INFO" "Granting privileged access (sudo) to $USERNAME" + file_log "INFO" "Granting privileged access (sudo) to [ $USERNAME ]" + console_log "INFO" "Granting privileged access (sudo) to [ $USERNAME ]" if getent group wheel >/dev/null 2>&1; then if command -v pw >/dev/null 2>&1; then # FreeBSD @@ -370,27 +365,27 @@ user_privileged_access() { file_log "INFO" "$output" - if [ "$command_status" -ne 0 ]; then - console_log "ERROR" "Failed to grant privileged access to $USERNAME" - file_log "ERROR" "Failed to grant privileged access to $USERNAME" - console_log "WARNING" "From $USERNAME, use [su -] to login to root & perform special operations" - file_log "WARNING" "From $USERNAME, use [su -] to login to root & perform special operations" + if [ "$command_status" -eq 0 ]; then + file_log "SUCCESS" "[ $USERNAME ] granted privileged access" + console_log "SUCCESS" "[ $USERNAME ] granted privileged access" else - file_log "SUCCESS" "$USERNAME granted privileged access" - console_log "SUCCESS" "$USERNAME granted privileged access" + console_log "ERROR" "Failed to grant privileged access to [ $USERNAME ]" + file_log "ERROR" "Failed to grant privileged access to [ $USERNAME ]" + console_log "WARNING" "From [ $USERNAME ], use [su -] to login to root & perform special operations" + file_log "WARNING" "From [ $USERNAME ], use [su -] to login to root & perform special operations" fi } generate_ssh_key() { - console_log "INFO" "Generating SSH key for $SSH_KEY_USER..." - file_log "INFO" "Generating SSH key for $SSH_KEY_USER" + console_log "INFO" "Generating SSH key for [ $SSH_KEY_USER ]..." + file_log "INFO" "Generating SSH key for [ $SSH_KEY_USER ]" # Create .ssh directory & set proper permissions home_dir=$(eval echo "~$USERNAME") ssh_dir="$home_dir/.ssh" if [ ! -d "$home_dir" ]; then - console_log "ERROR" "Home directory not found for $SSH_KEY_USER" - file_log "ERROR" "Home directory not found for $SSH_KEY_USER" + console_log "ERROR" "Home directory not found for [ $SSH_KEY_USER ]" + file_log "ERROR" "Home directory not found for [ $SSH_KEY_USER ]" return 1 else mkdir -p "$ssh_dir" && chown "$SSH_KEY_USER:$SSH_KEY_USER" "$ssh_dir" && chmod 700 "$ssh_dir" || return 1 @@ -447,26 +442,24 @@ generate_ssh_key() { } revert_ssh_config_changes() { - # Revert to backup and try restarting again + # Revert backup and try restarting again console_log "INFO" "Reverting to backup configuration..." file_log "INFO" "Reverting to backup configuration from: $SSH_CONFIG_BACKUP_FILE" - if ! cp "$SSH_CONFIG_BACKUP_FILE" "$SSHD_CONFIG_FILE"; then + if ! cp "$SSH_CONFIG_BACKUP_FILE" "$SSHD_CONFIG_FILE" >/dev/null 2>&1; then console_log "ERROR" "Failed to restore SSH config backup" file_log "ERROR" "Failed to restore SSH config backup" - exit 1 fi # Try restarting SSH with original config - if manage_service sshd restart || manage_service ssh restart; then - console_log "SUCCESS" "SSH service restarted with original configuration" - file_log "SUCCESS" "SSH service restarted with original configuration" - exit 1 + if manage_service sshd restart >/dev/null 2>&1 || manage_service ssh restart >/dev/null 2>&1; then + console_log "INFO" "SSH service restarted with original configuration" + file_log "INFO" "SSH service restarted with original configuration" + else + console_log "ERROR" "Failed to restart SSH service even with original configuration" + file_log "ERROR" "Failed to restart SSH service even with original configuration" + return 1 fi - - console_log "ERROR" "Failed to restart SSH service even with original configuration" - file_log "ERROR" "Failed to restart SSH service even with original configuration" - exit 1 } update_ssh_setting() { @@ -481,17 +474,18 @@ update_ssh_setting() { command_status=$? rm -f "$SSHD_CONFIG_FILE.tmp" >/dev/null 2>&1 - if [ $command_status -ne 0 ]; then + file_log "INFO" "$output" + + if [ $command_status -eq 0 ]; then + # Add new setting at the end of file + echo "${setting} ${value}" >>"$SSHD_CONFIG_FILE" + file_log "INFO" "Updated SSH setting: ${setting} ${value}" + else console_log "ERROR" "Updating SSH configuration [ $setting $value ] failed" file_log "ERROR" "Updating SSH configuration [ $setting $value ] failed: $output" revert_ssh_config_changes + return 1 fi - - file_log "INFO" "$output" - - # Add new setting at the end of file - echo "${setting} ${value}" >>"$SSHD_CONFIG_FILE" - file_log "INFO" "Updated SSH setting: ${setting} ${value}" } harden_ssh_config() { @@ -527,11 +521,11 @@ harden_ssh_config() { if manage_service sshd restart || manage_service ssh restart; then console_log "SUCCESS" "SSH service restarted" file_log "SUCCESS" "SSH service restarted" - return 0 else console_log "ERROR" "Failed to restart SSH service" file_log "ERROR" "Failed to restart SSH service" revert_ssh_config_changes + return 1 fi } @@ -539,15 +533,16 @@ install_packages() { console_log "INFO" "Installing required applications..." file_log "INFO" "Installing required applications..." - LINUX_ONLY_PACKAGES="firewalld" - COMMON_PACKAGES="curl sudo sshguard" + LINUX_ONLY_PACKAGES="firewalld fail2ban" + FREEBSD_ONLY_PACKAGES="py311-fail2ban" + COMMON_PACKAGES="curl sudo" # Detect the package manager and OS if [ -f /etc/debian_version ] || [ -f /etc/ubuntu_version ]; then # Debian/Ubuntu # Don't let timezone setting stop installation: make UTC server's timezone ln -fs /usr/share/zoneinfo/UTC /etc/localtime >/dev/null - console_log "WARNING" "Server's timezone set to UTC to avoid installation interruption." - file_log "WARNING" "Server's timezone set to UTC to avoid installation interruption. Change this after the script completes." + console_log "WARNING" "Timezone set to UTC to avoid installation interruption" + file_log "WARNING" "Timezone set to UTC to avoid installation interruption. Change this after the script completes." file_log "INFO" "Installing $COMMON_PACKAGES $LINUX_ONLY_PACKAGES using apt..." # shellcheck disable=SC2086 output=$(DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get install -y --no-install-recommends $COMMON_PACKAGES $LINUX_ONLY_PACKAGES 2>&1) @@ -570,7 +565,7 @@ install_packages() { elif [ -f /etc/freebsd-update.conf ]; then # FreeBSD file_log "INFO" "Installing $COMMON_PACKAGES using pkg..." # shellcheck disable=SC2086 - output=$(pkg update && pkg install -y $COMMON_PACKAGES 2>&1) + output=$(pkg update 2>&1 && pkg install -y $COMMON_PACKAGES $FREEBSD_ONLY_PACKAGES 2>&1) command_status=$? else file_log "ERROR" "Unsupported operating system" @@ -579,194 +574,451 @@ install_packages() { file_log "INFO" "Applications installation output: $output" - if [ $command_status -ne 0 ]; then - console_log "SUCCESS" "Installed all required applications" + if [ $command_status -eq 0 ]; then + file_log "SUCCESS" "Installed required applications" + console_log "SUCCESS" "Installed required applications" + else + console_log "ERROR" "Failed to install applications" file_log "ERROR" "Failed to install applications" return 1 + fi +} + +configure_firewall_linux() { + # Debian/Ubuntu -> Disable ufw if active + if command -v ufw >/dev/null 2>&1 && ufw status 2>&1 | grep -q "Status: active"; then + file_log "INFO" "ufw installed & active. Disabling it..." + output=$(ufw disable 2>&1) + console_log "WARNING" "Pre-installed firewall application ufw disabled" + file_log "WARNING" "Pre-installed firewall application ufw disabled: $output" + output=$(systemctl disable --now ufw 2>&1) + file_log "INFO" "$output" + fi + + # Enable Firewalld + output=$(systemctl enable firewalld 2>&1 && systemctl start --now firewalld) + file_log "INFO" "Enable firewalld: $output" + + output=$(firewall-cmd --permanent --add-service=ssh 2>&1) + file_log "INFO" "Allow SSH: $output" + + output=$(firewall-cmd --permanent --add-service=http 2>&1) + file_log "INFO" "Allow HTTP: $output" + + output=$(firewall-cmd --permanent --add-service=https 2>&1) + file_log "INFO" "Allow HTTPS: $output" + + # Enable Firewall + output=$(firewall-cmd --reload 2>&1) + file_log "INFO" "Reload firewalld service: $output" + + # Verify Firewall is active + output=$(firewall-cmd --list-services 2>&1) + file_log "INFO" "Active firewalls: $output" + + if echo "$output" | grep -q '\bssh\b' && + echo "$output" | grep -q '\bhttp\b' && + echo "$output" | grep -q '\bhttps\b'; then + return 0 else - file_log "SUCCESS" "Installed applications" + return 1 fi } -configure_ufw() { - console_log "INFO" "Starting UFW configuration..." - file_log "INFO" "Starting UFW configuration" +configure_firewall_freebsd() { + # Path to the new pf configuration file + PF_CONF_FILE="/etc/pf.conf" - # Check if UFW is installed - if ! command -v ufw >/dev/null 2>&1; then - file_log "ERROR" "UFW is not installed" - return 1 + # Create backup with timestamps + if [ -f "$PF_CONF_FILE" ]; then + PF_CONF_BACKUP_FILE="${PF_CONF_FILE}.bak.${TIMESTAMP}" + output=$(mv "$PF_CONF_FILE" "$PF_CONF_BACKUP_FILE" 2>&1) + file_log "INFO" "Backed up existing configuration to $PF_CONF_BACKUP_FILE" + file_log "INFO" "$output" fi - output=$(ufw allow ssh 2>&1) - if [ -n "$output" ]; then - file_log "INFO" "ufw allow ssh output: $output" + touch $PF_CONF_FILE + cat >>$PF_CONF_FILE <<'EOF' +# Network Hygiene: Normalize network packets +scrub in all + +# Do not filter on the loopback interface for performance +set skip on lo0 + +# Block all incoming and outgoing traffic by default +# Only those that are allowed subsequently will happen +block all + +# Allow all outgoing traffic and track connections +pass out all keep state + +# Allow incoming ssh, http, https traffic +pass in proto tcp from any to any port { ssh, http, https } keep state +EOF + + output=$(service pf start 2>&1) + file_log "INFO" "$output" + + # Verify rules and load configuration + output=$(pfctl -nf $PF_CONF_FILE 2>&1 && pfctl -vvf $PF_CONF_FILE 2>&1) + command_status=$? + file_log "INFO" "$output" + + # On config success, enable PF & pflog on boot + if [ $command_status -eq 0 ]; then + output=$(pfctl -e 2>/dev/null || true) + file_log "INFO" "PF Enabled: $output" + + # Enable the PF firewall service on boot + output=$(sysrc pf_enable="YES" 2>&1) + file_log "INFO" "$output" + + output=$(sysrc pf_rules="$PF_CONF_FILE" 2>&1) + file_log "INFO" "$output" + + # Enable logging for the firewall + output=$(sysrc pflog_enable="YES" 2>&1) + file_log "INFO" "$output" + + # Set pf logfile to /var/log/pflog + output=$(sysrc pflog_logfile="/var/log/pflog" 2>&1) + file_log "INFO" "$output" + + # Start pflog service + output=$(service pflog start 2>&1) + file_log "INFO" "$output" + + file_log "SUCCESS" "PF firewall configured" + + return $command_status + else # Error in PF configuration + console_log "ERROR" "PF firewall configuration failed" + file_log "ERROR" "PF firewall configuration failed" + + console_log "INFO" "Reverting PF configuration..." + file_log "INFO" "Reverting PF configuration..." + + if cp "$PF_CONF_BACKUP_FILE" "$PF_CONF_FILE" >/dev/null 2>&1; then + console_log "INFO" "Restored [ $PF_CONF_FILE ]" + file_log "INFO" "Restored [ $PF_CONF_FILE ]" + else + console_log "ERROR" "Failed to restore $PF_CONF_FILE" + file_log "ERROR" "Failed to restore $PF_CONF_FILE" + fi + + # Load original PF config + if pfctl -vvnf $PF_CONF_FILE >/dev/null 2>&1 && pfctl -f $PF_CONF_FILE >/dev/null 2>&1; then + console_log "INFO" "Restarted PF with original configuration" + file_log "INFO" "Restarted PF with original configuration" + else + console_log "ERROR" "Failed to restart PF even with original configuration" + file_log "ERROR" "Failed to restart PF even with original configuration" + fi + + return $command_status fi - output=$(ufw allow http 2>&1) - if [ -n "$output" ]; then - file_log "INFO" "ufw allow http output: $output" - fi - - output=$(ufw allow https 2>&1) - if [ -n "$output" ]; then - file_log "INFO" "ufw allow https output: $output" - fi - - # Enable UFW - output=$(echo "y" | ufw enable 2>&1) - if [ -n "$output" ]; then - file_log "INFO" "ufw enable output: $output" - fi - - # Verify UFW is active - output=$(ufw status 2>&1) - if [ -n "$output" ]; then - file_log "INFO" "ufw status output: $output" - fi - - if ! echo "$output" | grep -q "Status: active"; then - console_log "ERROR" "UFW is not active after enabling" - file_log "ERROR" "UFW is not active after enabling" - return 1 - fi - - console_log "SUCCESS" "UFW configuration completed successfully" - file_log "SUCCESS" "UFW configuration completed successfully" - return 0 + # TIP: Troubleshoot: + # List defined "rules": pfctl -s rules + # Debug rules: pfctl -vvsr + # Reset PF: pfctl -F all } -update_fail2ban_jail_local_file() { - search_term="$1" - new_value="$2" +configure_firewall() { + console_log "INFO" "Configuring firewall..." + file_log "INFO" "Configuring firewall..." - # We want to update the setting in the [DEFAULT] section - # [DEFAULT] section ends right before "# JAILS" - range_start="^\[DEFAULT\]$" - range_end="^# JAILS$" - file=$JAIL_LOCAL_FILE + if command -v firewall-cmd >/dev/null 2>&1; then # Linux + configure_firewall_linux + command_status=$? + elif [ -f /etc/freebsd-update.conf ]; then # FreeBSD + configure_firewall_freebsd + command_status=$? + else + console_log "ERROR" "Could not find required application for firewall configuration" + file_log "ERROR" "Could not find required application for firewall configuration" + return 1 + fi - # When the setting exists & it's NOT commented out -> comment it and add the new setting on next line - if sed -n "/${range_start}/,/${range_end}/p" "$file" | grep -q "^${search_term}[[:blank:]]*="; then - sed -ri "/${range_start}/,/${range_end}/ s/^(${search_term}[[:blank:]]*=.*)/#\1/" "$file" - sed -ri "/${range_start}/,/${range_end}/ s/^#${search_term}[[:blank:]]*=.*/&\n${search_term} = ${new_value}/" "$file" - else # If the setting is commented out or it doesn't exist -> add it after the commented line or at the end of the section - sed -ri "/${range_start}/,/${range_end}/ s/^#${search_term}[[:blank:]]*=.*/&\n${search_term} = ${new_value}/" "$file" + if [ $command_status -eq 0 ]; then + console_log "SUCCESS" "Firewall configured" + file_log "SUCCESS" "Firewall configured" + else + console_log "ERROR" "Failed to configure firewall" + file_log "ERROR" "Failed to configure firewall" + return 1 + fi +} + +fail2ban_jail_settings() { + JAIL_LOCAL=$1 + + # Backup jail.local if it exists + if [ -f "$JAIL_LOCAL" ]; then + JAIL_LOCAL_BACKUP="${JAIL_LOCAL}.bak.${TIMESTAMP}" + cp "$JAIL_LOCAL" "$JAIL_LOCAL_BACKUP" + file_log "INFO" "Created backup of existing jail.local at $JAIL_LOCAL_BACKUP" + fi + + # Get server's public IP + file_log "Getting server's public IP..." + PUBLIC_IP=$(curl -s -4 ifconfig.me 2>&1 || curl -s -4 icanhazip.com 2>&1 || curl -s -4 ipinfo.io/ip 2>&1) + file_log "INFO" "Server public IP: $PUBLIC_IP" + + file_log "INFO" "Adding jails to $JAIL_LOCAL..." + + cat <"$JAIL_LOCAL" +[DEFAULT] +backend = auto +banaction = firewallcmd-rich-rules[actiontype=] +banaction_allports = firewallcmd-rich-rules[actiontype=] +ignoreip = 127.0.0.1/8 ::1 $PUBLIC_IP +bantime = 1h +findtime = 10m +maxretry = 5 +# Action: ban only (action_) or ban and email (action_mwl) +action = %(action_)s + +# +# SSH Jail +# +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = %(sshd_log)s + /var/log/auth.log + /var/log/secure +maxretry = 5 +bantime = 1h +findtime = 10m + +# +# Nginx Bot Search - Blocks bots searching for vulnerabilities (404 errors) +# +[nginx-botsearch] +enabled = true +port = http,https +filter = nginx-botsearch +logpath = %(nginx_access_log)s + /var/log/nginx/access.log + $(dirname "$JAIL_LOCAL_BACKUP")/emptylog +maxretry = 5 +bantime = 6h +findtime = 10m + +# +# Nginx HTTP Authentication +# +[nginx-http-auth] +enabled = true +port = http,https +filter = nginx-http-auth +logpath = %(nginx_error_log)s + /var/log/nginx/error.log + $(dirname "$JAIL_LOCAL_BACKUP")/emptylog +maxretry = 3 +bantime = 6h +findtime = 10m + +# +# Nginx Limit Request (DDoS protection) +# +[nginx-limit-req] +enabled = true +port = http,https +filter = nginx-limit-req +logpath = %(nginx_error_log)s + /var/log/nginx/error.log + $(dirname "$JAIL_LOCAL_BACKUP")/emptylog +maxretry = 10 +bantime = 6h +findtime = 10m + +# +# HAProxy HTTP Authentication Failures +# +[haproxy-http-auth] +enabled = true +port = http,https +filter = haproxy-http-auth +logpath = /var/log/haproxy.log + /var/log/haproxy/haproxy.log + /var/log/haproxy/*.log + $(dirname "$JAIL_LOCAL_BACKUP")/emptylog +maxretry = 3 +bantime = 6h +findtime = 10m + +# +# Recidive Jail - Ban repeat offenders +# This jail monitors fail2ban.log for IPs that have been banned multiple times +# +[recidive] +enabled = true +filter = recidive +logpath = /var/log/fail2ban.log +banaction = %(banaction_allports)s +bantime = 1w +findtime = 1d +maxretry = 3 +FAIL2BAN + + # FreeBSD specific ban-actions + if [ -f /etc/pf.conf ]; then + sed -i.bak -E 's/(^banaction = )firewallcmd.*/\1pf[actiontype=]/' "$JAIL_LOCAL" + sed -i.bak -E 's/(^banaction_allports = )firewallcmd.*/\1pf[actiontype=]/' "$JAIL_LOCAL" + rm "$JAIL_LOCAL".bak >/dev/null 2>&1 + fi + + # Dummy logfile so the configuration doesn't fail + touch "$(dirname "$JAIL_LOCAL")"/emptylog && chmod 644 "$(dirname "$JAIL_LOCAL")"/emptylog + + file_log "INFO" "Jails added to $JAIL_LOCAL" +} + +revert_fail2ban_jail_file() { + if [ -f "$JAIL_LOCAL_BACKUP" ]; then + console_log "INFO" "Reverting jail.local using [ $JAIL_LOCAL_BACKUP ]..." + file_log "INFO" "Reverting jail.local using [ $JAIL_LOCAL_BACKUP ]..." + + if cp "$JAIL_LOCAL_BACKUP" "$JAIL_LOCAL" >/dev/null 2>&1; then + console_log "INFO" "Restored jail.local to original version" + file_log "INFO" "Restored jail.local to original version" + else + console_log "ERROR" "Failed to restore jail.local" + file_log "INFO" "Failed to restore jail.local" + return 1 + fi + else + # If no backup exists -> we created new jail.local file; delete it + console_log "INFO" "Removing jail.local..." + file_log "INFO" "Removing jail.local..." + rm -f "$JAIL_LOCAL" + console_log "INFO" "Removed jail.local file" + file_log "INFO" "Removed jail.local file" + fi + + # Try restarting fail2ban with original configuration + if manage_service fail2ban restart; then + console_log "INFO" "Restarted fail2ban with original configuration" + file_log "INFO" "Restarted fail2ban with original configuration" + else + console_log "ERROR" "Failed to restart fail2ban service even with original configuration" + file_log "ERROR" "Failed to restart fail2ban service even with original configuration" + return 1 + fi +} + +configure_fail2ban_linux() { + fail2ban_jail_settings "/etc/fail2ban/jail.local" + + # Restart fail2ban + if ! manage_service fail2ban restart; then # Error in configuration + console_log "ERROR" "Failed to restart fail2ban service" + file_log "ERROR" "Failed to restart fail2ban service" + + revert_fail2ban_jail_file + + return 1 + fi +} + +configure_fail2ban_freebsd() { + RC_CONF_FILE="/etc/rc.conf" + PF_CONF_FILE="/etc/pf.conf" + fail2ban_jail_settings "/usr/local/etc/fail2ban/jail.local" + + # Auto start fail2ban on boot and restart with new configuration + output=$(sysrc fail2ban_enable="YES" 2>&1 && manage_service fail2ban restart 2>&1) + command_output=$? + + file_log "INFO" "$output" + + # Revert fail2ban & rc.conf changes if fail2ban starting failed + if [ $command_output -ne 0 ]; then + file_log "ERROR" "Could not start fail2ban service. Reverting changes..." + + # Don't start fail2ban on boot + sed -i.bak '/^fail2ban_enable/d' $RC_CONF_FILE + rm "$RC_CONF_FILE".bak >/dev/null 2>&1 + + file_log "INFO" "Reverted $RC_CONF_FILE" + + revert_fail2ban_jail_file + + return 1 + fi + + if [ -f "$PF_CONF_FILE" ]; then + PF_CONF_BACKUP_FILE="${PF_CONF_FILE}.bak.${TIMESTAMP}" + output=$(mv "$PF_CONF_FILE" "$PF_CONF_BACKUP_FILE" 2>&1) + file_log "INFO" "Backed up existing configuration to $PF_CONF_BACKUP_FILE" + file_log "INFO" "$output" + fi + + # Add fail2ban table to PF configuration + if ! grep -q 'table ' "$PF_CONF_FILE" 2>/dev/null; then + cat >>"$PF_CONF_FILE" <<'EOF' + +# Fail2ban table and anchor +table persist +anchor "f2b/*" +block drop in quick from to any +EOF + fi + + # Verify rules and load configuration + output=$(pfctl -nf $PF_CONF_FILE 2>&1 && pfctl -vvf $PF_CONF_FILE 2>&1) + command_status=$? + file_log "INFO" "$output" + + if [ $command_output -ne 0 ]; then + console_log "ERROR" "Failed to restart pf post fail2ban. Reverting pf.config..." + file_log "ERROR" "Failed to restart pf post fail2ban. Reverting pf.config..." + + if cp "$PF_CONF_BACKUP_FILE" "$PF_CONF_FILE" >/dev/null 2>&1; then + console_log "INFO" "Restored pf.conf to original version" + file_log "INFO" "Restored pf.conf to original version" + else + console_log "ERROR" "Failed to restore pf.conf" + file_log "INFO" "Failed to restore pf.conf" + return 1 + fi + + # Try restarting pf with original configuration + if pfctl -f $PF_CONF_FILE >/dev/null 2>&1; then + console_log "INFO" "Restarted pf with original configuration" + file_log "INFO" "Restarted pf with original configuration" + else + console_log "ERROR" "Failed to restart pf even with original configuration" + file_log "ERROR" "Failed to restart pf even with original configuration" + fi + + return 1 fi } configure_fail2ban() { - console_log "INFO" "Starting Fail2ban configuration..." - file_log "INFO" "Starting Fail2ban configuration" + console_log "INFO" "Configuring Fail2ban..." + file_log "INFO" "Configuring Fail2ban..." - if ! command -v fail2ban-client >/dev/null 2>&1; then - file_log "ERROR" "Fail2ban is not installed" + if command -v firewall-cmd >/dev/null 2>&1; then # Linux + configure_fail2ban_linux + command_status=$? + elif [ -f /etc/pf.conf ]; then # FreeBSD + configure_fail2ban_freebsd + command_status=$? + fi + + if [ "$command_status" -eq 0 ]; then + console_log "SUCCESS" "Configured Fail2ban" + file_log "SUCCESS" "Configured Fail2ban" + else + console_log "ERROR" "Fail2ban configuration unsuccessful" + file_log "ERROR" "Fail2ban configuration unsuccessful" return 1 fi - - JAIL_LOCAL_FILE="/etc/fail2ban/jail.local" - DEFAULT_JAIL_CONF_FILE="/etc/fail2ban/jail.conf" - CUSTOM_JAILS_FILE="/etc/fail2ban/jail.d/custom-enabled.conf" - - # Backup jail.local if it exists - if [ -f "$JAIL_LOCAL_FILE" ]; then - JAIL_LOCAL_BACKUP_FILE="${JAIL_LOCAL_FILE}.bak.${TIMESTAMP}" - cp "$JAIL_LOCAL_FILE" "$JAIL_LOCAL_BACKUP_FILE" - file_log "INFO" "Created backup of existing jail.local at $JAIL_LOCAL_BACKUP_FILE" - else # Copy jail.conf to jail.local if jail.local doesn't exist - if [ -f "$DEFAULT_JAIL_CONF_FILE" ]; then - cp "$DEFAULT_JAIL_CONF_FILE" "$JAIL_LOCAL_FILE" - file_log "INFO" "Created jail.local from jail.conf" - else - console_log "ERROR" "Neither jail.conf nor jail.local exists" - file_log "ERROR" "Neither jail.conf nor jail.local exists" - return 1 - fi - fi - - # Fetch public IP using ipinfo.io/ip - file_log "INFO" "Attempting to get server's public IP" - output=$(curl -s -4 ifconfig.me 2>&1 || curl -s -4 icanhazip.com 2>&1 || curl -s -4 ipinfo.io/ip 2>&1) - if [ -z "$output" ]; then - console_log "ERROR" "Could not determine server's public IP" - file_log "ERROR" "Could not determine server's public IP" - PUBLIC_IP="" - else - PUBLIC_IP="$output" - file_log "INFO" "Server public IP: $PUBLIC_IP" - fi - - # Update default settings in jail.local - update_fail2ban_jail_local_file "bantime" "5h" - update_fail2ban_jail_local_file "backend" "systemd" - update_fail2ban_jail_local_file "ignoreip" "127.0.0.1\/8 ::1 $PUBLIC_IP" - - # Enable jails and more settings for them in /etc/fail2ban/jail.d/custom-enabled.conf - file_log "INFO" "Enabling jails in $CUSTOM_JAILS_FILE" - cat <$CUSTOM_JAILS_FILE -[sshd] -enabled = true -filter = sshd -bantime = 1d -maxretry = 3 - -[nginx-http-auth] -enabled = true -logpath = /var/log/nginx/error.log -maxretry = 3 - -# Repeat offenders across all other jails -[recidive] -enabled = true -filter = recidive -findtime = 1d -bantime = 30d -maxretry = 50 -FAIL2BAN - - if ! manage_service fail2ban restart; then - - console_log "ERROR" "Failed to restart fail2ban service" - file_log "ERROR" "Failed to restart fail2ban service" - - # Revert jail.local to backup if it exists - if [ -f "$JAIL_LOCAL_BACKUP_FILE" ]; then - console_log "INFO" "Reverting jail.local to backup..." - file_log "INFO" "Reverting jail.local to backup from: $JAIL_LOCAL_BACKUP_FILE" - - if ! cp "$JAIL_LOCAL_BACKUP_FILE" "$JAIL_LOCAL_FILE"; then - console_log "ERROR" "Failed to restore jail.local backup" - file_log "ERROR" "Failed to restore jail.local backup" - exit 1 - fi - else # If no backup exists (i.e, jail.local was created from jail.conf) -> remove jail.local - console_log "INFO" "Removing newly created jail.local..." - file_log "INFO" "Removing newly created jail.local" - rm -f "$JAIL_LOCAL_FILE" - fi - - # Remove the custom enabled configuration - if [ -f "$CUSTOM_JAILS_FILE" ]; then - console_log "INFO" "Removing custom jail configuration..." - file_log "INFO" "Removing custom jail configuration: $CUSTOM_JAILS_FILE" - rm -f "$CUSTOM_JAILS_FILE" - fi - - # Try restarting fail2ban with original configuration - if ! manage_service fail2ban restart; then - console_log "ERROR" "Failed to restart fail2ban service even with original configuration" - file_log "ERROR" "Failed to restart fail2ban service even with original configuration" - exit 1 - fi - - console_log "INFO" "Fail2ban restarted with original configuration" - file_log "INFO" "Fail2ban restarted with original configuration" - exit 1 - fi - - console_log "SUCCESS" "Fail2ban configuration completed successfully" - file_log "SUCCESS" "Fail2ban configuration completed successfully" - return 0 } main() { @@ -776,11 +1028,14 @@ main() { print_operation_details print_log_file_details + + echo echo "Press [Enter] to continue. [Ctrl + c] to cancel..." # shellcheck disable=SC2162,SC2034 read dummy # Log script start + START_TIME=$(date +%s) console_log "INFO" "Starting $SCRIPT_NAME v$SCRIPT_VERSION..." file_log "INFO" "Starting $SCRIPT_NAME v$SCRIPT_VERSION..." @@ -793,9 +1048,11 @@ main() { # Step 2: Create new user if [ -n "$USERNAME" ]; then if ! create_user; then + print_log_file_details return 1 # Abort on error fi if ! user_privileged_access; then + print_log_file_details return 1 # Abort on error fi fi @@ -808,52 +1065,45 @@ main() { fi if ! generate_ssh_key "$SSH_KEY_USER"; then - console_log "ERROR" "Failed to generate SSH key for $SSH_KEY_USER" + console_log "ERROR" "Failed to generate SSH key for [ $SSH_KEY_USER ]" + print_log_file_details return 1 # Abort on error fi # Step 4: Configure SSH if ! harden_ssh_config; then console_log "ERROR" "Failed to update ssh configuration to harden it" + print_log_file_details return 1 # Abort on error fi # Step 5: Install required packages if ! install_packages; then + print_log_file_details return 1 # Abort on error fi - # Step 6: Configure UFW - console_log "INFO" "Configuring UFW..." - file_log "INFO" "Configuring UFW..." - if ! configure_ufw; then - console_log "ERROR" "Failed to configure UFW" - print_logfile_details + # Step 6: Configure Firewall + if ! configure_firewall; then + print_log_file_details return 1 # Abort on error fi - console_log "SUCCESS" "Successfully configured UFW" - file_log "SUCCESS" "Successfully configured UFW" # Step 7: Configure Fail2ban - console_log "INFO" "Configuring Fail2ban..." - file_log "INFO" "Configuring Fail2ban..." if ! configure_fail2ban; then - console_log "ERROR" "Failed to configure Fail2ban" - print_logfile_details + print_log_file_details return 1 # Abort on error fi - console_log "SUCCESS" "Successfully configured Fail2ban" - file_log "SUCCESS" "Successfully configured Fail2ban" - console_log "SUCCESS" "Script completed successfully" - file_log "SUCCESS" "Script completed successfully" + console_log "SUCCESS" "All Done" + file_log "SUCCESS" "All Done" # Calculate and show execution time FORMATTED_DURATION=$(formatted_execution_duration) - console_log "INFO" "Total execution time: $FORMATTED_DURATION" - file_log "INFO" "Total execution time: $FORMATTED_DURATION" + console_log "INFO" "Total execution time: [ $FORMATTED_DURATION ]" + file_log "INFO" "Total execution time: [ $FORMATTED_DURATION ]" - print_logfile_details + print_log_file_details return 0 }