Information Technology Grimoire

Version .0.0.1

IT Notes from various projects because I forget, and hopefully they help you too.

iptables automated blacklist threat intel

IPTables

IPtables is a host based firewall. It’s pretty decent, but the interface is something from a geek nightmare. It can do burst protection, blacklists and more. This article will show you how to use iptables to automate ingestion of a threat intel feed and apply burst and other rules to your host.

Specifically this will help you protect a low end VPS for free from scans and a few basic attacks.

What does this script do?

IPv4 Only

I don’t allow IPv6 in, even though it’s best practice. I did this for simplicity sake. If I block all ipv6 I don’t have to spend any thought, server resources or scripting time in how to defend against it.

Geo IP Blocking

Geofencing isn’t really protection, it’s more of a low hanging fruit. Some companies have global customers. I do not. This script blocks all known networks from a manual list of top attacker networks that you choose:

https://www.ipdeny.com/ipblocks/data/countries/${country}.zone

# Top Attackers, in order for 2023 May
# China (cn)
# Russia (ru)
# North Korea (kp)
# Iran (ir)
# United States (us)
# Brazil (br)
# India (in)
# Turkey (tr)
# Ukraine (ua)
# Vietnam (vn)

Total count of all ips in the geoblock list is 575454752 in all geo blocked 46183 networks. This is irrelevant but I thought it was interesting, so I added the counts!

(1:1022)# ./countgeoblock.sh banned_countries
Total IPs in banned_countries: 575454752

(1:1028)# ipset list banned_countries | wc -l
46183

The acutal banned_countries ipset is a list of cidr networks:

(1:1029)# ipset list banned_countries | tail -n5
170.80.192.0/22
170.238.120.0/22
91.235.196.0/22
213.110.160.0/19
37.153.176.0/20

Threat Intel Feed Blocking

These are some free feeds. There are other free feeds, and paid ones too.

And you can easily add paid or other free feeds if you wish by simply adding another URL to the list. Again I thought the counts were interesting:

(1:1030)# ipset list blacklist_set | wc -l
11768

And the list is just a list of ipv4 addresses:

(1:1031)# ipset list blacklist_set | tail -n5
162.216.150.47
167.94.138.179
117.200.129.5
83.24.56.244
114.25.91.226

Whitelisting

The script intelligently uses whitelists for static ip, networks and dynamic hosts as per whatever your configuration in those whitelists might be.

Hashtables/ipsets

A hash table is a key value pair table that is typically faster than lists. All of the blacklist blocks are added to these ipsets so performance is fast and not noticeable, even for extremely large lists.

Basic Brute Force/DOS Protection

In addition to the automatic blocks on the blacklists, rules are modified manually as part of the install to rate limit and protect against scans, DOS attacks, tcp connection consumption and brute force. This protection isn’t foolproof, and will still slow down a VPS if attacked, but they are automatic and help slow an attacker down, or in some cases completely block their attempts. Here is a list of the automated realtime protections:

  • Rate limit icmp to 10/second
  • Rate limit ssh to 5/minute
  • Rate limit http to 50/second
  • Rate limit https to 100/second
  • Concurrent limit Connection 100 per IP

Automated Feed Updates

  • Within 5 minutes of reboot, refresh all lists
  • Every 12 hours update blacklists
  • Every 14 days update geo fencing

Email Notification

If the script completes or runs over 200 seconds, or if any blacklist seems too small, it errors and sends alert by mail.

If the script operates normally, you will get a notification email with basic stats and results summary. Here is an example summary email:

Blacklist Script completed successfully:

Count for blacklist_set: 11760
Count for banned_countries: 46175
Run time: 89 seconds

JUST DROPS (all syslog)

2198  IPTables-Blacklist-Drop:
776   IPTables-Country-Ban-Drop:
15    TCP-conn-limit:

RECENT LOG ENTRIES

Oct  7 06:56:34 hosts kernel: [40349.608948] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=43.230.67.235 DST=167.99.62.44 LEN=60 TOS=0x00 PREC=0x00 TTL=55 ID=64166 DF PROTO=TCP SPT=49736 DPT=22 WINDOW=29200 RES=0x00 SYN URGP=0 
(etc)

Manual Example (basics)

This is just an example of a simple blacklist using ipset so you can understand the complete script, which is much more complicated:

Create IPSET

ipset create blacklist hash:ip

Add an IP to IPSET

ipset create blacklist hash:ip
ipset add blacklist 1.2.3.4

Load IPSET from File

ipset create blacklist hash:ip
while IFS= read -r ip; do
    sudo ipset add blacklist $ip
done < blacklist.txt

Use Ipset in IPTables

Regardless of how you build the ipset, you then use the ipset in iptables firewall:

iptables -A INPUT -m set --match-set blacklist src -j DROP

Threat Intel Feeds

Where do we get a list of bad actors?

There are many free and paid threat intel feeds which are built from various ISP generating lists of known attackers. This intelligence is shared so people can use it to protect themselves from known bad actors. For this script, we’ll use these 3, but there are many more free and paid blacklists:

They are in different formats and update regularly. So we need to convert them all to a common format and then deduplicate the entire list (they will have duplicates).

We have all of the components now - a way to generate a list, and add it to the firewall. Now that you have a very basic understanding, we’ll go over how to install the prewritten script that does all of this and more.

Blacklist Script Installation Summary

We’ll go over each of these steps in detail, but here is the high levl overview:

  • update and install prerequisites
  • create initial ufw rules
  • modify prestart ufw rules
  • create whitelist files
  • configure geo block list choices
  • modify admin email
  • test manually
  • add cron job
  • create and enable startup service
  • reboot to verify automation

Blacklist Script Installation Details

Update and Install Prerequisites

UFW is recommended, and ipset is required:

apt update
apt upgrade
apt install ufw ipset

Create Initial UFW rules

My server is only using ssh, http, http2 and https to the public, so my rules might look like this:

ufw default deny incoming
ufw default allow outgoing
ufw logging on
ufw allow from 0.0.0.0/0 to any port 22 proto tcp comment 'SSH for IPv4 only'
ufw allow from 0.0.0.0/0 to any port 80 proto tcp comment 'HTTP for IPv4 only'
ufw allow from 0.0.0.0/0 to any port 443 proto tcp comment 'HTTPS for IPv4 only'
ufw allow from 0.0.0.0/0 to any port 443 proto udp comment 'QUIC/HTTP2 IPv4 only'
ufw enable
ufw status numbered

Modify Prestart UFW Rules

the “before.rules” are processed first, before your standard rules. This is important because like most firewalls the rules are processed in top down order, and we want to block blacklist, allow wanted traffic and then block everything else.

In addition, we are limiting icmp, 100 connections per client at the same time, and 50 connections a second for http/https.

These settings are not as powerful as Akamai or even some of the advanced NGINX settings, but they are still helpful to protect against basic abuse.

Replace your existing /etc/ufw/before.rules with the following:

#
# rules.before
#
# Rules that should be run before the ufw command line added rules. Custom
# rules should be added to one of these chains:
#   ufw-before-input
#   ufw-before-output
#   ufw-before-forward
#

# Don't delete these required lines, otherwise there will be errors
*filter
:ufw-before-input - [0:0]
:ufw-before-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-not-local - [0:0]
# End required lines

# Action for IPs in the blacklist_set
-A ufw-before-input -m set --match-set blacklist_set src -m limit --limit 5/min -j LOG --log-prefix "IPTables-Blacklist-Drop: " --log-level 4
-A ufw-before-input -m set --match-set blacklist_set src -j DROP

# Action for the banned_countries set
-A ufw-before-input -m set --match-set banned_countries src -m limit --limit 5/min -j LOG --log-prefix "IPTables-Country-Ban-Drop: " --log-level 4
-A ufw-before-input -m set --match-set banned_countries src -j DROP

# allow all on loopback
-A ufw-before-input -i lo -j ACCEPT
-A ufw-before-output -o lo -j ACCEPT

# quickly process packets for which we already have a connection
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# drop INVALID packets (logs these in loglevel medium and higher)
-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP

# Rate limit ICMP packets
-A ufw-before-input -p icmp -m limit --limit 10/second -j ACCEPT

# ok icmp codes for INPUT
-A ufw-before-input -p icmp --icmp-type destination-unreachable -j ACCEPT
-A ufw-before-input -p icmp --icmp-type time-exceeded -j ACCEPT
-A ufw-before-input -p icmp --icmp-type parameter-problem -j ACCEPT
-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT

# ok icmp code for FORWARD
-A ufw-before-forward -p icmp --icmp-type destination-unreachable -j ACCEPT
-A ufw-before-forward -p icmp --icmp-type time-exceeded -j ACCEPT
-A ufw-before-forward -p icmp --icmp-type parameter-problem -j ACCEPT
-A ufw-before-forward -p icmp --icmp-type echo-request -j ACCEPT

# allow dhcp client to work
-A ufw-before-input -p udp --sport 67 --dport 68 -j ACCEPT

#
# ufw-not-local
#
-A ufw-before-input -j ufw-not-local

# if LOCAL, RETURN
-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN

# if MULTICAST, RETURN
-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN

# if BROADCAST, RETURN
-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN

# all other non-local packets are dropped
-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
-A ufw-not-local -j DROP

# allow MULTICAST mDNS for service discovery (be sure the MULTICAST line above
# is uncommented)
-A ufw-before-input -p udp -d 224.0.0.251 --dport 5353 -j ACCEPT

# allow MULTICAST UPnP for service discovery (be sure the MULTICAST line above
# is uncommented)
-A ufw-before-input -p udp -d 239.255.255.250 --dport 1900 -j ACCEPT


# Log SSH connection attempts over the limit.
-A ufw-before-input -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 5 -m limit --limit 5/min -j LOG --log-prefix "SSH-rate-limit: " --log-level 4
-A ufw-before-input -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 5 -j DROP -m comment --comment "Drop SSH if >5 attempts in 60 seconds"

# Log HTTP connection attempts over the limit.
-A ufw-before-input -p tcp --dport 80 -m state --state NEW -m recent --update --seconds 1 --hitcount 50 -m limit --limit 5/min -j LOG --log-prefix "HTTP-rate-limit: " --log-level 4
-A ufw-before-input -p tcp --dport 80 -m state --state NEW -m recent --update --seconds 1 --hitcount 50 -j DROP -m comment --comment "Drop HTTP if >50 attempts in 1 second"

# Log HTTPS connection attempts over the limit.
-A ufw-before-input -p tcp --dport 443 -m state --state NEW -m recent --update --seconds 2 --hitcount 100 -m limit --limit 2/min -j LOG --log-prefix "HTTPS-rate-limit: " --log-level 4
-A ufw-before-input -p tcp --dport 443 -m state --state NEW -m recent --update --seconds 2 --hitcount 100 -j DROP -m comment --comment "Drop HTTPS if >100 attempts in 1 second"

# Log packets that exceed the parallel TCP connection limit.
-A ufw-before-input -p tcp --syn -m connlimit --connlimit-above 200 -m limit --limit 5/min -j LOG --log-prefix "TCP-conn-limit: " --log-level 4

# Reject packets that exceed the parallel TCP connection limit.
-A ufw-before-input -p tcp --syn -m connlimit --connlimit-above 200 -j REJECT -m comment --comment "Limit parallel TCP connections to 200 per IP"

# Allow established and related connections
-A ufw-before-input -m state --state ESTABLISHED,RELATED -j ACCEPT




# don't delete the 'COMMIT' line or these rules won't be processed
COMMIT

Create Whitelist Files

The whitelists files are required. The script will not operate with them.

The whitelist-domains.txt are for dynamic domains that should be allowed such as search engine bots, or your travelling hotspot with ddns.

The /whitelists/ directory should contain a set of files (they can be commented with a #). The file format is whitelist-something.txt. Here is an example of the whitelists I’m running, which are a mix of ip, cidr, and domain names depending on the file. Some of these are automatically created by the script, others are edited by hand.

(1:1165)# ls -alF /root/scripts/fw/whitelists/
total 56
drwxr-xr-x 2 root root  4096 Oct  7 12:38 ./
drwxr-xr-x 8 root root  4096 Oct  7 12:38 ../
-rw-r--r-- 1 root root 16139 Oct  7 12:58 whitelist-ahrefsbots.txt
-rw-r--r-- 1 root root   412 Oct  7 12:58 whitelist-bingbots.txt
-rw-r--r-- 1 root root   184 Oct  6 09:16 whitelist-domains.txt
-rw-r--r-- 1 root root   230 Oct  7 10:12 whitelist-duckduckbot.txt
-rw-r--r-- 1 root root  3986 Oct  7 12:58 whitelist-facebook.txt
-rw-r--r-- 1 root root  2088 Oct  7 12:58 whitelist-googlebots.txt
-rw-r--r-- 1 root root    23 Oct  6 17:41 whitelist-ips.txt
-rw-r--r-- 1 root root    82 Oct  7 11:27 whitelist-nets.txt
-rw-r--r-- 1 root root    96 Oct  7 11:57 whitelist-semrush.txt

Modify Admin Email

This is also obvious in the script.

Configure Geo Blocklists

I’m blocking the top 10 worst offenders for scans and attacks (besides the US). This is not something everyone will want to do though. The config is at the top of the blacklist.sh script.

banned_countries=('cn' 'tw' 'ru' 'br' 'ir' 'kp' 'ua' 'in') # do not overload this list, just pick the top 3-5
# Top Attackers, in order for 2023 May
# China (cn)
# Russia (ru)
# North Korea (kp)
# Iran (ir)
# United States (us)
# Brazil (br)
# India (in)
# Turkey (tr)
# Ukraine (ua)
# Vietnam (vn)

Test Manually

Your output should look something like this:

(1:1204)# /root/scripts/fw/blacklist.sh -v
2023-10-07-13:42
Purging existing lists
Preflight checks passed
Internet connection seems to be available
Downloading searchbot whitelists
https://developers.google.com/search/apis/ipranges/googlebot.json
https://api.ahrefs.com/v3/public/crawler-ips
https://www.bing.com/toolbox/bingbot.json
Downloading threat intel feeds
http://www.talosintelligence.com/documents/ip-blacklist
https://secureupdates.checkpoint.com/IP-list/TOR.txt
Populating all whitelists
/root/scripts/fw/whitelists/whitelist-ahrefsbots.txt
/root/scripts/fw/whitelists/whitelist-bingbots.txt
/root/scripts/fw/whitelists/whitelist-domains.txt
/root/scripts/fw/whitelists/whitelist-duckduckbot.txt
/root/scripts/fw/whitelists/whitelist-facebook.txt
/root/scripts/fw/whitelists/whitelist-googlebots.txt
/root/scripts/fw/whitelists/whitelist-ips.txt
/root/scripts/fw/whitelists/whitelist-nets.txt
/root/scripts/fw/whitelists/whitelist-semrush.txt
Downloading dshield and adding to blacklist
Processing threat intel feeds and adding to blacklist
/root/scripts/fw/blacklists/blacklist-secureupdates.checkpoint.com.txt
/root/scripts/fw/blacklists/blacklist-www.talosintelligence.com.txt
Starting geo-blocking process...
Using existing ban list for cn...
Using existing ban list for tw...
Using existing ban list for ru...
Using existing ban list for br...
Using existing ban list for ir...
Using existing ban list for kp...
Using existing ban list for ua...
Using existing ban list for in...
Geo-blocking ban lists completed.
Attempting to purge any stale archives
Attempting to purge any stale archives
11758 ip added to blacklist_set
46175 ip added to banned_countries
Run time: 82 seconds

Add the Cron Job

crontab -e
30 3,15 * * * nice -n 19 /root/scripts/fw/blacklist.sh >> /root/scripts/fw/blacklistlog.txt 2>&1

Enable Startup Service

In the event the firewall is rebooted, the ipset will purge and we need to restart the process the first time. To do this, we need to create a helper script:

(1:513)# cat /root/scripts/fw/blacklist-startup.sh
#!/bin/bash

# Call the blacklist script to create the ipset
/root/scripts/fw/blacklist.sh

# Reload UFW so it will apply the ipset
ufw reload

Then, we need to call it from the startup script service:

vim /etc/systemd/system/blacklist-startup.service
[Unit]
Description=Run blacklist and reload UFW at startup and 5 minutes later
After=network.target

[Service]
Type=oneshot
ExecStart=/root/scripts/fw/blacklist-startup.sh
ExecStartPost=/bin/sleep 300
ExecStartPost=/root/scripts/fw/blacklist-startup.sh
TimeoutStartSec=500  # Increasing this since we have added sleep

[Install]
WantedBy=multi-user.target

Finally, we enable the service on every boot:

systemctl enable blacklist-startup.service

checkfirewalllogs.sh

To see our logs for the things our protections are protecting, we can use a command like this (saved into a script, so it is easy to remember):

(1:519)# cat checkfirewalllogs.sh
iptables -L -v -n  | egrep "limit|blacklist|burst" -i
egrep 'IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' /var/log/syslog | tail -n 20

blacklist.sh

This is the main script. It works, but I’m adding in the dynamic firewall script too for the next version.

The dynamic firewall version will allow your dynamic hostnames to connect to a specified port, no matter what that IP currently is. I use this for my jump server along with ddns on my hotspot/phone/cradlepoint/home, to allow me access wherever I am.

(1:525)# cat /root/blacklist.sh
#!/bin/bash
# pulls up to date data on geo blocks, threat intel feeds and whitelists
# skips whitelist members, but blacklists the rest in iptables
# sends report
# cron friendly
# -v verbose mode option
# ipset updates automatically and reload not required
# various health/integrity checks or fail
# archive whitelist and blacklist for research if needed
# safely auto purges archives

# because different os use slight variants of curl (checkpoint uses curl_cli):
curl_cmd="curl --connect-timeout 5 --max-time 60 -L -ks --fail"

# basic variables for setup
base_path="/root/scripts/fw"
blacklist_dir="${base_path}/blacklists"
whitelist_dir="${base_path}/whitelists"
geozones_dir="${base_path}/geozones"
whitelist_archives="${base_path}/whitelist_archives"
threat_intel_archives="${base_path}/threat_intel_archives"
required_space_mb=1024  # 1G
admin_email="james@somesite.com"
banned_countries=('cn' 'tw' 'ru' 'br' 'ir' 'kp' 'ua' 'in') # do not overload this list, just pick the top 3-5
# Top Attackers, in order for 2023 May
# China (cn)
# Russia (ru)
# North Korea (kp)
# Iran (ir)
# United States (us)
# Brazil (br)
# India (in)
# Turkey (tr)
# Ukraine (ua)
# Vietnam (vn)

# default verbose to false, see details with -v on script run
verbose=false

# Check arguments
while getopts ":v" opt; do
    case $opt in
        v) verbose=true ;;
        \?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
    esac
done

# dshield has a special format, 3 columns network address, end ip, mask
dshield_feed="https://feeds.dshield.org/block.txt"

# other feeds are expected to have 1 ip per line
threat_intel_feeds=(
    "http://www.talosintelligence.com/documents/ip-blacklist"
    "https://secureupdates.checkpoint.com/IP-list/TOR.txt"
)

SECONDS=0

# handle verbose messages
logverbose() {
    $verbose && echo "$@"
}

# make directories required if they don't exist
mkdir -p "$blacklist_dir"
mkdir -p "$geozones_dir"
mkdir -p "$threat_intel_archives"
mkdir -p "whitelist_archives"

DTS=$(date "+%Y-%m-%d-%H:%M")
logverbose "${DTS}"
logverbose "Purging existing lists"

blacklist_name="blacklist_set"
ipset create $blacklist_name hash:ip -exist
ipset flush $blacklist_name

geoblock_name="banned_countries"
ipset create $geoblock_name hash:net hashsize 131072 maxelem 262144 -exist
ipset flush $geoblock_name

whitelist_name="whitelist_net_set"
ipset create $whitelist_name hash:net -exist
ipset flush $whitelist_name

# Create an array to hold whitelisted IPs and nets
declare -A whitelist_ips
declare -A whitelist_nets

fetch_company_whitelists_from_asn() {
    local company_name="$1"
    local asn="$2"
    local file="${whitelist_dir}/whitelist-${company_name}.txt"
    local timestamp=$(date "+%Y%m%d_%H%M")
    local archive_file="${whitelist_archives}/${timestamp}_whitelist-${company_name}.txt"

    whois -h whois.radb.net -- "-i origin AS${asn}" | grep '^route:' | awk '{print $2}' | tee "$archive_file" > "$file"
}

fetch_searchbot_whitelists() {
    logverbose "Downloading searchbot whitelists"

    declare -A searchbot_urls=(
        [google]="https://developers.google.com/search/apis/ipranges/googlebot.json"
        [bing]="https://www.bing.com/toolbox/bingbot.json"
        [ahrefs]="https://api.ahrefs.com/v3/public/crawler-ips"
    )

    for bot in "${!searchbot_urls[@]}"; do
        local url="${searchbot_urls[$bot]}"
        local file="${whitelist_dir}/whitelist-${bot}bots.txt"
        local timestamp=$(date "+%Y%m%d_%H%M")
        local archive_file="${whitelist_archives}/${timestamp}_whitelist-${bot}bots.txt"

        logverbose "$url"

        # Extract IPs/CIDRs from JSON data
        $curl_cmd "$url" | jq -r '.. | select(type=="string")? | select(test("^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+(/\\d{1,2})?$"))' | tee "$archive_file" > "$file"
    done

    fetch_company_whitelists_from_asn "facebook" "32934"
    # You can add more lines like the above for other companies
    # fetch_company_whitelists_from_asn "another_company" "ASN_NUMBER"
    # https://www.radb.net/
    # https://stat.ripe.net/
    # https://bgp.he.net/AS11309#_graph4
}

populate_whitelists() {
    logverbose "Populating all whitelists"
    
    for file in "${whitelist_dir}/whitelist-"*.txt; do
        logverbose "$file"

        while IFS= read -r entry; do
            if [[ "$entry" = \#* ]]; then
                # Skip lines that start with "#"
                continue
            fi

            # If it's an IP
            if [[ $entry =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
                whitelist_ips[$entry]=1
            # If it's a CIDR notation
            elif [[ $entry =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]{1,2}$ ]]; then
                ipset add $whitelist_name "$entry" -exist
            # If it's a domain
            else
                ip=$(dig +short "$entry" | tail -1)
                if [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
                    whitelist_ips[$ip]=1
                fi
            fi
        done < "$file"
    done
}

# is this a valid ip format, and not on whitelist
is_valid_ip() {
    local ip=$1
    local stat=1

    # Regular expression to match valid IPv4 addresses
    local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"

    # Check if IP matches the regular expression
    if [[ $ip =~ $regex && ! "$ip" =~ ^(0|224) ]]; then
        # Ensure each octet is less than or equal to 255
        IFS='.' read -ra ip_array <<< "$ip"
        if [[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 && ${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 ]]; then
            stat=0
        else
            stat=1
        fi
    fi

    # Check against whitelist and invalidate if found
    if [[ ${whitelist_ips[$1]} ]]; then
        stat=1
    fi

    # Return true for valid and false for invalid IPs
    return $stat
}

# is this a valid network format, and not on whitelist
is_valid_network() {
    local network=$1
    local stat=1

    # Regular expression to match valid IPv4 CIDR format networks
    local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$"

    # Check if the network matches the regular expression
    if [[ $network =~ $regex ]]; then
        # Ensure each octet of the IP part is less than or equal to 255
        # Also check that the subnet mask is between 0 and 32

        IFS='.' read -ra ip_array <<< "${network%%/*}"  # Extract IP portion using the new method

        local mask=${network#*/} # Extract subnet mask

        if [[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 && ${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 && $mask -ge 0 && $mask -le 32 ]]; then
            stat=0
        fi
    fi

    # Check against whitelist and invalidate if found
    if [[ ${whitelist_nets[$1]} ]]; then
        stat=1
    fi

    # Return true for valid and false for invalid networks
    return $stat
}

# convert IP addresses to integers
ip2int () {
   echo "$1" | awk -F. '{ printf("%d\n", ($1 * 256^3) + ($2 * 256^2) + ($3* 256) + $4) }'
}

# convert integers to IP addresses
int2ip () {
   local ip=$(($1 / 256 / 256 / 256))
   ip+=".$(($1 / 256 / 256 % 256))"
   ip+=".$(($1 / 256 % 256))"
   ip+=".$(($1 % 256))"
   echo $ip
}

# Purge files older than specified retention_days in the given directory
purge_old_files() {
    local dir_to_purge="$1"
    local retention_days="$2"

    logverbose "Attempting to purge any stale archives ${dir_to_purge}"

    # Check if the variable is set and is not empty
    if [[ -z "$dir_to_purge" ]]; then
        echo "Error: Directory to purge is not set." >&2
        exit 1
    fi

    # Ensure path starts with base_path
    if [[ ! "$dir_to_purge" = "$base_path"* ]]; then
        echo "Error: Directory to purge does not start with base_path." >&2
        exit 1
    fi

    # Check for minimum directory depth of 1 from base_path
    local relative_path="${dir_to_purge#"$base_path/"}"  # Remove base_path prefix

    # If the relative path does not contain '/', then the depth is 1
    if [[ "$relative_path" != */* ]]; then
        local depth=1
    else
        local depth=$(awk -F"/" '{print NF-1}' <<< "$relative_path")  # Count number of '/' to determine depth
    fi

    if (( depth < 1 )); then
        echo "Error: Directory to purge is not deep enough from the base_path." >&2
        exit 1
    fi

    # Check if it's not pointing to critical directories (can add more as needed)
    for critical_dir in "/" "/bin" "/etc" "/usr" "/var" "/root"; do
        if [[ "$dir_to_purge" = "$critical_dir" || "$dir_to_purge" = "${critical_dir}/" ]]; then
            echo "Error: Directory to purge is set to a critical directory." >&2
            exit 1
        fi
    done

    # Check if the variable points to a directory
    if [[ ! -d "$dir_to_purge" ]]; then
        echo "Error: Directory to purge does not point to a valid directory." >&2
        exit 1
    fi

    find "$dir_to_purge" -type f -mtime +"$retention_days" -delete
}

# dshield format is 3 columns, network, endip, mask
populate_dshield() {
    local domain="dshield.org"
    local timestamp=$(date "+%Y%m%d_%H%M")
    local archive_file="${threat_intel_archives}/${timestamp}_${domain}.txt"
    
    logverbose "Downloading dshield and adding to blacklist"
    
    # Save the result to the archive file and read from it
    if ! $curl_cmd $dshield_feed | tee "$archive_file" | while read -r line; do
        if [[ "$line" = \#* ]]; then
            # Skip lines that start with "#"
            continue
        fi

        # Extract the first and second IP addresses and the subnet mask
        network_address=$(echo "$line" | awk '{print $1}')    # network address
        subnet_mask=$(echo "$line" | awk '{print $3}') # network mask

        # Construct a network in CIDR format
        network="${network_address}/${subnet_mask}"

        # Validate the network CIDR format
        if is_valid_network "$network"; then
            # Calculate the start and end integers based on the subnet mask
            start_int=$(ip2int "$network_address")
            end_int=$(( start_int + 2**(32-subnet_mask) - 2 ))

            # Loop over IP range and execute command on each IP, but skip the network address
            for ((i=$start_int; i<=$end_int; i++)); do
                ip=$(int2ip "$i")
                if is_valid_ip "$ip"; then
                    ipset add $blacklist_name "$ip" -exist
                fi
            done
        fi
    done; then
        logverbose "Error: Failed to download DShield feed. Exiting."
        exit 1
    fi
}

fetch_blacklists() {
    logverbose "Downloading threat intel feeds"
    
    for feed in "${threat_intel_feeds[@]}"; do
        echo "$feed"
        local domain=$(echo "$feed" | awk -F/ '{print $3}')  # Extract domain from URL
        local timestamp=$(date "+%Y%m%d_%H%M")
        local archive_file="${threat_intel_archives}/${timestamp}_${domain}.txt"
        local working_file="${blacklist_dir}/blacklist-${domain}.txt"
        
        # Download the feed and save it to both the working set and the archive
        if ! $curl_cmd "$feed" | tee "$working_file" > "$archive_file"; then
            logverbose "Error: Failed to download threat intel feed from ${domain}. Exiting."
            exit 1
        fi
    done
}

populate_blacklists() {
    logverbose "Processing threat intel feeds and adding to blacklist"

    # Process the downloaded threat intel feeds
    for file in "${blacklist_dir}/blacklist-"*.txt; do
        logverbose "$file"
        while IFS= read -r ip; do
            if is_valid_ip "$ip"; then
                ipset add $blacklist_name "$ip" -exist
            fi
        done < "$file"
    done
}

# geo blocks lists are networks in cidr format 1 net per line
geo_block() {
    local file_age
    local current_date=$(date +%s)  # Current date in seconds since 1970-01-01 00:00:00 UTC
    local two_weeks=$((14*24*60*60))  # 14 days in seconds

    logverbose "Starting geo-blocking process..."

    for country in "${banned_countries[@]}"; do
        local file_path="${geozones_dir}/${country}.zone"

        # If the file exists, determine its age
        if [[ -f "$file_path" ]]; then
            file_age=$(stat -c %Y "$file_path")  # File modification date in seconds since 1970-01-01 00:00:00 UTC
        else
            file_age=0
        fi

        # If the file doesn't exist or is older than two weeks, download a new copy
        if (( current_date - file_age > two_weeks )); then
            logverbose "Downloading fresh ban list for ${country}..."
            if ! $curl_cmd "https://www.ipdeny.com/ipblocks/data/countries/${country}.zone" -o "$file_path"; then
                logverbose "Error: Failed to download geo-blocking list for ${country}. Exiting."
                exit 1
            fi
        else
            logverbose "Using existing ban list for ${country}..."
        fi

        # Now, add entries from the file to the ipset
        while IFS= read -r IP; do
            ipset add $geoblock_name "$IP"
        done < "$file_path"
    done

    logverbose "Geo-blocking ban lists completed."
}

# test dns and basic connectivity first
check_internet_access() {
    # List of websites to check
    local websites=("google.com" "amazon.com" "cloudflare.com")

    local success=0
    for site in "${websites[@]}"; do
        # Pinging once and setting a timeout of 2 seconds
        if ping -c 1 -W 2 "$site" &> /dev/null; then
            success=1
            break
        fi
    done

    # If none of the pings were successful, exit the script
    if [[ $success -eq 0 ]]; then
        logverbose "No internet connection. Exiting."
        exit 1
    fi
    
    logverbose "Internet connection seems to be available"
}

preflight_checks() {
    # Check for available disk space
    local avail_space=$(df --output=avail -B1MB "$base_path" | tail -1 | awk '{print $1}')
    if (( avail_space < required_space_mb )); then
        send_alert "DISK SPACE ERROR" "Error: Not enough disk space in $base_path." "Required ${required_space_mb}MB but only ${avail_space}MB available."
        exit 1
    fi

    # Check if whitelist files exist
    local files=(
        "${whitelist_dir}/whitelist-ips.txt"
        "${whitelist_dir}/whitelist-domains.txt"
        "${whitelist_dir}/whitelist-nets.txt"
    )

    for file in "${files[@]}"; do
        if [[ ! -f "$file" ]]; then
            echo "Error: Required file $file is missing. Exiting."
            exit 1
        fi
    done

    # Check if 'dig', 'ipset', 'mail', 'curl', 'awk' and 'postfix' are installed
    if ! command -v dig &> /dev/null; then
        echo "Error: 'dig' is not installed. Run 'apt update && apt install -y dnsutils'."
        exit 1
    fi
    if ! command -v ipset &> /dev/null; then
        echo "Error: 'ipset' is not installed. Run 'apt update && apt install -y ipset'."
        exit 1
    fi
    if ! command -v mail &> /dev/null; then
        echo "Error: 'mail' is not installed. Run 'apt update && apt install -y mailutils'."
        exit 1
    fi
    if ! command -v postfix &> /dev/null; then
        echo "Error: 'postfix' is not installed. Run 'apt update && apt install -y postfix'."
        echo "Note:  choose 'Internet Site'"
        exit 1
    fi
    if ! command -v jq &> /dev/null; then
        echo "Error: 'jq' is not installed. Run 'apt update && apt install -y jq'."
        exit 1
    fi

    if ! command -v curl &> /dev/null; then
        echo "curl is not installed. Run 'apt update && apt install -y curl'."
    fi

    logverbose "Preflight checks passed"
}

send_alert() {
    local mail_subject="$1"
    local mail_body="$2"
    local log_entry="$3"
    local from_host=$(hostname)

    # Log the entry
    logger -p user.err "$log_entry"

    # Send email alert
    printf "%b" "$mail_body" | mail -s "$from_host: $mail_subject" "$admin_email"
}

check_firewall_logs() {
    local result=""
    local temp_file_all=$(mktemp)
    local temp_file_latest=$(mktemp)

    # Identify the latest syslog file
    local latest_syslog=$(ls -t /var/log/syslog* | head -n 1)

    # Capture all relevant lines from all syslog files
    grep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' /var/log/syslog* > "$temp_file_all"

    # Extract relevant lines from the latest syslog
    if [[ "$latest_syslog" == *.gz ]]; then
        zgrep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' "$latest_syslog" > "$temp_file_latest"
    else
        grep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' "$latest_syslog" > "$temp_file_latest"
    fi

    result+="COUNTERS\n\n"
    result+="$(iptables -L -v -n | grep -E "limit|blacklist|burst" -i)\n\n"

    result+="JUST DROPS (all syslog)\n\n"
    result+="$(cat "$temp_file_all" | awk -F'] ' '{print $2}' | cut -d' ' -f1 | sort | uniq -c | column -t)\n\n"

    result+="RECENT LOG ENTRIES (from $latest_syslog)\n\n"
    result+="$(tail -n 20 "$temp_file_latest")\n"

    # Cleanup temporary files
    rm -f "$temp_file_all" "$temp_file_latest"

    echo -ne "$result"
}


# script logic flow
preflight_checks
check_internet_access
fetch_searchbot_whitelists
fetch_blacklists
populate_whitelists
populate_dshield
populate_blacklists
geo_block
purge_old_files "$whitelist_archives" 14
purge_old_files "$threat_intel_archives" 7

# display counts
count=$(( $(ipset list $blacklist_name | wc -l) - 8 ))
logverbose "$count ip added to $blacklist_name"

count2=$(( $(ipset list $geoblock_name | wc -l) - 8 ))
logverbose "$count2 ip added to $geoblock_name"

logverbose "Run time: $SECONDS seconds"

# Check for conditions and send alerts
if [[ $count -lt 100 || $count2 -lt 100 || $SECONDS -gt 200 ]]; then
    # Construct the email body with details about the failure reason(s)
    mail_body="Blacklist Script Alert:\n\n"
    [[ $count -lt 100 ]] && mail_body+="Count for $blacklist_name is less than 100.\n"
    [[ $count2 -lt 100 ]] && mail_body+="Count for $geoblock_name is less than 100.\n"
    [[ $SECONDS -gt 200 ]] && mail_body+="Script run time ($SECONDS seconds) exceeded 200 seconds.\n"
    send_alert "Script Failure" "$mail_body" "Blacklist Failure: $blacklist_name: $count, $geoblock_name: $count2, Run time: $SECONDS seconds"
else
    # Everything seems fine, send a success alert
    mail_body="Blacklist Script completed successfully:\n\n"
    mail_body+="Count for $blacklist_name: $count\n"
    mail_body+="Count for $geoblock_name: $count2\n"
    mail_body+="Run time: $SECONDS seconds\n\n"
    mail_body+="$(check_firewall_logs)"
    send_alert "Script Success" "$mail_body" "Blacklist success: $blacklist_name: $count, $geoblock_name: $count2, Run time: $SECONDS seconds"
fi

Automation

Cron

Cron runs on various schedules. Every user has their own cron job list. This particular script needs to be run as root, so we need to edit root crontab with sudo or as root.

crontab -e
# m h  dom mon dow   command
30 3,15 * * * nice -n 19 /root/scripts/fw/blacklist.sh >> /root/blacklistlog.txt 2>&1

Startup Services

We need to trigger our startup script (detailed above) make that script a startup service, we do that by making a service called “blacklist-startup” and making it run the script when it starts. This sets the basic iptables, but the lists aren’t populated yet. So 150 seconds after boot it runs again to populate all of the lists.

vim /etc/systemd/system/blacklist-startup.service

[Unit]
Description=Run blacklist and reload UFW at startup and 5 minutes later
After=network.target

[Service]
Type=oneshot
ExecStart=/root/scripts/fw/blacklist-startup.sh
ExecStartPost=/bin/sleep 150
ExecStartPost=/root/scripts/fw/blacklist-startup.sh
TimeoutStartSec=500  # Increasing this since we have added sleep

[Install]
WantedBy=multi-user.target

Next, enable the service on every boot

sudo systemctl enable blacklist-startup.service

I can start it manually once if I want to right now or anytime too:

sudo systemctl start blacklist-startup.service

This is enough to start it once every time the server boots and load everything into perfect working order.

Verify Rule Hits

But, let’s reboot now and verify it’s loading ipsets. Remember that it takes a few minutes to load after a reboot. There are several things to check, so I’ve created a script to show me summary data that I might want to see often:

Scripted Firewall Log Review

(1:1207)# cat /root/scripts/fw/checkfirewalllogs.sh
#!/bin/bash

# just looks at firewall results and ipsets

check_firewall_logs() {
    local result=""
    local temp_file_all=$(mktemp)
    local temp_file_latest=$(mktemp)

    # Identify the latest syslog file
    local latest_syslog=$(ls -t /var/log/syslog* | head -n 1)

    # Capture all relevant lines from all syslog files
    grep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' /var/log/syslog* > "$temp_file_all"

    # Extract relevant lines from the latest syslog
    if [[ "$latest_syslog" == *.gz ]]; then
        zgrep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' "$latest_syslog" > "$temp_file_latest"
    else
        grep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' "$latest_syslog" > "$temp_file_latest"
    fi

    result+="COUNTERS\n\n"
    result+="$(iptables -L -v -n | grep -E "limit|blacklist|burst" -i)\n\n"

    result+="JUST DROPS (all syslog)\n\n"
    result+="$(cat "$temp_file_all" | awk -F'] ' '{print $2}' | cut -d' ' -f1 | sort | uniq -c | column -t)\n\n"

    result+="RECENT LOG ENTRIES (from $latest_syslog)\n\n"
    result+="$(tail -n 20 "$temp_file_latest")\n"

    # Cleanup temporary files
    rm -f "$temp_file_all" "$temp_file_latest"

    echo -ne "$result"
}


get_ipset_counts() {
        blacklist=$(( $(ipset list blacklist_set | wc -l) - 8 ))
        geoblock=$(( $(ipset list banned_countries | wc -l) - 8 ))


    echo -ne "IPSET DATA:\n\n"
    echo -ne "    BLACKLIST $blacklist\n"
    echo -ne "    GEOBLOCK $geoblock\n"
    echo
    echo
}

get_ipset_counts
check_firewall_logs

Run the Verify Script

Bad actors should be hitting your site every few seconds, so wait a few minutes and you’ll be able to see them:

(1:1208)# /root/scripts/fw/checkfirewalllogs.sh
IPSET DATA:

    BLACKLIST 11758
    GEOBLOCK 46175


COUNTERS

    0     0 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 3/min burst 10 LOG flags 0 level 4 prefix "[UFW BLOCK] "
    0     0 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 3/min burst 10 LOG flags 0 level 4 prefix "[UFW BLOCK] "
    5   220 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set blacklist_set src limit: avg 5/min burst 5 LOG flags 0 level 4 prefix "IPTables-Blacklist-Drop: "
    5   220 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set blacklist_set src
    8   364 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set banned_countries src limit: avg 5/min burst 5 LOG flags 0 level 4 prefix "IPTables-Country-Ban-Drop: "
  870 42846 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 10/sec burst 5
    0     0 LOG        tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 state NEW recent: UPDATE seconds: 60 hit_count: 5 name: DEFAULT side: source mask: 255.255.255.255 limit: avg 5/min burst 5 LOG flags 0 level 4 prefix "SSH-rate-limit: "
    0     0 LOG        tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80 state NEW recent: UPDATE seconds: 1 hit_count: 50 name: DEFAULT side: source mask: 255.255.255.255 limit: avg 5/min burst 5 LOG flags 0 level 4 prefix "HTTP-rate-limit: "
    0     0 LOG        tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:443 state NEW recent: UPDATE seconds: 2 hit_count: 100 name: DEFAULT side: source mask: 255.255.255.255 limit: avg 2/min burst 5 LOG flags 0 level 4 prefix "HTTPS-rate-limit: "
    0     0 LOG        tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp flags:0x17/0x02 #conn src/32 > 200 limit: avg 5/min burst 5 LOG flags 0 level 4 prefix "TCP-conn-limit: "
    0     0 REJECT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp flags:0x17/0x02 #conn src/32 > 200 /* Limit parallel TCP connections to 200 per IP */ reject-with icmp-port-unreachable
    0     0 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 3/min burst 10 LOG flags 0 level 4 prefix "[UFW ALLOW] "
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate INVALID limit: avg 3/min burst 10
    0     0 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 3/min burst 10 LOG flags 0 level 4 prefix "[UFW BLOCK] "
    0     0 ufw-logging-deny  all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 3/min burst 10
Chain ufw-user-limit (0 references)
    0     0 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 3/min burst 5 LOG flags 0 level 4 prefix "[UFW LIMIT BLOCK] "
Chain ufw-user-limit-accept (0 references)

JUST DROPS (all syslog)

2220  IPTables-Blacklist-Drop:
854   IPTables-Country-Ban-Drop:
15    TCP-conn-limit:

RECENT LOG ENTRIES (from /var/log/syslog)

Oct  7 12:37:42 hosts kernel: [ 3242.452806] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.163.63.23 DST=167.99.62.44 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=58418 DF PROTO=TCP SPT=36924 DPT=22 WINDOW=14600 RES=0x00 SYN URGP=0
Oct  7 12:37:43 hosts kernel: [ 3243.451190] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.163.63.23 DST=167.99.62.44 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=58419 DF PROTO=TCP SPT=36924 DPT=22 WINDOW=14600 RES=0x00 SYN URGP=0
Oct  7 12:37:45 hosts kernel: [ 3245.451706] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.163.63.23 DST=167.99.62.44 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=58420 DF PROTO=TCP SPT=36924 DPT=22 WINDOW=14600 RES=0x00 SYN URGP=0
Oct  7 12:37:49 hosts kernel: [ 3249.476737] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.163.63.23 DST=167.99.62.44 LEN=60 TOS=0x00 PREC=0x00 TTL=49 ID=58421 DF PROTO=TCP SPT=36924 DPT=22 WINDOW=14600 RES=0x00 SYN URGP=0
Oct  7 12:40:40 hosts kernel: [ 3420.351712] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.168.49.136 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=51 ID=63068 PROTO=TCP SPT=60346 DPT=22 WINDOW=42543 RES=0x00 SYN URGP=0
Oct  7 12:43:20 hosts kernel: [ 3580.458924] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.168.49.136 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=51 ID=19471 PROTO=TCP SPT=60346 DPT=22 WINDOW=42543 RES=0x00 SYN URGP=0
Oct  7 12:43:55 hosts kernel: [ 3615.952580] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.168.49.136 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=51 ID=19490 PROTO=TCP SPT=60346 DPT=22 WINDOW=42543 RES=0x00 SYN URGP=0
Oct  7 12:55:12 hosts kernel: [  484.480652] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=94.102.61.78 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=54321 PROTO=TCP SPT=48186 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0
Oct  7 12:57:55 hosts kernel: [  646.812359] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=94.102.61.78 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=54321 PROTO=TCP SPT=45463 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0
Oct  7 13:06:33 hosts kernel: [ 1164.618329] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.161.156.212 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=50 ID=57012 PROTO=TCP SPT=22382 DPT=22 WINDOW=43745 RES=0x00 SYN URGP=0
Oct  7 13:07:05 hosts kernel: [ 1197.410314] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.161.156.212 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=50 ID=9855 PROTO=TCP SPT=22382 DPT=22 WINDOW=43745 RES=0x00 SYN URGP=0
Oct  7 13:11:44 hosts kernel: [ 1476.514255] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=118.161.156.212 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=50 ID=28339 PROTO=TCP SPT=22382 DPT=22 WINDOW=43745 RES=0x00 SYN URGP=0
Oct  7 13:17:07 hosts kernel: [ 1799.484923] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=220.163.130.214 DST=167.99.62.44 LEN=40 TOS=0x00 PREC=0x00 TTL=44 ID=19635 PROTO=TCP SPT=48876 DPT=80 WINDOW=51882 RES=0x00 SYN URGP=0
Oct  7 13:42:16 hosts kernel: [ 3307.563421] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=117.202.211.92 DST=10.17.0.5 LEN=40 TOS=0x00 PREC=0x00 TTL=53 ID=4202 PROTO=TCP SPT=53545 DPT=22 WINDOW=47695 RES=0x00 SYN URGP=0
Oct  7 13:44:51 hosts kernel: [ 3463.191016] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=94.102.61.78 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=247 ID=54321 PROTO=TCP SPT=40021 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0
Oct  7 13:47:06 hosts kernel: [ 3598.164512] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=205.210.31.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=59 ID=21481 PROTO=TCP SPT=50243 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  7 13:51:42 hosts kernel: [ 3874.478499] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=121.229.185.160 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=42 ID=16957 DF PROTO=TCP SPT=46673 DPT=80 WINDOW=14600 RES=0x00 SYN URGP=0
Oct  7 13:51:44 hosts kernel: [ 3876.479595] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=121.229.185.160 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=42 ID=16958 DF PROTO=TCP SPT=46673 DPT=80 WINDOW=14600 RES=0x00 SYN URGP=0
Oct  7 13:56:17 hosts kernel: [ 4148.641671] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=205.210.31.200 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=59 ID=8349 PROTO=TCP SPT=52399 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  7 13:58:49 hosts kernel: [ 4300.997568] IPTables-Country-Ban-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=179.182.112.63 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=48 ID=42404 PROTO=TCP SPT=2131 DPT=22 WINDOW=6579 RES=0x00 SYN URGP=0

Attacking Yourself

Blacklisting is permanent (until removed or rebooted), but burst protection is temporary.

Burst protection doesn’t trigger as often, because most attackers do not burst attack until they are ready to scan all ports or fuzz directories.

You can view burst protection in effect by simply attacking yourself using ab or hping (among many tools)

AB

The ab (apache benchmark) is a good start. Here I’m sending 1000 connections at once (or as fast as I can), and it shows that only 16 of them actually connected. The rest were dropped because it exceeded the TCP connection limit. I am using nginx and checking a static file serve, so this didn’t cripple memory and probably wouldn’t cripple memory anyway. The point is the changes blocked it. NGINX also has a module for rate limiting, but that’s not fun is it?

AB is a great tool for simulating heavy loads of static pages.

──(james㉿kali)-[~]
└─$ ab -c 1000 -n 1000 http://hosts.jamesfraze.com:80/
This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking hosts.jamesfraze.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        nginx
Server Hostname:        hosts.jamesfraze.com
Server Port:            80

Document Path:          /
Document Length:        123 bytes

Concurrency Level:      1000
Time taken for tests:   3.234 seconds
Complete requests:      1000
Failed requests:        984
   (Connect: 0, Receive: 0, Length: 984, Exceptions: 0)
Non-2xx responses:      984
Total transferred:      301368 bytes
HTML transferred:       1968 bytes
Requests per second:    309.17 [#/sec] (mean)
Time per request:       3234.467 [ms] (mean)
Time per request:       3.234 [ms] (mean, across all concurrent requests)
Transfer rate:          90.99 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       42  107 220.1     52    1099
Processing:    43   95 135.6     55    1292
Waiting:       43   95 135.6     55    1292
Total:         86  202 257.8    113    2131

Percentage of the requests served within a certain time (ms)
  50%    113
  66%    125
  75%    153
  80%    188
  90%    381
  95%   1097
  98%   1126
  99%   1138
 100%   2131 (longest request)

hping3 to SYN Flood

This flood attack seemed to really slow down my ssh session. I ran it from another computer and until I stopped it I couldn’t use ssh on the server. I had hoped to block this, but it looks like the act of blocking a SYN flood is more than the server can keep up with.

Of course you know this is considered an attack, so do not run this on equipment or against equipment that you don’t own. I run it for a few seconds on my own VPS, but I know it triggers various alarms still.

sudo hping3 -S -p 22 --flood hosts.jamesfraze.com

Now review your logs:

Use the script to check firewall logs, or here are some of the ways to check manually if you like retyping the commands

iptables -L -v -n  | egrep "limit|blacklist|burst" -i
egrep 'IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' /var/log/syslog | tail -n 20

IP Table Logs

Viewing Blacklist

You can view the ipset easily, and grep for specific ips too:

(1:528)# ipset list
Name: blacklist_set
Type: hash:ip
Revision: 5
Header: family inet hashsize 4096 maxelem 65536 bucketsize 12 initval 0xd670485e
Size in memory: 279112
References: 2
Number of entries: 11892
Members:
198.235.24.20
194.26.135.217
167.94.145.245
106.201.147.240
103.15.252.225
...
etc

Or search for particular ips:

(1:529)# ipset list | grep 1.85.217.4
1.85.217.4

Or see how many times an ip attacked and what they are doing.

(1:1209)# grep 185.233.19.242 /var/log/syslog
Oct  1 20:14:22 vhost2023-jamesfraze kernel: [179730.737837] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=244 ID=21346 PROTO=TCP SPT=58914 DPT=80 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  1 21:40:12 vhost2023-jamesfraze kernel: [184880.383084] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=14250 PROTO=TCP SPT=58914 DPT=80 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  2 01:02:55 vhost2023-jamesfraze kernel: [197042.852488] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=51949 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  2 01:20:01 vhost2023-jamesfraze kernel: [198069.068433] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=51933 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  2 04:19:22 vhost2023-jamesfraze kernel: [208830.207009] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=20973 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  2 05:40:22 vhost2023-jamesfraze kernel: [213689.463176] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=65078 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  3 10:29:35 vhost2023-jamesfraze kernel: [317439.942659] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=34896 PROTO=TCP SPT=58914 DPT=80 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  3 12:23:01 vhost2023-jamesfraze kernel: [324246.467301] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=244 ID=21279 PROTO=TCP SPT=58914 DPT=80 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  3 16:13:44 vhost2023-jamesfraze kernel: [338088.329781] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=16505 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  3 16:15:14 vhost2023-jamesfraze kernel: [338178.327318] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=52228 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  3 20:08:55 vhost2023-jamesfraze kernel: [352199.511494] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=7187 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  3 20:34:53 vhost2023-jamesfraze kernel: [353757.699169] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=20612 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  5 00:28:44 vhost2023-jamesfraze kernel: [454185.921552] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=244 ID=52499 PROTO=TCP SPT=58914 DPT=80 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  5 00:47:10 vhost2023-jamesfraze kernel: [455291.619659] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=56836 PROTO=TCP SPT=58914 DPT=80 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  5 04:59:29 vhost2023-jamesfraze kernel: [470430.440921] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=32130 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  5 05:01:13 vhost2023-jamesfraze kernel: [470534.340539] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=51485 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  5 08:34:30 vhost2023-jamesfraze kernel: [ 6569.073055] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=2648 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  5 09:47:51 vhost2023-jamesfraze kernel: [10969.518992] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=1927 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  6 19:09:09 hosts kernel: [  573.835520] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=8393 PROTO=TCP SPT=58914 DPT=443 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  7 00:08:57 hosts kernel: [15892.934005] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=10.17.0.5 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=36116 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
Oct  7 00:17:07 hosts kernel: [16382.842404] IPTables-Blacklist-Drop: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=185.233.19.242 DST=167.99.62.44 LEN=44 TOS=0x00 PREC=0x00 TTL=245 ID=39736 PROTO=TCP SPT=58914 DPT=22 WINDOW=1024 RES=0x00 SYN URGP=0
1

We labeled each type of drop specifically, so you could look for those if you wanted, even in past sylogs (searching all syslogs this time):

(1:1211)# grep TCP-conn-limit /var/log/syslog*
/var/log/syslog.1:Sep 29 22:19:15 hosts kernel: [348435.070354] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=37246 DF PROTO=TCP SPT=37856 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 22:19:15 hosts kernel: [348435.070474] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=35039 DF PROTO=TCP SPT=37810 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 22:19:15 hosts kernel: [348435.070523] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=2393 DF PROTO=TCP SPT=37746 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 22:19:15 hosts kernel: [348435.070563] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=27104 DF PROTO=TCP SPT=37752 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 22:19:15 hosts kernel: [348435.070661] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=18050 DF PROTO=TCP SPT=37798 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 18:25:57 vhost2023-jamesfraze kernel: [  430.018413] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=61847 DF PROTO=TCP SPT=35494 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 18:25:57 vhost2023-jamesfraze kernel: [  430.018501] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=49675 DF PROTO=TCP SPT=35514 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 18:25:57 vhost2023-jamesfraze kernel: [  430.018563] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=35308 DF PROTO=TCP SPT=35438 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 18:25:57 vhost2023-jamesfraze kernel: [  430.018619] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=33907 DF PROTO=TCP SPT=35424 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 29 18:25:57 vhost2023-jamesfraze kernel: [  430.018669] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=45227 DF PROTO=TCP SPT=35380 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 30 05:30:25 vhost2023-jamesfraze kernel: [40297.145173] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=42962 DF PROTO=TCP SPT=54856 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 30 05:30:25 vhost2023-jamesfraze kernel: [40297.145286] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=64507 DF PROTO=TCP SPT=54744 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 30 05:30:25 vhost2023-jamesfraze kernel: [40297.145337] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=38439 DF PROTO=TCP SPT=54820 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 30 05:30:25 vhost2023-jamesfraze kernel: [40297.145403] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=17192 DF PROTO=TCP SPT=54834 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0
/var/log/syslog.1:Sep 30 05:30:25 vhost2023-jamesfraze kernel: [40297.145457] TCP-conn-limit: IN=eth0 OUT= MAC=ca:95:0d:55:c7:91:fe:00:00:00:01:01:08:00 SRC=47.186.105.130 DST=10.17.0.5 LEN=60 TOS=0x00 PREC=0x00 TTL=53 ID=5245 DF PROTO=TCP SPT=54846 DPT=80 WINDOW=64240 RES=0x00 SYN URGP=0

Remember the script we saved that has all of the drop types:

egrep 'IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' /var/log/syslog

We can also summarize by counting each type of drop (for example I have 1719 blacklist drops and 15 TCP-conn-limit drops in the 4 days since I’ve created this server):

(1:1215)#  grep -E 'IPTables-Country-Ban-Drop:|IPTables-Blacklist-Drop:|SSH-rate-limit:|HTTP-rate-limit:|HTTPS-rate-limit:|TCP-conn-limit:' /var/log/syslog*  | awk -F'] ' '{print $2}' | cut -d' ' -f1 | sort | uniq -c | column -t
2220  IPTables-Blacklist-Drop:
854   IPTables-Country-Ban-Drop:
15    TCP-conn-limit:

Remember, the firewall log checker script does all of this at once too.

Dynamic UFW Script

I am rewriting the blacklist script to combine the dynamic script from before. The dynamic script checks dynamic hostname and opens the firewall for that ip. This allows a default deny with nothing open on port 22 for example, except your home router or hotspot.

It will currently work side by side with the blacklist script. We loaded blacklists in the rules.before, and all other changes manually and by the dynamic host rule allow script go after these rules. This means I would modify my ruleset to remove 0.0.0.0/0 access to port 22 and instead use my dynamic script so only I can ssh into my servers.

https://grimoire.jamesfraze.com/operating-system/linux/bash-script-update-ufw-rule-for-dynamic-host/

Last updated on 4 Oct 2023
Published on 4 Oct 2023