Menü schliessen
Created: July 30th 2024
Last updated: August 14th 2024
Categories: Linux
Author: Marcus Fleuti

Dovecot Defender: Multi-Domain IP Banning! GeoIP-Smart Script Catches Brute Force Attacks Across Mail Sender Domains. Boost your E-Mail Security!

Donation Section: Background
Monero Badge: QR-Code
Monero Badge: Logo Icon Donate with Monero Badge: Logo Text
82uymVXLkvVbB4c4JpTd1tYm1yj1cKPKR2wqmw3XF8YXKTmY7JrTriP4pVwp2EJYBnCFdXhLq4zfFA6ic7VAWCFX5wfQbCC

Ultimate Dovecot Security: Automate IP Banning Across Multiple Domains

In today's digital landscape, securing your email server is more crucial than ever. Dovecot, a popular IMAP and POP3 server, is a prime target for malicious actors. To combat this, we've developed a powerful bash script that automates IP banning across multiple domains, significantly enhancing your Dovecot security. Let's dive into how this script works and how you can implement it to protect your email infrastructure.

What Does This Script Do?

Our Dovecot multi-domain IP banning script is a robust solution designed to:

  • Analyze Dovecot logs for failed login attempts
  • Identify IPs attempting to access multiple domains
  • Use GeoIP data to apply different thresholds based on country
  • Integrate with fail2ban to automatically ban malicious IPs
  • Handle log rotation to ensure continuous protection

By automating these processes, the script provides a powerful first line of defense against brute force attacks and unauthorized access attempts.

Dependencies and Setup

Before we dive into the script itself, let's cover the dependencies and setup required:

  • AWK: Used for text processing (usually pre-installed on most Unix-like systems)
  • fail2ban: For IP banning integration
  • GeoIP database and mmdblookup: For country-based IP lookups
  • CRON: For scheduling regular script execution

To install fail2ban on a Debian-compatible distribution, use:

sudo apt-get update
sudo apt-get install fail2ban

Configuring fail2ban

Create the fail2ban filter in:/etc/fail2ban/filter.d/empty.conf

[Definition]
failregex = 
ignoreregex =

Set up the fail2ban jail in:/etc/fail2ban/jail.d/dovecot-multidomain.conf

[dovecot-multidomain]
enabled = true
port = 110,143,993,995
filter = empty
logpath = /dev/null
maxretry = 0
findtime = 86400
bantime = 86400

After configuring, restart fail2ban:

sudo systemctl restart fail2ban

In this configuration, 86400 represents 24 hours in seconds. Adjust findtime and bantime as needed for your security requirements.

Setting Up GeoIP Dependencies

Our Dovecot multi-domain IP banning script relies on GeoIP data to apply country-specific thresholds. Here's how to set up the necessary GeoIP dependencies:

1. Install Required Packages

First, install the mmdb-bin and geoipupdate packages:

sudo apt-get update
sudo apt-get install mmdb-bin geoipupdate

2. Configure GeoIP Updates

Edit the GeoIP configuration file:

sudo nano /etc/GeoIP.conf

Add your MaxMind account ID and license key (you'll need to create a free account at maxmind.com):

AccountID YOUR_ACCOUNT_ID
LicenseKey YOUR_LICENSE_KEY
EditionIDs GeoLite2-Country GeoLite2-City

3. Perform Initial Database Update

Run the following command to download the latest GeoIP databases:

sudo geoipupdate

4. Set Up Automatic Updates

To keep your GeoIP data current, set up a weekly cron job. Open your crontab:

sudo crontab -e

Add the following line to run geoipupdate every Monday at 1 AM:

0 1 * * 1 /usr/bin/geoipupdate

With these steps completed, your system will have up-to-date GeoIP data, allowing our script to accurately apply country-specific thresholds for IP banning.

Verifying GeoIP Functionality

To ensure GeoIP is working correctly, you can test it with an IP address:

mmdblookup --file /usr/share/GeoIP/GeoLite2-Country.mmdb --ip 8.8.8.8 country names en

This should return the country name for the given IP address (in this case, "United States").

By properly setting up and maintaining your GeoIP database, you enhance the effectiveness of the Dovecot multi-domain IP banning script, allowing for more nuanced and accurate security measures based on geographical data.

The Script

Latest script version on GitHub

Please check out our GitHub repository for the latest version of the script:

Go to GitHub Repo →

Now, let's break down our Dovecot multi-domain IP banning script and explain its key components.

#!/bin/bash

# Configuration
DOVECOT_LOG="/var/log/dovecot-info.log"  # The path to your dovecot-info.log file
DOVECOT_LOG_1="/var/log/dovecot-info.log.1"  # Previous log file for rotation handling
BAN_LOG="/var/log/dovecot-multidomain-ip-ban.log"
AMOUNTOFHOURSTOCHECK=24  # Number of hours to look back for failed logins
JAIL_NAME="dovecot-multidomain"  # Fail2Ban jail name
GEOIP_BIN=$(which mmdblookup)  # Path to GeoIP lookup binary
COUNTRY_DB="/var/lib/GeoIP/GeoLite2-Country.mmdb"  # GeoIP database file
HIGH_THRESHOLD_COUNTRIES="CH LI CA"  # Countries with higher threshold for banning
LOW_THRESHOLD=2  # Minimum number of domains for most countries
HIGH_THRESHOLD=4  # Minimum number of domains for high-threshold countries
MAX_AMOUNT_LINES=600000  # Maximum lines to fetch from log files. Lower values improve speed but may miss entries with large logs or longer check periods. Adjust based on log volume and AMOUNTOFHOURSTOCHECK.
WHITELIST="127.0.0.1 178.22.109.64"  # IPs that should never be banned

# Console output control:
# Set CONSOLE_DEBUG_OUTPUT to 1 to enable any console output, 0 to disable all console output
CONSOLE_DEBUG_OUTPUT=1
# Set CONSOLE_DEBUG_OUTPUT_VERBOSE to 1 for full console output (all skipped, banned, and already banned messages),
# or 0 to show only newly banned IPs. This setting has no effect if CONSOLE_DEBUG_OUTPUT is set to 0.
CONSOLE_DEBUG_OUTPUT_VERBOSE=0

# Function to log messages with timestamp and log level as well as taking care of CLI debug output
log_message() {
    local level=$1
    shift
    local message="$@"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" >> "$BAN_LOG"

    # Control console output based on debug settings
    if [ "$CONSOLE_DEBUG_OUTPUT" -eq 1 ]; then
        if [ "$CONSOLE_DEBUG_OUTPUT_VERBOSE" -eq 1 ] || [[ "$message" == BANNED* ]] || [[ "$level" == "ERROR" ]]; then
            echo "[$timestamp] [$level] $message"
        fi
    fi
}

# Function to get country code from IP using GeoIP database
get_country_code() {
    local ip=$1
    ${GEOIP_BIN} -f ${COUNTRY_DB} -i "$ip" country iso_code | awk -F'"' '{print $2}' | xargs
}

# Function to determine minimum domains required for banning based on country
get_min_domains() {
    local country=$1
    if [[ " $HIGH_THRESHOLD_COUNTRIES " =~ " $country " ]]; then
        echo $HIGH_THRESHOLD
    else
        echo $LOW_THRESHOLD
    fi
}

# Function to check if an IP is already banned
is_ip_banned() {
    local ip=$1
    fail2ban-client status $JAIL_NAME | grep -q "$ip"
    return $?
}

log_message "INFO" "Script execution start"

# Build the regex pattern for the last X hours
FAILED_LOGIN_PATTERN=""
for (( i = ${AMOUNTOFHOURSTOCHECK}; i > 0; i-- )); do
    hour=$(date -d "-${i} hour" '+%Y-%m-%d %H')
    FAILED_LOGIN_PATTERN+="(${hour}.*auth failed)|"
done
FAILED_LOGIN_PATTERN=${FAILED_LOGIN_PATTERN%|}  # Remove trailing '|'

# Fetch all failed login attempts from the log
current_day=$(date +%d)
current_hour=$(date +%H)
if [ "$current_day" -eq 1 ] && [ "$current_hour" -lt "$AMOUNTOFHOURSTOCHECK" ]; then
    # Include both current and previous log files
    FAILED_LOGINS=$(tail -q -n ${MAX_AMOUNT_LINES} "$DOVECOT_LOG_1" "$DOVECOT_LOG" | grep -h -E "$FAILED_LOGIN_PATTERN")
else
    # Only use current log file
    FAILED_LOGINS=$(tail -q -n ${MAX_AMOUNT_LINES} "$DOVECOT_LOG" | grep -h -E "$FAILED_LOGIN_PATTERN")
fi

# Count total failed logins and unique IPs
FAILED_LOGIN_COUNT=$(echo "$FAILED_LOGINS" | wc -l)
UNIQUE_IP_COUNT=$(echo "$FAILED_LOGINS" | grep -oP 'rip=\K[0-9.]+' | sort -u | wc -l)

log_message "INFO" "Amount of failed logins found: $FAILED_LOGIN_COUNT"
log_message "INFO" "Number of unique IPs: $UNIQUE_IP_COUNT"

# Process failed logins
echo "$FAILED_LOGINS" | while read -r line; do
    ip=$(echo "$line" | grep -oP 'rip=\K[0-9.]+')
    user=$(echo "$line" | grep -oP 'user=<\K[^>]+')
    timestamp=$(echo "$line" | awk '{print $1, $2}')

    # Check if the user field contains a domain
    if [[ $user == *@* ]]; then
        domain=$(echo "$user" | cut -d'@' -f2)
    else
        domain="no_domain.tld"
    fi

    if [ -n "$ip" ] && [ -n "$domain" ] && [ -n "$timestamp" ]; then
        echo "$ip $domain $timestamp"
    fi
done | sort | uniq | awk '
{
    ip[$1] = $1
    domains[$1][$2] = 1
    if (!timestamp[$1] || $3 " " $4 < timestamp[$1]) {
        timestamp[$1] = $3 " " $4
    }
}
END {
    for (i in ip) {
        printf "%s: ", i
        domain_list = ""
        for (d in domains[i]) {
            domain_list = domain_list d " "
        }
        printf "%s|%s\n", domain_list, timestamp[i]
    }
}' | while read -r line; do
    # Extract information for each IP
    ip=$(echo "$line" | cut -d':' -f1)
    domain_info=$(echo "$line" | cut -d':' -f2-)
    domain_list=$(echo "$domain_info" | cut -d'|' -f1 | sed 's/^ *//' | sed 's/ *$//')
    earliest_timestamp=$(echo "$domain_info" | cut -d'|' -f2)
    domains=$(echo "$domain_list" | wc -w)
    country=$(get_country_code "$ip")
    min_domains=$(get_min_domains "$country")

    # Create a single-line log entry for each IP
    log_entry="IP: $ip | Country: $country | Domains: $domains/$min_domains | Earliest: $earliest_timestamp | List: [$domain_list]"

    # Decide whether to ban the IP based on the number of unique domains
    if [ "$domains" -ge "$min_domains" ]; then
        if [[ ! " $WHITELIST " =~ " $ip " ]]; then
            if is_ip_banned "$ip"; then
                log_message "INFO" "ALREADY BANNED | $log_entry"
            else
                RETVAL=$(fail2ban-client set $JAIL_NAME banip $ip 2>&1)
                if [ "$RETVAL" = "1" ]; then
                    log_message "WARN" "BANNED | $log_entry"
                else
                    log_message "ERROR" "FAILED TO BAN | $log_entry"
                    log_message "DEBUG" "fail2ban-client output: $RETVAL"
                fi
            fi
        else
            log_message "INFO" "SKIPPED (whitelisted) | $log_entry"
        fi
    else
        log_message "INFO" "SKIPPED (insufficient domains) | $log_entry"
    fi
done

log_message "INFO" "Script execution completed"

Key Features Explained

  1. Configurable Parameters: The script allows easy customization of log paths, thresholds, and country-specific settings.
  2. Log Rotation Handling: It checks for log rotation on the first day of the month, ensuring continuous protection.
  3. GeoIP Integration: Uses GeoIP data to apply different thresholds based on the country of origin.
  4. Efficient Log Processing: Limits log analysis to 500,000 lines for optimal performance.
  5. Multi-Domain Analysis: Identifies IPs attempting to access multiple domains.
  6. fail2ban Integration: Automatically bans IPs that meet the specified criteria.

Important! Configuring Log Rotation for Dovecot

Proper log rotation is crucial for maintaining the effectiveness of our multi-domain IP banning script and managing disk space efficiently. We'll use logrotate to configure this for Dovecot logs. Here's how to set it up:

Create or edit the following file (ensure that there's only 1 configuration for this!):/etc/logrotate.d/dovecot.conf
sample content:

/var/log/dovecot*.log {
        rotate 6
        monthly
        missingok
        notifempty
        compress
        sharedscripts
        delaycompress
        postrotate
                doveadm log reopen
        endscript
}

Let's break down the key parameters:

  • rotate 6: This keeps 6 months of logs, which is recommended for our script's operation. You can adjust this value based on your needs and storage capacity.
  • monthly: Logs are rotated on a monthly basis, aligning with our script's design for handling log rotation.
  • delaycompress: This is crucial for our script. It delays compression of the rotated log file until the next rotation cycle, ensuring that the previous month's log file (dovecot-info.log.1) remains uncompressed and accessible to our script.

Important Note: The delaycompress parameter is essential for the proper functioning of our multi-domain IP banning script. It ensures that the script can access the previous month's log file when needed, especially important during the first few days of each month.

Other parameters like missingoknotifemptycompress are standard log rotation settings that help manage logs efficiently.

The postrotate script uses doveadm log reopen to ensure Dovecot properly handles the log rotation without requiring a restart.

Adjusting Rotation Settings

While we recommend keeping at least 6 months of logs (rotate 6), you may need to adjust this based on your server's storage capacity and compliance requirements. Remember, increasing the number of rotations will increase the historical data available to the script but will also consume more disk space.

After making changes to the logrotate configuration, you can test it without waiting for the scheduled run:

sudo logrotate -d /etc/logrotate.d/dovecot.conf

This command will show you what logrotate would do, without actually rotating the logs.

By properly configuring log rotation, you ensure that our multi-domain IP banning script has access to the necessary historical data while maintaining efficient use of your server's storage resources. This setup complements the script's functionality, particularly its ability to handle log rotation scenarios at the beginning of each month.

Optimizing Performance

The script has been optimized to run efficiently:

  • It limits log analysis to 500,000 lines, balancing thoroughness with performance.
  • Typical execution time is 2-3 seconds, minimizing server load.
  • Log rotation handling ensures consistent performance across month boundaries.

Setting Up CRON

To ensure regular execution, set up a CRON job. We recommend running the script every 5 minutes:

*/5    *        *    *    *    /home/srvdata/dovecot-multidomain-ip-ban

Adjust the path to match your script's location.

Testing and Evaluation

After implementation, monitor the script's effectiveness by reviewing the log file. Here's an example of what you might see:

[2024-08-13 12:59:03] [INFO] BANNED | IP: 92.52.146.18 | Country: UA | Domains: 2/2 | Earliest: 2024-08-12 15:04:50 | List: [no_domain.tld domain1.ch]
[2024-08-13 12:59:03] [INFO] SKIPPED (insufficient domains) | IP: 24.38.214.194 | Country: US | Domains: 1/2 | Earliest: 2024-08-12 21:02:59 | List: [domain2.ch]
[2024-08-13 13:20:04] [INFO] ALREADY BANNED | IP: 88.235.188.89 | Country: TR | Domains: 2/2 | Earliest: 2024-08-12 19:02:18 | List: [no_domain.tld domain3.ch]

This log excerpt shows the script in action, identifying potentially malicious IPs and taking appropriate action based on the configured thresholds.

Disclaimer and Liability

Important: This script is provided as-is, without any warranty or guarantee. Users should understand that they are using this script at their own risk. LEXO does not take any responsibilities or liabilities for any data loss, system damage, or any other issues that may arise from the use of this script. It is strongly recommended to thoroughly test the script in a non-production environment before using it on critical systems. Always ensure you have multiple backups of your important data using various methods.

Optimization Suggestions (continued)

While the script is already optimized for performance, here are some potential areas for further improvement:

  1. Caching GeoIP Results: Implement a simple caching mechanism for GeoIP lookups to reduce repeated queries for the same IP addresses. This has not been done because we're using a local database which is already very fast and has very low resource demand.
  2. Real-time Monitoring: Implement a real-time monitoring system that can trigger the script immediately upon detecting suspicious activity, rather than waiting for the next CRON execution.

Frequently Asked Questions

Q: How does this script differ from standard fail2ban configurations?

A: Unlike standard fail2ban setups, this script analyzes login attempts across multiple domains, applies country-specific thresholds, and provides more granular control over the banning process.

Q: Can this script be used with other IMAP/POP3 servers?

A: While designed for Dovecot, the script can be adapted for other servers that generate similar log formats. You'll need to adjust the log parsing logic accordingly.

Q: How often should I update the GeoIP database?

A: It's recommended to update the GeoIP database at least monthly to ensure accurate country identification. Some providers offer weekly updates for more current data.

Q: What should I do if I accidentally ban a legitimate IP?

A: You can unban an IP using the fail2ban-client command: sudo fail2ban-client unban ip [IP_ADDRESS]Consider adding frequently mis-banned IPs to the whitelist.

Conclusion

Implementing this Dovecot multi-domain IP banning script is a significant step towards enhancing your email server's security. By automating the process of identifying and banning potentially malicious IPs across multiple domains, you create a robust defense against brute force attacks and unauthorized access attempts.

Remember, security is an ongoing process. Regularly review and adjust the script's parameters based on your specific needs and the evolving threat landscape. Stay informed about new security threats and best practices in email server administration.

With proper implementation, monitoring, and integration with other security measures, this script can be a powerful tool in your server security arsenal, helping to ensure the integrity and availability of your email services.

We encourage you to share your experiences, suggestions, or questions about this script in the comments section below. Your feedback helps us improve and provides valuable insights for other users implementing similar security measures.