Using the blocklist on your systems

This documentation describes how to use the SSHWatch blocklist service to protect your Linux systems from malicious IP addresses. The service provides an API endpoint that returns a list of IP addresses that have been identified as sources of malicious activity.

Service Overview

The SSHWatch blocklist service provides a regularly updated list of IP addresses that have been identified as sources of malicious activity. The service is accessible via a simple REST API endpoint:

https://blocklist.sshwatch.com/?id={your-list-id}

Each user of the service is assigned a unique 6-digit list ID and a secret token that must be used for authentication.

API Authentication

Authentication to the API is performed using a Bearer token in the HTTP Authorization header.

Authentication Parameters

Example Request

Using curl:

curl -H "Authorization: Bearer your-blocklist-secret" https://blocklist.sshwatch.com/?id=123456

Response Format

  • Success: A plain text list of IP addresses, one per line
  • Error: A JSON object with an error message, for example:
    {"error": "Unauthorized: Invalid authentication token"}
    

HTTP Status Codes

  • 200: Success
  • 400: Bad Request (e.g., invalid list ID format)
  • 401: Unauthorized (missing or invalid token)
  • 404: List not found
  • 429: Too Many Requests (rate limit exceeded)
  • 500: Internal Server Error

Rate Limiting

To ensure service stability, rate limiting is applied to all API requests:

  • Limit: 60 requests per hour per IP address
  • Headers: The response includes the following rate limit headers:
    • X-RateLimit-Limit: Maximum allowed requests
    • X-RateLimit-Remaining: Remaining requests
    • X-RateLimit-Reset: Unix timestamp when the limit resets

If you exceed the rate limit, you will receive a 429 status code with a JSON error message.

Setting Up Automatic IP Blocking

Here are examples of how to set up automatic IP blocking on Linux systems using different firewall solutions.

Using Firewalld

For systems using firewalld (e.g., CentOS, RHEL, Fedora):

  1. Create a bash script named update-sshwatch-blocklist.sh:

This script for firewalld-based systems uses the efficient ipset utility to manage large lists of IP addresses. The script follows these steps:

First, it sets up configuration variables and a logging function to keep track of activities. It then checks if the required ipset utility is installed, and creates a named IP set if it doesn't already exist.

The core of the script fetches the blocklist from the SSHWatch API using curl with proper authentication headers. It captures both the HTTP status code and the response content to handle errors gracefully. If successful, the script uses a smart approach of creating a temporary ipset, populating it with the new IPs, and then atomically swapping it with the active ipset to minimize any service interruption.

For firewalld integration, the script ensures that firewalld knows about the ipset and adds a firewall rule that drops all traffic from IPs in the set. The script handles both the initial setup case and subsequent updates differently. Finally, it reloads firewalld to apply the changes and cleans up temporary files.

#!/bin/bash

# Configuration
LIST_ID="123456"  # Replace with your list ID
SECRET_TOKEN="your-secret-token"  # Replace with your token
LOG_FILE="/var/log/sshwatch-blocklist.log"
IPSET_NAME="sshwatch-blocklist"

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

# Ensure ipset is installed
if ! command -v ipset &>/dev/null; then
    log_message "ERROR: ipset is not installed. Please install it with 'dnf install ipset'."
    exit 1
fi

# Create ipset if it doesn't exist
ipset list "$IPSET_NAME" &>/dev/null || {
    log_message "Creating new ipset: $IPSET_NAME"
    ipset create "$IPSET_NAME" hash:ip hashsize 4096
}

# Temporary file for new blocklist
TMP_FILE=$(mktemp)

# Fetch the blocklist
log_message "Fetching blocklist from SSHWatch..."

HTTP_STATUS=$(curl -s -w "%{http_code}" -o "$TMP_FILE" \
    -H "Authorization: Bearer $SECRET_TOKEN" \
    "https://blocklist.sshwatch.com/?id=$LIST_ID")

if [ "$HTTP_STATUS" -ne 200 ]; then
    ERROR_MSG=$(cat "$TMP_FILE")
    log_message "ERROR: Failed to fetch blocklist. HTTP status: $HTTP_STATUS, Message: $ERROR_MSG"
    rm "$TMP_FILE"
    exit 1
fi

# Count IPs in the new list
IP_COUNT=$(wc -l < "$TMP_FILE")
log_message "Retrieved $IP_COUNT IP addresses from blocklist"

# Swap out the ipset with the new one
TEMP_IPSET="${IPSET_NAME}_temp"
ipset create "$TEMP_IPSET" hash:ip hashsize 4096

# Add IPs to the temporary ipset
while IFS= read -r ip; do
    # Skip empty lines
    [ -z "$ip" ] && continue
    ipset add "$TEMP_IPSET" "$ip"
done < "$TMP_FILE"

# Swap the sets and remove the old one
ipset swap "$TEMP_IPSET" "$IPSET_NAME"
ipset destroy "$TEMP_IPSET"

# Ensure firewalld is using this ipset
if ! firewall-cmd --permanent --get-ipsets | grep -q "^$IPSET_NAME$"; then
    log_message "Adding ipset to firewalld"
    firewall-cmd --permanent --new-ipset="$IPSET_NAME" --type=hash:ip --option=timeout=0
    firewall-cmd --permanent --ipset="$IPSET_NAME" --add-entries-from-file="$TMP_FILE"
else
    log_message "Updating firewalld ipset"
    firewall-cmd --permanent --delete-ipset="$IPSET_NAME"
    firewall-cmd --permanent --new-ipset="$IPSET_NAME" --type=hash:ip --option=timeout=0
    firewall-cmd --permanent --ipset="$IPSET_NAME" --add-entries-from-file="$TMP_FILE"
fi

# Add/verify the rule to block traffic from these IPs
if ! firewall-cmd --permanent --direct --query-rule ipv4 filter INPUT 0 -m set --match-set "$IPSET_NAME" src -j DROP; then
    log_message "Adding firewalld rule to block IPs in the set"
    firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -m set --match-set "$IPSET_NAME" src -j DROP
fi

# Reload firewalld
firewall-cmd --reload

log_message "Blocklist update completed successfully"

# Clean up
rm "$TMP_FILE"
  1. Make the script executable:
chmod +x update-sshwatch-blocklist.sh

Using IPTables

For systems using iptables directly:

This script is designed for systems that use iptables directly without a higher-level firewall management tool. Like the firewalld script, it uses ipset for efficient IP list management, but integrates with iptables rules instead.

The script begins by checking for required dependencies (both iptables and ipset) and setting up the ipset if it doesn't exist. It then fetches the blocklist from the SSHWatch API with proper authentication and error handling.

The core technique here uses the same efficient approach of creating a temporary ipset, populating it, and then swapping it with the active set. This approach ensures that there's minimal window where IPs might not be blocked during the update process.

For the iptables integration, the script first checks if a rule for the ipset already exists to avoid duplicate rules. If the rule doesn't exist, it inserts it at the beginning of the INPUT chain for maximum effectiveness. What's particularly useful is the script's ability to make the rules persistent across reboots by detecting the distribution type (Debian/Ubuntu vs RHEL/CentOS) and saving the rules in the appropriate location.

#!/bin/bash

# Configuration
LIST_ID="123456"  # Replace with your list ID
SECRET_TOKEN="your-secret-token"  # Replace with your token
LOG_FILE="/var/log/sshwatch-blocklist.log"
IPSET_NAME="sshwatch-blocklist"

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

# Ensure ipset and iptables are installed
if ! command -v ipset &>/dev/null || ! command -v iptables &>/dev/null; then
    log_message "ERROR: ipset or iptables is not installed."
    exit 1
fi

# Create ipset if it doesn't exist
ipset list "$IPSET_NAME" &>/dev/null || {
    log_message "Creating new ipset: $IPSET_NAME"
    ipset create "$IPSET_NAME" hash:ip hashsize 4096
}

# Temporary file for new blocklist
TMP_FILE=$(mktemp)

# Fetch the blocklist
log_message "Fetching blocklist from SSHWatch..."

HTTP_STATUS=$(curl -s -w "%{http_code}" -o "$TMP_FILE" \
    -H "Authorization: Bearer $SECRET_TOKEN" \
    "https://blocklist.sshwatch.com/?id=$LIST_ID")

if [ "$HTTP_STATUS" -ne 200 ]; then
    ERROR_MSG=$(cat "$TMP_FILE")
    log_message "ERROR: Failed to fetch blocklist. HTTP status: $HTTP_STATUS, Message: $ERROR_MSG"
    rm "$TMP_FILE"
    exit 1
fi

# Count IPs in the new list
IP_COUNT=$(wc -l < "$TMP_FILE")
log_message "Retrieved $IP_COUNT IP addresses from blocklist"

# Swap out the ipset with the new one
TEMP_IPSET="${IPSET_NAME}_temp"
ipset create "$TEMP_IPSET" hash:ip hashsize 4096

# Add IPs to the temporary ipset
while IFS= read -r ip; do
    # Skip empty lines
    [ -z "$ip" ] && continue
    ipset add "$TEMP_IPSET" "$ip"
done < "$TMP_FILE"

# Swap the sets and remove the old one
ipset swap "$TEMP_IPSET" "$IPSET_NAME"
ipset destroy "$TEMP_IPSET"

# Check if the iptables rule already exists, if not add it
if ! iptables -C INPUT -m set --match-set "$IPSET_NAME" src -j DROP &>/dev/null; then
    log_message "Adding iptables rule to block IPs in the set"
    iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP
    
    # Make the rule persistent (different methods depending on distribution)
    if command -v iptables-save &>/dev/null; then
        if [ -d "/etc/iptables" ]; then
            # Debian/Ubuntu style
            iptables-save > /etc/iptables/rules.v4
        elif [ -f "/etc/sysconfig/iptables" ]; then
            # RHEL/CentOS style
            iptables-save > /etc/sysconfig/iptables
        else
            log_message "WARNING: Could not determine how to make iptables rules persistent on this system"
        fi
    fi
fi

log_message "Blocklist update completed successfully"

# Clean up
rm "$TMP_FILE"

Using UFW

For Ubuntu and other systems using UFW (Uncomplicated Firewall):

This script is specifically designed for Ubuntu and other systems that use the Uncomplicated Firewall (UFW) for firewall management. Unlike the previous scripts that use ipset, this approach works directly with UFW's rule system.

The script begins with standard setup and dependency checking. What makes this script unique is its approach to maintaining state between runs. It extracts currently blocked IPs by searching for rules with a specific comment ("sshwatch block") in the UFW configuration file, creating a record of the current state before making any changes.

After fetching the blocklist from the SSHWatch API, the script performs a two-way sync operation. First, it removes any previously blocked IPs that are no longer in the blocklist (preventing unnecessary rule bloat). Then it adds new IPs that aren't already blocked. This intelligent approach minimizes the number of UFW commands executed, which is important because each UFW command can take a moment to process.

The script uses UFW's comment feature to tag all rules it creates, making it possible to identify and manage them separately from other firewall rules. The "insert 1" command ensures that these blocking rules take precedence by placing them at the beginning of the rule list. This approach allows for clean integration with existing UFW setups without disrupting other firewall rules.

#!/bin/bash

# Configuration
LIST_ID="123456"  # Replace with your list ID
SECRET_TOKEN="your-secret-token"  # Replace with your token
LOG_FILE="/var/log/sshwatch-blocklist.log"

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

# Ensure UFW is installed
if ! command -v ufw &>/dev/null; then
    log_message "ERROR: UFW is not installed."
    exit 1
fi

# Temporary file for new blocklist
TMP_FILE=$(mktemp)
OLD_LIST=$(mktemp)

# Keep track of currently blocked IPs
grep -E "^#sshwatch block#" /etc/ufw/user.rules | awk '{print $4}' | sort > "$OLD_LIST"

# Fetch the blocklist
log_message "Fetching blocklist from SSHWatch..."

HTTP_STATUS=$(curl -s -w "%{http_code}" -o "$TMP_FILE" \
    -H "Authorization: Bearer $SECRET_TOKEN" \
    "https://blocklist.sshwatch.com/?id=$LIST_ID")

if [ "$HTTP_STATUS" -ne 200 ]; then
    ERROR_MSG=$(cat "$TMP_FILE")
    log_message "ERROR: Failed to fetch blocklist. HTTP status: $HTTP_STATUS, Message: $ERROR_MSG"
    rm "$TMP_FILE" "$OLD_LIST"
    exit 1
fi

# Count IPs in the new list
IP_COUNT=$(wc -l < "$TMP_FILE")
log_message "Retrieved $IP_COUNT IP addresses from blocklist"

# Remove old IPs that are not in the new list
while IFS= read -r ip; do
    if ! grep -q "^$ip$" "$TMP_FILE"; then
        log_message "Removing old IP from blocklist: $ip"
        ufw delete deny from "$ip" to any comment "sshwatch block"
    fi
done < "$OLD_LIST"

# Add new IPs that are not in the old list
while IFS= read -r ip; do
    # Skip empty lines
    [ -z "$ip" ] && continue
    
    if ! grep -q "^$ip$" "$OLD_LIST"; then
        log_message "Adding new IP to blocklist: $ip"
        ufw insert 1 deny from "$ip" to any comment "sshwatch block"
    fi
done < "$TMP_FILE"

log_message "Blocklist update completed successfully"

# Clean up
rm "$TMP_FILE" "$OLD_LIST"

Automation with Cron

To automatically update your blocklist, set up a cron job to run the script at regular intervals.

  1. Edit the crontab:
crontab -e
  1. Add a line to run the script every hour:
0 * * * * /path/to/update-sshwatch-blocklist.sh

This will run the script at the top of every hour. You can adjust the schedule as needed, but remember that API requests are rate-limited to 60 per hour.

Advanced Configuration

Blocking Only SSH Traffic

If you want to block the IPs only for SSH traffic (port 22) rather than all traffic, modify the firewall rules in the script:

For firewalld:

# Replace this line:
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -m set --match-set "$IPSET_NAME" src -j DROP

# With this:
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -m set --match-set "$IPSET_NAME" src -p tcp --dport 22 -j DROP

For iptables:

# Replace this line:
iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP

# With this:
iptables -I INPUT -m set --match-set "$IPSET_NAME" src -p tcp --dport 22 -j DROP

For UFW:

# Replace this line:
ufw insert 1 deny from "$ip" to any comment "sshwatch block"

# With this:
ufw insert 1 deny from "$ip" to any port 22 proto tcp comment "sshwatch block"

Logging Blocked Attempts

To log blocked connection attempts:

For firewalld:

firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -m set --match-set "$IPSET_NAME" src -j LOG --log-prefix "SSHWATCH-BLOCKED: "
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 -m set --match-set "$IPSET_NAME" src -j DROP

For iptables:

iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j LOG --log-prefix "SSHWATCH-BLOCKED: "
iptables -I INPUT -m set --match-set "$IPSET_NAME" src -j DROP

Troubleshooting

Common Issues

API Errors

  • 401 Unauthorized: Verify your list ID and secret token are correct
  • 429 Too Many Requests: You've exceeded the rate limit. Wait for the reset time provided in the X-RateLimit-Reset header.

Firewall Issues

  1. Rules not persisting after reboot:

    • For iptables, make sure you're saving the rules (see script examples)
    • For firewalld, ensure you're using --permanent flag and reloading
  2. Script execution errors:

    • Check the log file (default: /var/log/sshwatch-blocklist.log)
    • Ensure the script has execute permissions (chmod +x)
  3. Large blocklists performance:

    • If the blocklist is very large, consider using ipset (as shown in the examples) rather than individual firewall rules
    • Increase the hashsize parameter for ipset if needed

Testing Your Setup

To test if your firewall is correctly blocking IPs from the blocklist:

  1. Backup your current firewall rules
  2. Temporarily add your own IP to the blocklist
  3. Try to connect to your server
  4. Remove your IP from the blocklist
  5. Restore connectivity

Warning: Be careful when testing with your own IP. Always maintain an alternative way to access the system to avoid locking yourself out.


For additional support, please contact support@sshwatch.com


Was this article helpful?