• Plesk Uservoice will be deprecated by October. Moving forward, all product feature requests and improvement suggestions will be managed through our new platform Plesk Productboard.
    To continue sharing your ideas and feedback, please visit features.plesk.com

Input Spamhaus (e)DROP Script for iptables

AbramS

Basic Pleskian
While migrating my Plesk server due to an OS upgrade last year, I pulled together the scripts that I wrote/modified and added some better documentation. I figured I would share these here, as there are probably some among you that can benefit from them. Hope it's helpful.
Note: I've added links to inspiration/authors wherever possible. But I couldn't quite trace the origins of some of these scripts. Happy to add a name if another author is found.

In this post I'm sharing the two scripts that I use for adding Spamhaus DROP and eDROP lists into iptables. They're basically the same scripts, just calling a different source file.

Spamhaus DROP script for iptables
Bash:
#!/bin/bash

# based off the following two scripts
# http://www.theunsupported.com/2012/07/block-malicious-ip-addresses/
# http://www.cyberciti.biz/tips/block-spamming-scanning-with-iptables.html
# Inspyr Media 2020.

# path to iptables
IPTABLES="/sbin/iptables";

# list of known spammers
URL="www.spamhaus.org/drop/drop.lasso";

# save local copy here
FILE="/tmp/drop.lasso";

# iptables custom chain
CHAIN="SpamhausDROP";

# check to see if the chain already exists
$IPTABLES -L $CHAIN -n

# check to see if the chain already exists
if [ $? -eq 0 ]; then

    # flush the old rules
    $IPTABLES -F $CHAIN

    echo "Flushed old rules. Applying updated Spamhaus list...."

else

    # create a new chain set
    $IPTABLES -N $CHAIN

    # tie chain to input rules so it runs
    $IPTABLES -A INPUT -j $CHAIN

    # don't allow this traffic through
    $IPTABLES -A FORWARD -j $CHAIN

    echo "Chain not detected. Creating new chain and adding Spamhaus list...."

fi;

# get a copy of the spam list
wget -qc $URL -O $FILE

# iterate through all known spamming hosts
for IP in $( cat $FILE | egrep -v '^;' | awk '{ print $1}' ); do

    # add the ip address log rule to the chain
    $IPTABLES -A $CHAIN -p 0 -s $IP -j LOG --log-prefix "[SPAMHAUS BLOCK]" -m limit --limit 3/min --limit-burst 10

    # add the ip address to the chain
    $IPTABLES -A $CHAIN -p 0 -s $IP -j DROP

    echo $IP

done

echo "Done!"

# remove the spam list
unlink $FILE
exit 0

Spamhaus eDROP script for iptables
Bash:
#!/bin/bash

# based off the following two scripts
# http://www.theunsupported.com/2012/07/block-malicious-ip-addresses/
# http://www.cyberciti.biz/tips/block-spamming-scanning-with-iptables.html
# Inspyr Media 2020.

# path to iptables
IPTABLES="/sbin/iptables";

# list of known spammers
URL="www.spamhaus.org/drop/edrop.lasso";

# save local copy here
FILE="/tmp/edrop.lasso";

# iptables custom chain
CHAIN="SpamhausEDROP";

# check to see if the chain already exists
$IPTABLES -L $CHAIN -n

# check to see if the chain already exists
if [ $? -eq 0 ]; then

    # flush the old rules
    $IPTABLES -F $CHAIN

    echo "Flushed old rules. Applying updated Spamhaus list...."

else

    # create a new chain set
    $IPTABLES -N $CHAIN

    # tie chain to input rules so it runs
    $IPTABLES -A INPUT -j $CHAIN

    # don't allow this traffic through
    $IPTABLES -A FORWARD -j $CHAIN

    echo "Chain not detected. Creating new chain and adding Spamhaus list...."

fi;

# get a copy of the spam list
wget -qc $URL -O $FILE

# iterate through all known spamming hosts
for IP in $( cat $FILE | egrep -v '^;' | awk '{ print $1}' ); do

    # add the ip address log rule to the chain
    $IPTABLES -A $CHAIN -p 0 -s $IP -j LOG --log-prefix "[SPAMHAUS BLOCK]" -m limit --limit 3/min --limit-burst 10

    # add the ip address to the chain
    $IPTABLES -A $CHAIN -p 0 -s $IP -j DROP

    echo $IP

done

echo "Done!"

# remove the spam list
unlink $FILE
exit 0

Disclaimer: I'm not a full-time coder and created/modified these to help solve problems that I encountered. There's probably cleaner/more efficient ways of doing this, so feel free to share any improvements.
 
Updated script for 2024: eDROP is no longer a thing as it has been merged with DROP, and the DROP file has a new URL. Also rewrote the script with more validation and logging. It also uses fail2ban instead of directly writing to IPTables, making the results visible in Plesk. Note: You will have to create a jail called spamhaus-drop. I have it setup to expire once a week, but it really doesn't matter as I run this script daily via crontab.

Bash:
#!/bin/bash

# Constants
DROP_URL="https://www.spamhaus.org/drop/drop.txt"
DROP_FILE="/tmp/drop.txt"
JAIL_NAME="spamhaus-drop"
LOG_FILE="/var/log/spamhaus_update.log"

# Reset the log file
> "$LOG_FILE" || echo "Failed to reset log file: $LOG_FILE"

# Function to log messages
log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

# Function to exit on failure
exit_on_failure() {
    log_message "ERROR: $1"
    exit 1
}

# Function to validate IP addresses (IPv4 and CIDR)
validate_ip() {
    local ip=$1
    local ip_regex="^(([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?)$"
    [[ $ip =~ $ip_regex ]]
}

# Download the Spamhaus DROP list
log_message "Downloading Spamhaus DROP list..."
curl -s -o "$DROP_FILE" "$DROP_URL"

# Check if the file was downloaded successfully and is not empty
if [[ -s "$DROP_FILE" ]]; then
    log_message "Download successful. File is not empty."
else
    exit_on_failure "Download failed or file is empty."
fi

# Extract "Last-Modified" and "Expires" values
last_modified=$(grep "^; Last-Modified:" "$DROP_FILE" | cut -d':' -f2- | xargs)
expires=$(grep "^; Expires:" "$DROP_FILE" | cut -d':' -f2- | xargs)

# Log the extracted values
log_message "Last-Modified: $last_modified"
log_message "Expires: $expires"

# Unban all IPs and reload the spamhaus-drop jail
log_message "Unbanning all IPs and reloading jail: $JAIL_NAME..."
fail2ban-client reload --unban "$JAIL_NAME" || exit_on_failure "Failed to unban IPs in $JAIL_NAME."

# Add all IPs from drop.txt to the spamhaus-drop jail
log_message "Adding IPs from $DROP_FILE to jail: $JAIL_NAME..."
last_ip=""
while IFS= read -r line; do
    # Skip comments and empty lines
    if [[ "$line" =~ ^\; ]] || [[ -z "$line" ]]; then
        continue
    fi

    # Extract the IP or CIDR range (remove the space and everything after the ;)
    ip=$(echo "$line" | awk -F' ;' '{print $1}')
    last_ip="$ip"  # Keep track of the last IP/CIDR

    # Validate the IP or CIDR range
    if validate_ip "$ip"; then
        # Add the IP or CIDR to the jail
        fail2ban-client set "$JAIL_NAME" banip "$ip" || log_message "Failed to ban IP: $ip"
        log_message "Successfully banned IP: $ip"
    else
        log_message "Invalid IP skipped: $ip"
    fi
done < "$DROP_FILE"

# Final check: Ensure the last IP/CIDR was added
if fail2ban-client status "$JAIL_NAME" | grep -q "$last_ip"; then
    log_message "The last IP/CIDR ($last_ip) was successfully added to the jail."
    log_message "Update completed successfully."
else
    exit_on_failure "Failed to add the last IP/CIDR ($last_ip) to the jail. Update completed with errors."
fi

# Cleanup: Remove the drop.txt file
log_message "Cleaning up temporary file..."
rm -f "$DROP_FILE" || log_message "Failed to remove temporary file $DROP_FILE."
log_message "Temporary file $DROP_FILE has been removed."
 
Updated script for 2024: eDROP is no longer a thing as it has been merged with DROP, and the DROP file has a new URL. Also rewrote the script with more validation and logging. It also uses fail2ban instead of directly writing to IPTables, making the results visible in Plesk. Note: You will have to create a jail called spamhaus-drop. I have it setup to expire once a week, but it really doesn't matter as I run this script daily via crontab.

Bash:
#!/bin/bash

# Constants
DROP_URL="https://www.spamhaus.org/drop/drop.txt"
DROP_FILE="/tmp/drop.txt"
JAIL_NAME="spamhaus-drop"
LOG_FILE="/var/log/spamhaus_update.log"

# Reset the log file
> "$LOG_FILE" || echo "Failed to reset log file: $LOG_FILE"

# Function to log messages
log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

# Function to exit on failure
exit_on_failure() {
    log_message "ERROR: $1"
    exit 1
}

# Function to validate IP addresses (IPv4 and CIDR)
validate_ip() {
    local ip=$1
    local ip_regex="^(([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?)$"
    [[ $ip =~ $ip_regex ]]
}

# Download the Spamhaus DROP list
log_message "Downloading Spamhaus DROP list..."
curl -s -o "$DROP_FILE" "$DROP_URL"

# Check if the file was downloaded successfully and is not empty
if [[ -s "$DROP_FILE" ]]; then
    log_message "Download successful. File is not empty."
else
    exit_on_failure "Download failed or file is empty."
fi

# Extract "Last-Modified" and "Expires" values
last_modified=$(grep "^; Last-Modified:" "$DROP_FILE" | cut -d':' -f2- | xargs)
expires=$(grep "^; Expires:" "$DROP_FILE" | cut -d':' -f2- | xargs)

# Log the extracted values
log_message "Last-Modified: $last_modified"
log_message "Expires: $expires"

# Unban all IPs and reload the spamhaus-drop jail
log_message "Unbanning all IPs and reloading jail: $JAIL_NAME..."
fail2ban-client reload --unban "$JAIL_NAME" || exit_on_failure "Failed to unban IPs in $JAIL_NAME."

# Add all IPs from drop.txt to the spamhaus-drop jail
log_message "Adding IPs from $DROP_FILE to jail: $JAIL_NAME..."
last_ip=""
while IFS= read -r line; do
    # Skip comments and empty lines
    if [[ "$line" =~ ^\; ]] || [[ -z "$line" ]]; then
        continue
    fi

    # Extract the IP or CIDR range (remove the space and everything after the ;)
    ip=$(echo "$line" | awk -F' ;' '{print $1}')
    last_ip="$ip"  # Keep track of the last IP/CIDR

    # Validate the IP or CIDR range
    if validate_ip "$ip"; then
        # Add the IP or CIDR to the jail
        fail2ban-client set "$JAIL_NAME" banip "$ip" || log_message "Failed to ban IP: $ip"
        log_message "Successfully banned IP: $ip"
    else
        log_message "Invalid IP skipped: $ip"
    fi
done < "$DROP_FILE"

# Final check: Ensure the last IP/CIDR was added
if fail2ban-client status "$JAIL_NAME" | grep -q "$last_ip"; then
    log_message "The last IP/CIDR ($last_ip) was successfully added to the jail."
    log_message "Update completed successfully."
else
    exit_on_failure "Failed to add the last IP/CIDR ($last_ip) to the jail. Update completed with errors."
fi

# Cleanup: Remove the drop.txt file
log_message "Cleaning up temporary file..."
rm -f "$DROP_FILE" || log_message "Failed to remove temporary file $DROP_FILE."
log_message "Temporary file $DROP_FILE has been removed."
Great job! Please give an example of how to configure spamhaus-drop Jail in /etc/fail2ban/jail.conf and /etc/fail2ban/filter.d/spamhaus-drop.conf (if needed)
 
Updated script for 2024: eDROP is no longer a thing as it has been merged with DROP, and the DROP file has a new URL. Also rewrote the script with more validation and logging. It also uses fail2ban instead of directly writing to IPTables, making the results visible in Plesk. Note: You will have to create a jail called spamhaus-drop. I have it setup to expire once a week, but it really doesn't matter as I run this script daily via crontab.

Bash:
#!/bin/bash

# Constants
DROP_URL="https://www.spamhaus.org/drop/drop.txt"
DROP_FILE="/tmp/drop.txt"
JAIL_NAME="spamhaus-drop"
LOG_FILE="/var/log/spamhaus_update.log"

# Reset the log file
> "$LOG_FILE" || echo "Failed to reset log file: $LOG_FILE"

# Function to log messages
log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

# Function to exit on failure
exit_on_failure() {
    log_message "ERROR: $1"
    exit 1
}

# Function to validate IP addresses (IPv4 and CIDR)
validate_ip() {
    local ip=$1
    local ip_regex="^(([0-9]{1,3}\.){3}[0-9]{1,3}(\/[0-9]{1,2})?)$"
    [[ $ip =~ $ip_regex ]]
}

# Download the Spamhaus DROP list
log_message "Downloading Spamhaus DROP list..."
curl -s -o "$DROP_FILE" "$DROP_URL"

# Check if the file was downloaded successfully and is not empty
if [[ -s "$DROP_FILE" ]]; then
    log_message "Download successful. File is not empty."
else
    exit_on_failure "Download failed or file is empty."
fi

# Extract "Last-Modified" and "Expires" values
last_modified=$(grep "^; Last-Modified:" "$DROP_FILE" | cut -d':' -f2- | xargs)
expires=$(grep "^; Expires:" "$DROP_FILE" | cut -d':' -f2- | xargs)

# Log the extracted values
log_message "Last-Modified: $last_modified"
log_message "Expires: $expires"

# Unban all IPs and reload the spamhaus-drop jail
log_message "Unbanning all IPs and reloading jail: $JAIL_NAME..."
fail2ban-client reload --unban "$JAIL_NAME" || exit_on_failure "Failed to unban IPs in $JAIL_NAME."

# Add all IPs from drop.txt to the spamhaus-drop jail
log_message "Adding IPs from $DROP_FILE to jail: $JAIL_NAME..."
last_ip=""
while IFS= read -r line; do
    # Skip comments and empty lines
    if [[ "$line" =~ ^\; ]] || [[ -z "$line" ]]; then
        continue
    fi

    # Extract the IP or CIDR range (remove the space and everything after the ;)
    ip=$(echo "$line" | awk -F' ;' '{print $1}')
    last_ip="$ip"  # Keep track of the last IP/CIDR

    # Validate the IP or CIDR range
    if validate_ip "$ip"; then
        # Add the IP or CIDR to the jail
        fail2ban-client set "$JAIL_NAME" banip "$ip" || log_message "Failed to ban IP: $ip"
        log_message "Successfully banned IP: $ip"
    else
        log_message "Invalid IP skipped: $ip"
    fi
done < "$DROP_FILE"

# Final check: Ensure the last IP/CIDR was added
if fail2ban-client status "$JAIL_NAME" | grep -q "$last_ip"; then
    log_message "The last IP/CIDR ($last_ip) was successfully added to the jail."
    log_message "Update completed successfully."
else
    exit_on_failure "Failed to add the last IP/CIDR ($last_ip) to the jail. Update completed with errors."
fi

# Cleanup: Remove the drop.txt file
log_message "Cleaning up temporary file..."
rm -f "$DROP_FILE" || log_message "Failed to remove temporary file $DROP_FILE."
log_message "Temporary file $DROP_FILE has been removed."

@AbramS,

I have just noticed this script, which seems to be sufficient in many ways.

Nevertheless, it is more convenient to load the DROP list into a custom Nginx conf with the format

deny [ IP1 ];
deny [ IP2 ];
...
deny [ IPn ];

and refresh the Nginx conf on a frequent basis, preferably on a daily basis.

A cronjob can be easily created for that.


The above is just mentioned for the simple reason that one does not have to rely on Fail2Ban, which Fail2Ban can cause performance issues (and in some rare cases, these performance issues can make the server vulnerable to attacks, even from the IPs that one wishes to ban from the DROP list).

In addition, it must be noted that it is even better to add the IPs to a hosts.deny file.


All of the above is just some food for thought.


Kind regards....
 
Not sure about that, because nothing can be faster as banning an IP via iptables (or even better: ipsets). Because there, incoming packets will be filtered by the kernel with minimal effort. When you first allow the traffic in and, then handle it by a service on the system, you already have a much higher cpu load impact than when you block it on the front line of defense.
 
Not sure about that, because nothing can be faster as banning an IP via iptables (or even better: ipsets). Because there, incoming packets will be filtered by the kernel with minimal effort. When you first allow the traffic in and, then handle it by a service on the system, you already have a much higher cpu load impact than when you block it on the front line of defense.
@Bitpalast

I fully agree, that is why I suggested the Nginx route.

The iptables route is better and the hosts.deny should be even more better.

Nevertheless, the iptables route can be quite cumbersome and dangerous to some extent, if the DROP list changes and requires some iptables adjustments.

I do not really see many changes in the DROP list, but any change might require a more elaborate script to keep iptables updated and working properly.

So, I full agree with you, but I do want to add the "matter of convenience" into the considerations.

Kind regards...
 
Back
Top