iptables scripts

This is a revisit of the use of iptables from my Shell Scripts web page, showing how I use these. You need to develop your own logic and shell scripting with crontab for blocking IP addresses.

We are not able to spend time knowing how everything in Linux works. The commands shown appear to be reasonable for use with Nginx and Debian 12 using WordPress.

If we experiment with iptables and get odd behaviour such as the wget command failing, or domain names not resolving, modify the entries you made.

An example of logic flow is:

Run the firewall.sh script to clear out all iptable entries, then the ip.sh script one a week from crontab to have a clean slate for all blocking.

In the mean time, run an update script once a night to find bad IP addresses in /var/log/nginx/error.log or anywhere else you want to examine. After each run, ensure the next nightly run does not repeat the same IP addresses.

If your site also uses an AAAA record, filter out IP6 addresses using grep -v “::” as this is way tooooo complex and not needed.

Add the nightly IP addresses (if any) to a separate .txt file that grows in size which the ip.sh script uses to include all previous addresses you collected each night.


The main script - ip.sh

Basic iptables commands

cd /home/ec2-user (or admin - whatever you use)
Assuming eth0 is your primary ethernet device (Akamai linode does not require eth1, but check your operating system in case you need both eth0 and ethe1)

vi ip.sh

#!/bin/bash

/home/ec2-user/firewall.sh

# from https://www.digitalocean.com/community/tutorials/iptables-essentials-common-firewall-rules-and-commands

iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

for i in $(cat /var/www/html/countries/domains.txt); do
    echo "Blocking all traffic to and from $i"
    iptables -I INPUT -s $i -j DROP
    iptables -I OUTPUT -d $i -j REJECT
done

# from https://www.cyberciti.biz/faq/iptables-connection-limits-howto/

IPT=/usr/sbin/iptables
# Max connection in seconds
SECONDS=100
# Max connections per IP
BLOCKCOUNT=10
# default action can be DROP or REJECT
 DACTION="DROP"
 $IPT -A INPUT -p tcp --dport 80 -i eth0 -m state --state NEW -m recent --set
 $IPT -A INPUT -p tcp --dport 80 -i eth0 -m state --state NEW -m recent --update --seconds ${SECONDS} --hitcount ${BLOCKCOUNT} -j ${DACTION}

iptables -A INPUT -p tcp --syn --dport 443 -m connlimit --connlimit-above 10 -j DROP
iptables -A INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 10 -j DROP
iptables -A INPUT -p tcp --syn --dport 22 -m connlimit --connlimit-above 10 -j DROP

# Insert IP ranges causing you major concern here:
# from USA DIGITALOCEAN-ASN
iptables -A INPUT -s 104.248.64.0/22 -j DROP
# from Tokyo Kagoya
iptables -A INPUT -s 210.128.0.0/13 -j DROP
iptables -A INPUT -s 153.127.224.0/19 -j DROP

# Now run the specific blacklists - countries and your own shell scripts that record bad IP actors:

/home/ec2-user/blacklist.sh

iptables -L -vn

exit

[save and exit]

chmod 777 ip.sh
chgrp ec2-user ip.sh

 

This is the firewall.sh script to clean out all iptables entries, good for when we refresh everything:

cd /home/ec2-user (or admin)

vi firewall.sh

#!/bin/bash
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
exit

[save and exit]

chmod 777 firewall.sh
chgrp ec2-user firewall.sh

We now add the generic blacklist.sh script, adding our own various .txt files that list bad IP actors.

 

blacklist.sh

cd /home/ec2-user (or admin)

vi blacklist.sh

#!/bin/sh
# IP blacklisting script for Linux servers
# Pawel Krawczyk 2014-2015
# documentation https://github.com/kravietz/blacklist-scripts
# iptables logging limit
LIMIT="10/minute"
# try to load config file
# it should contain one blacklist URL per line
config_file="/etc/ip-blacklist.conf"
if [ -f "${config_file}" ]; then
    source ${config_file}
else
    # if no config file is available, load default set of blacklists
    # URLs for further blocklists are appended using the classical
    # shell syntax:  "$URLS new_url"
    # Emerging Threats lists offensive IPs such as botnet command servers
    URLS="http://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt"
    # Blocklist.de collects reports from fail2ban probes, listing password brute-forces, scanners and other offenders
    URLS="$URLS https://www.blocklist.de/downloads/export-ips_all.txt"

# YOUR OWN BLACKLISTS

URLS="$URLS https://mydomain.com/countries/russia.txt"
URLS="$URLS https://mydomain.com/countries/china.txt"
URLS="$URLS https://mydomain.com/countries/northkorea.txt"
URLS="$URLS https://mydomain.com/countries/iran.txt"
URLS="$URLS https://mydomain.com/countries/india.txt"
fi
link_set () {
    if [ "$3" = "log" ]; then
        iptables -A "$1" -m set --match-set "$2" src,dst -m limit --limit "$LIMIT" -j LOG --log-prefix "BLOCK $2 "
    fi
    iptables -A "$1" -m set --match-set "$2" src -j DROP
    iptables -A "$1" -m set --match-set "$2" dst -j DROP
}
# This is how it will look like on the server
# Chain blocklists (2 references)
#  pkts bytes target     prot opt in     out     source               destination
#     0     0 LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set manual-blacklist src,dst limit: avg 10/min burst 5 LOG flags 0 level 4 prefix "BLOCK manual-blacklist "
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set manual-blacklist src,dst
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set rules.emergingthreats src
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set rules.emergingthreats dst
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set www.blocklist.de src
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set www.blocklist.de dst
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set www.badips.com src
#     0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set www.badips.com dst
blocklist_chain_name=blocklists
# check if we are on OpenWRT
if [ "$(which uci 2>/dev/null)" ]; then
    # we're on OpenWRT
    wan_iface=pppoe-wan
    IN_OPT="-i $wan_iface"
    INPUT=input_rule
    FORWARD=forwarding_rule
    COMPRESS_OPT=""
else
    COMPRESS_OPT="--compressed"
    INPUT=INPUT
    FORWARD=FORWARD
fi
# create main blocklists chain
if ! iptables -nL | grep -q "Chain ${blocklist_chain_name}"; then
    iptables -N ${blocklist_chain_name}
fi
# inject references to blocklist in the beginning of input and forward chains
if ! iptables -nL ${INPUT} | grep -q ${blocklist_chain_name}; then
  iptables -I ${INPUT} 1 ${IN_OPT} -j ${blocklist_chain_name}
fi
if ! iptables -nL ${FORWARD} | grep -q ${blocklist_chain_name}; then
  iptables -I ${FORWARD} 1 ${IN_OPT} -j ${blocklist_chain_name}
fi
# flush the chain referencing blacklists, they will be restored in a second
iptables -F ${blocklist_chain_name}
# create the "manual" blacklist set
# this can be populated manually using ipset command:
# ipset add manual-blacklist a.b.c.d
set_name="manual-blacklist"
if ! ipset list | grep -q "Name: ${set_name}"; then
    ipset create "${set_name}" hash:net
fi
link_set "${blocklist_chain_name}" "${set_name}" "$1"
# download and process the dynamic blacklists
for url in $URLS
do
    # initialize temp files
    unsorted_blocklist=$(mktemp)
    sorted_blocklist=$(mktemp)
    new_set_file=$(mktemp)
    headers=$(mktemp)
    # download the blocklist
    set_name=$(echo "$url" | awk -F/ '{print substr($3,0,21);}') # set name is derived from source URL hostname
    curl -L -v -s ${COMPRESS_OPT} -k "$url" >"${unsorted_blocklist}" 2>"${headers}"
    # this is required for blocklist.de that sends compressed content regardless of asked or not
    if [ -z "$COMPRESS_OPT" ]; then
        if grep -qi 'content-encoding: gzip' "${headers}"; then
            mv "${unsorted_blocklist}" "${unsorted_blocklist}.gz"
            gzip -d "${unsorted_blocklist}.gz"
        fi
    fi
    # autodetect iblocklist.com format as it needs additional conversion
    if echo "${url}" | grep -q 'iblocklist.com'; then
        if [ -f /etc/range2cidr.awk ]; then
            mv "${unsorted_blocklist}" "${unsorted_blocklist}.gz"
            gzip -d "${unsorted_blocklist}.gz"
            awk_tmp=$(mktemp)
            awk -f /etc/range2cidr.awk <"${unsorted_blocklist}" >"${awk_tmp}"
            mv "${awk_tmp}" "${unsorted_blocklist}"
        else
            echo "/etc/range2cidr.awk script not found, cannot process ${unsorted_blocklist}, skipping"
            continue
        fi
    fi
    sort -u <"${unsorted_blocklist}" | egrep "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$" >"${sorted_blocklist}"
    # calculate performance parameters for the new set
    if [ "${RANDOM}" ]; then
        # bash
        tmp_set_name="tmp_${RANDOM}"
    else
        # non-bash
        tmp_set_name="tmp_$$"
    fi
    new_list_size=$(wc -l "${sorted_blocklist}" | awk '{print $1;}' )
    hash_size=$(expr $new_list_size / 2)
    if ! ipset -q list ${set_name} >/dev/null ; then
        ipset create ${set_name} hash:net family inet
    fi
    # start writing new set file
    echo "create ${tmp_set_name} hash:net family inet hashsize ${hash_size} maxelem ${new_list_size}" >>"${new_set_file}"
    # convert list of IPs to ipset statements
    while read line; do
        echo "add ${tmp_set_name} ${line}" >>"${new_set_file}"
    done <"$sorted_blocklist"
    # replace old set with the new, temp one - this guarantees an atomic update
    echo "swap ${tmp_set_name} ${set_name}" >>"${new_set_file}"
    # clear old set (now under temp name)
    echo "destroy ${tmp_set_name}" >>"${new_set_file}"
    # actually execute the set update
    ipset -! -q restore < "${new_set_file}"
    link_set "${blocklist_chain_name}" "${set_name}" "$1"
    # clean up temp files
    rm "${unsorted_blocklist}" "${sorted_blocklist}" "${new_set_file}" "${headers}"
done

[save and exit]

chmod 777 blacklist.sh; chown root blacklist.sh; chgrp ec2-user blacklist.sh

With the countries you choose to block (replace mydomain.com with your own domain name) add .txt files, such as russia.txt and so on as shown above.

Then you add your own .txt files for bad actors attempting things like wp-login.php on WordPress. This is where you develop your own scripts and what you want to block. Place the URL entries and .txt files in the above script in the same way as blocked countries.

SEE THE INFO BELOW for country IP addresses.

Block domains - domains.txt

Here is an example of /var/www/html/countries/domains.txt you may create:

cd /var/www/html/countries
vi domains.txt
binance.com
binance.org
openai.com
anthropic.com
[save and exit]
chmod 777 domains.txt

We have already added this example to ip.sh


Domain blocking is added at the top of ip.sh as it may fail if further below in the blacklist.sh code.

We will now do an example where the .txt file is added to blacklist.sh along with the countries we choose to block.

Block failed wp-login.php and optionally failed nginx attempts.

 

cd /home/ec2-user

All the grep -v entries show an example of bypassing a range of broadband IP addresses from an Internet provider.
YOu need to find your provider's addresses, or simply use your own static IP4 address.

vi wplogin.sh

#!/bin/bash
d=`date | awk '{print $2,$3,$NF}'|tr " " "-"`
echo "wplogin hacker IPs:" $d >> /home/ec2-user/info.log
for i in `sort /var/log/nginx/access.log|grep "wp-login.php" | awk '{print $1}'|grep -v "::"|grep -v "117.20.68"|grep -v "117.20.69"|grep -v "117.20.70"|grep -v "117.20.71"| sort -u`
do
a=""
a=`grep $i /var/www/html/countries/all.txt`
if [ "$a" = "" ] ;
then
echo $i >> /var/www/html/countries/all.txt
echo $i >> /var/www/html/countries/wplogin.txt
echo $i >> /var/www/html/countries/new.txt
# iptables -I INPUT -p tcp -s $i -j DROP will be used in a crontab script to update the firewall rules each night
echo $i >> /home/ec2-user/info.log
fi
done

cp -p /var/log/nginx/access.log /var/log/nginx/archive-$d-access.log
:> /var/log/nginx/access.log

exit

[save and exit]
chmod 777 wplogin.txt
chgrp ec2-user wplogin.txt

Make sure the above files exist as 777, with no entries - that is, all.txt, wplogin.txt, and new.txt.

Then add this line to blacklist.sh along with the URL you used to block countries: (use your own domain name)

 URLS="$URLS https://mydomain.com/countries/wplogin.txt"

This means crontab can run ip.sh once a week, but we need the script to update each night's bad IIP addresses:

In crontab, add these lines:

# At 3am each night check bad wplogin attempts etc.
0 3 * * * sudo bash /home/ec2-user/wplogin.sh >/dev/null 2>&1
# Then at 3:30am redo any new firewall rules from wplogin.sh
30 3 * * * /home/ec2-user/new.sh /dev/null 2>&1
# Then at 4:30am each Saturday redo all firewall rules
30 4 * * 6 /home/ec2-user/ip.sh >/dev/null 2>&1

Now add new.sh:

vi new.sh

#!/bin/sh
for i in `cat /var/www/html/countries/new.txt`
do
 iptables -I INPUT -p tcp -s $i -j DROP
done
:> /var/www/html/countries/new.txt
exit

[save and exit, 777 and grp ownership as usual]

The new.txt file is cleared out so we have not duplicates on the next night's run.





The scripting above includes the logic I mentioned earlier so that we can manage nightly runs and the weekly refreshed run, without duplicating or missing bad IP’s we want to block.

You can add other .txt files like wplogin.sh. Here is an example of blocking people who try to access /usr/share/nginx/html:

cd /home/ec2-user

vi /home/ec2-user/nginx.sh

# BASED ON HAVING MY OWN STATIC IP ADDRESS or IP Range

d=`date | awk '{print $2,$3,$NF}'|tr " " "-"`
echo "/usr/share/nginx/html php hacker IPs:" $d >> /home/ec2-user/info.log

for i in `grep "/usr/share/nginx/html" /var/log/nginx/error.log | grep ".php"|grep client|awk -F : '{print $6}'|awk -F, '{print $1}'|awk '{print $1}'|sort -u|grep -v "::" | grep -v "117.20.68"|grep -v "117.20.69"|grep -v "117.20.70"|grep -v "117.20.71"| sort -u|grep -v No`
do
a=""
a=`grep $i /var/www/html/countries/all.txt`
if [ "$a" = "" ] ;
then
echo $i >> /var/www/html/countries/nginx.txt
echo $i >> /var/www/html/countries/all.txt
echo $i >> /var/www/html/countries/new.txt
# iptables -I INPUT -p tcp -s $i -j DROP --> this will be used in the new.sh script
echo $i >> /home/ec2-user/info.log
fi
done

cp -p /var/log/nginx/error.log /var/log/nginx/archive-$d-error.log
:> /var/log/nginx/error.log

[save and exit - chmod 777 and chgrp ec2-user]

Now add a line to the new.sh script to loop through nginx.txt. (The logic for new.txt will take care of itself already.)

crontab -e

# At 3:05am each night check bad nginx /user/share/nginx/html attempts
5 3 * * * sudo bash /home/ec2-user/nginx.sh >/dev/null 2>&1

Then add the URL to blacklist.sh:
URLS="$URLS https://mydomain.com/countries/nginx.txt"

 

Other iptables entries to ip.sh

I am cautious about adding further blocking in ip.sh.

Here are some lines that caused me issues, which I have not yet further refined or tested:

# https://www.baeldung.com/linux/iptables-packet-rate-limit


iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -m limit --limit 20/min --limit-burst 30 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -m limit --limit 20/min --limit-burst 30 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j DROP
iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j DROP

# These will not work with transferring files to AWS S3 buckets or use of wget command. They are given as reference only.
# iptables -N RATE_LIMIT
# iptables -A RATE_LIMIT -m limit --limit 1mbit/s -j ACCEPT
# iptables -A RATE_LIMIT -j DROP
#  iptables -A OUTPUT -o eth0 -j RATE_LIMIT

I was having problems with the wget command, domain names being resolved, and transferring files to Amazon S3 buckets.

I’ll update this section if I refine further.

Here is a countries.tar.zip file with 14 countries. I only use a few countries.

If you wish to manually add bad IP addresses, you append all.txt and new.txt with the address.

This ip2location file will not automatically capture all IP4 addresses from each country in the list. You need to use ip2location’s website to download or update lists, using the format shown in the .txt files.

Start typing and press Enter to search