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.