Skip to content

Iptables

The Basics

After much messing about with the likes of UFW and CSF firewalls, I decided to make the jump and use Iptables directly. Iptables works by comparing network traffic against a set of rules. The rules are worked through in order and the first applicable rule is applied and not over-ridden by any other rule further down the chain. For this reason, the ordering of rules is very important. The rules are organised into chains and the three default chains that are normally configured are INPUT, FORWARD, and OUTPUT. The INPUT chain deals with incoming traffic, the FORWARD chain deals with traffic to be forwarded on to another address on the local network, and OUTPUT deals with all outgoing traffic. Custom chains can also be created to better organise rules. Iptables only deals with IPv4 traffic. If your server uses IPv6 too then Ip6tables must also be configured with a set of rules.

Using Iptables at the command-line, rules can be appended to the end of a chain or inserting at a specified line number.

Chains are also set with a default policy. This policy is normally configured to accept all traffic or deny all traffic. I set mine to deny all and explicitly allow the ports I want opening in the rules.

Some Basic Commands

sudo iptables -L
This lists all the rules currently defined in all of the chains.

sudo iptables -L INPUT
This lists all the rules currently defined in the INPUT chain. -v can also be added for a more detailed output, and --line-numbers for the line numbers to be displayed.

sudo iptables -A  -i <interface> -p <protocol (tcp/udp) > -s <source> --dport <port no.>  -j <target>
-A is used to append a rule to the end of the chain.
-i specifies the interface to apply the rule to. this can be the name of an interface, or 'lo' for the local interface, ! -i lo for all interfaces except the local interface, or 'all' for all interfaces.
-j specifies the jump target, or the action that iptables performs when a rule is matched. This is commonly set to one of 'DROP', 'REJECT', 'ACCEPT', 'LOG', or a custom chain.
Hopefully the remaining switches are self explanatory!

Rules can also be inserted at a specific line number by using -I instead of -A. For example...

sudo iptables -I INPUT 1 -s 59.145.170.30 -j ACCEPT

The above command inserts a rule into the INPUT chain at position 1. The rule accepts all incoming traffic from the specified IP address, in effect whitelisting it. To work correctly, this rule should be inserted before the drop rules.

There's much more that can be said but I'm intentionally keeping it short here. A much more detailed guide to Iptables can be found at [https://www.booleanworld.com/depth-guide-iptables-linux-firewall/].

Creating the Rules

Defining the Tables, Chains, and Policies

First we'll define all of our tables, chains and default policies. We're also going to insert some rules into the mangle table. The mangle table is used to modify or mark packets and their header information.

From [https://javapipe.com/blog/iptables-ddos-protection/]:

If you want to block a DDoS attack with iptables, performance of the iptables rules is extremely important. Most TCP-based DDoS attack types use a high packet rate, meaning the sheer number of packets per second is what causes the server to go down. That’s why you want to make sure that you can process and block as many packets per second as possible. You’ll find that most if not all guides on how to block DDoS attacks using iptables use the filter table and the INPUT chain for anti-DDoS rules. The issue with this approach is that the INPUT chain is only processed after the PREROUTING and FORWARD chains and therefore only applies if the packet doesn’t match any of these two chains. This causes a delay in the filtering of the packet which consumes resources. In conclusion, to make our rules as effective as possible, we need to move our anti-DDoS rules as far up the chains as possible. The first chain that can apply to a packet is the PREROUTING chain, so ideally we’ll want to filter the bad packets in this chain already. However, the filter table doesn’t support the PREROUTING chain. To get around this problem, we can simply use the mangle table instead of the filter table for our anti-DDoS iptables rules. It supports most if not all rules that the filter table supports while also supporting all iptables chains.

Open up a text editor and input the following:

##################################
##### NAT Table ##################
##################################
#
*nat
:PREROUTING ACCEPT [22468:1121612]
:INPUT ACCEPT [4751:272885]
:OUTPUT ACCEPT [40674:3599411]
:POSTROUTING ACCEPT [39743:3195031]
COMMIT
#
##################################
##### Mangle Table ###############
##################################
#
*mangle
:PREROUTING ACCEPT [772928:345083910]
:INPUT ACCEPT [772928:345083910]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [745618:356007027]
:POSTROUTING ACCEPT [744649:355589121]
#
##################################
##### DOS Prevention #############
##################################
#
### 1: Drop invalid packets ### 
-A PREROUTING -m conntrack --ctstate INVALID -j DROP  
#
### 2: Drop TCP packets that are new and are not SYN ### 
-A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP 
#
### 3: Drop SYN packets with suspicious MSS value ### 
-A PREROUTING -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP  
#
### 4: Block packets with bogus TCP flags ### 
-A PREROUTING -p tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -j DROP 
-A PREROUTING -p tcp --tcp-flags FIN,SYN FIN,SYN -j DROP 
-A PREROUTING -p tcp --tcp-flags SYN,RST SYN,RST -j DROP 
-A PREROUTING -p tcp --tcp-flags FIN,RST FIN,RST -j DROP 
-A PREROUTING -p tcp --tcp-flags FIN,ACK FIN -j DROP 
-A PREROUTING -p tcp --tcp-flags ACK,URG URG -j DROP 
-A PREROUTING -p tcp --tcp-flags ACK,FIN FIN -j DROP 
-A PREROUTING -p tcp --tcp-flags ACK,PSH PSH -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL ALL -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL NONE -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP  
#
### 5: Block spoofed packets ### 
-A PREROUTING -s 224.0.0.0/3 -j DROP 
-A PREROUTING -s 169.254.0.0/16 -j DROP 
-A PREROUTING -s 172.16.0.0/12 -j DROP 
-A PREROUTING -s 192.0.2.0/24 -j DROP 
-A PREROUTING -s 192.168.0.0/16 -j DROP 
-A PREROUTING -s 10.0.0.0/8 -j DROP 
-A PREROUTING -s 0.0.0.0/8 -j DROP 
-A PREROUTING -s 240.0.0.0/5 -j DROP 
-A PREROUTING -s 127.0.0.0/8 ! -i lo -j DROP
#
### 6: Drop fragments in all chains ### 
-A PREROUTING -f -j DROP  
#
COMMIT
#
##################################
##### Raw Table ##################
##################################
#
*raw
:PREROUTING ACCEPT [772928:345083910]
:OUTPUT ACCEPT [745619:356007171]
COMMIT
#
##################################
##### Filter Table ###############
##################################
#
*filter
#
##################################
##### Create Chains ##############
##################################
#
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
:ALLOWDYNIN - [0:0]
:ALLOWDYNOUT - [0:0]
:ALLOWIN - [0:0]
:ALLOWOUT - [0:0]
:FAIL2BAN - [0:0]
:BLOCKLISTS - [0:0]
:BLOCKLISTSOUT - [0:0]
:LOCALINPUT - [0:0]
:LOCALOUTPUT - [0:0]
:DROPOUT - [0:0]
:DROPIN - [0:0]
#

Save it as ip4tables.save. We'll be adding to it shortly.

I have defined the default chains here as well as a whole bunch of custom chains which I'll explain later. I have also defined a chain for fail2ban. If you have no wish to install fail2ban then feel free to leave this chain out. On all chains, I have defined a default 'drop all' policy (0:0).

The INPUT Chain

Open up ip4tables.save and add the following (feel free to tweak to your requirements):

##################################
##### Input Chain ################
##################################
#
##################################
##### Rules Start Here ###########
##################################
#
##### Jump to LOCALINPUT chain
##############################
#
-A INPUT ! -i lo -j LOCALINPUT
#
##### Accept all local traffic
##############################
#
-A INPUT -i lo -j ACCEPT
#
##### Log all traffic that has made it this far
###############################################
#
-A INPUT -j LOG --log-prefix "[IPTABLES] " --log-tcp-options
#
##### Limit connections per source IP
#####################################
#
-A INPUT -p tcp -m connlimit --connlimit-above 111 -j REJECT --reject-with tcp-reset
#
##### Good practise is to explicately reject AUTH traffic so that it fails fast
###############################################################################
#
-A INPUT ! -i lo -p tcp --dport 113 --syn -m conntrack --ctstate NEW -j REJECT --reject-with tcp-reset
#
##### Rate limit Incoming Pings
###############################
#
-A INPUT ! -i lo -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -m limit --limit 1/sec --limit-burst 1 -j ACCEPT
#
##### Permit useful IMCP packet types for IPv4
##############################################
#
-A INPUT -p icmp -m icmp --icmp-type 0 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 3 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 11 -m conntrack --ctstate NEW -j ACCEPT
#
##### Accept established or related connections
###############################################
#
-A INPUT ! -i lo -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#
##### Accept traffic on these ports
###################################
#
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 25,465,587,993 -j ACCEPT -m comment --comment "Mail"
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 80,443 -j ACCEPT -m comment --comment "Web"
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 432 -j ACCEPT -m comment --comment "SSH"
#
##### Jump to DROPIN Chain and Drop all
#######################################
#
-A INPUT ! -i lo -j DROPIN
#

The rules start with a jump to a custom chain called LOCALINPUT. This is where I keep blocklists, whitelists, and jumps to other custom chains. More of this will be explained later. I then add a rule to log all traffic that has made it this far with additional TCP options, and a log prefix of '[IPTABLES]'. The rules go on to rate limit incoming ping requests and accept all other ICMP traffic as well as all packets that are part of an already established connection.

In the next section, I define the rules to allow traffic to the ports that I want opening. Assuming that this server has a dual function of web server and mail server, I open ports for HTTP and HTTPS, and also the ports for IMAPS, SMTPS, Submission, and SMTP. I have added comments to the end of the rules so that I can easily identify the ports when I list my Iptables rules. I also specified that these rules apply to new connections only. I have already taken care of established connections in a previous rule.

The FORWARD Chain

If you are running a standalone server then this chain can probably be left empty as you won't be forwarding packets on. A possible exception to this would be if you were running docker containers.

Add the following to ip4tables.save:

##################################
##### Forward Chain ##############
##################################
#
##### Log all forward traffic
#############################
#
-A FORWARD -j LOG --log-prefix "[IPTABLES] " --log-tcp-options
#

The OUTPUT Chain

Add the following to ip4tables.save:

##################################
##### Output Chain ###############
##################################
#
##### Jump to LOCALOUTPUT chain
###############################
#
-A OUTPUT ! -o lo -j LOCALOUTPUT
#
##### Allow all outgoing DNS requests
#####################################
#
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 53 -j ACCEPT
-A OUTPUT ! -o lo -p udp -m conntrack --ctstate NEW -m udp --dport 53 -j ACCEPT
#
##### Allow all outgoing local traffic
######################################
#
-A OUTPUT -o lo -j ACCEPT
#
##### Allow outgoing icmp requests
##################################
#
-A OUTPUT ! -o lo -p icmp -j ACCEPT
#
##### Allow all outgoing established and related connections
############################################################
#
-A OUTPUT ! -o lo -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#
##### Allow New outgoing connections on these ports
###################################################
#
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --sports 25,465,587,993 -j ACCEPT -m comment --comment "Mail"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 25 -j ACCEPT -m comment --comment "Mail"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 43 -j ACCEPT -m comment --comment "WhoIs"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 4321 -j ACCEPT -m comment --comment "Remote WhoIs"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --sports 80,443 -j ACCEPT -m comment --comment "Web"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 80,443 -j ACCEPT -m comment --comment "Web"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --sport 432 -j ACCEPT -m comment --comment "SSH"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 11371 -j ACCEPT -m comment --comment "GPG Server"
-A OUTPUT ! -o lo -p udp -m conntrack --ctstate NEW -m udp --sport 68 -j ACCEPT -m comment --comment "DHCP"
-A OUTPUT ! -o lo -p udp -m conntrack --ctstate NEW -m udp --dport 123 -j ACCEPT -m comment --comment "NTP"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 873 -j ACCEPT -m comment --comment "rsync"
#
##### Jump to DropOut Chain
###########################
#
-A OUTPUT ! -o lo -j DROPOUT
#

This follows much the same format as the INPUT chain so I won't repeat myself. The only addition is a section to allow DNS requests to be made. It is sometimes necessary to allow outgoing traffic to a destination port and also from a source port. For example, if your server was acting as a web server then it would need to allow outgoing traffic from the servers port 80 (or 443). However your server would also be retrieving data from other web servers on the internet and so would also need to allow outgoing traffic to a destination servers HTTP port. I have also added rules to allow outgoing traffic to GPG key servers, NTP servers, DHCP servers, and finally to allow WhoIs queries. Some of these I managed to pick up by monitoring dropped outgoing packets in the logs.

The LOCALINPUT Chain

Add the following to your ip4tables.save file:

######################################
##### LOCALINPUT Chain ###############
######################################
#
-A LOCALINPUT ! -i lo -j ALLOWDYNIN
-A LOCALINPUT ! -i lo -j ALLOWIN
-A LOCALINPUT ! -i lo -j BLOCKLISTS
-A LOCALINPUT ! -i lo -j FAIL2BAN
#

The LOCALINPUT chain is basically a list of jumps to other custom chains. The whitelisting comes first... ALLOWDYNIN is a chain that deals with whitelisted DynamicDNS addresses. ALLOWIN deals with whitelisted IP addresses. FAIL2BAN is a chain used by its respective application. And finally BLOCKLISTS is exectly how it sounds - a variety of blocklists freely available on the internet. All will be explained in detail shortly.

The ALLOWDYNIN Chain

Add the following to your ip4tables.save file:

######################################
##### ALLOWDYNIN Chain ###############
######################################
#
-A ALLOWDYNIN -m set --match-set dyndns src -p tcp --dport 88 -j ACCEPT
#

This one needs a little more explaining! --match-set references something called an ipset. And what is an ipset? From LinuxJournal.com:

ipset is an extension to iptables that allows you to create firewall rules that match entire "sets" of addresses at once. Unlike normal iptables chains, which are stored and traversed linearly, IP sets are stored in indexed data structures, making lookups very efficient, even when dealing with large sets.

In this case, we are referencing an ipset that stores the IP address of a dynamic DNS address. This ipset is created via a script and regulary ran using cron. The referenced ipset 'dyndns.address' will need replacing with the name of the ipset you create with the script below. Before we do that though, we'll first make sure that we have ipset installed:

sudo apt update && sudo apt install ipset

Create the script:

sudo nano /usr/local/sbin/dyndns-updater.sh

Enter the following - feel free to change the values of file_path, input_file, and output_file:

#!/bin/bash
file_path='/etc/dyndns-updater'
input_file='dyndns.txt'
output_file='dyndnsaddr.txt'

if [[ ! -e $file_path/$input_file ]]; then
    mkdir -p $file_path
    touch $file_path/$input_file
fi

for i in `cat "$file_path"/"$input_file"`; do 
    nslookup $i resolver1.opendns.com | awk -F': ' 'NR==6 {print $2}' | grep -v -e '^[[:space:]]*$';
done > $file_path/$output_file

/sbin/ipset create -! dyndns hash:ip
/sbin/ipset flush dyndns

while read ip; do
    /sbin/ipset add -! dyndns $ip
done < $file_path/$output_file

exit 0

Make the script executable:

sudo chmod 700 /usr/local/sbin/dyndns-updater.sh

Create the directory:

sudo mkdir -p /etc/dyndns-updater

and create the file containing your whitelisted dyndns addresses:

sudo nano /etc/dyndns-updater/dyndns.txt

Input your addresses - a new line for each one, then save. You can now run the script...

sudo /usr/local/sbin/dyndns-updater.sh

Check that the ipset exists and is populated with your dyndns IP address(es):

sudo ipset list dyndns

Create a cron job to update the ipset on a regular basis. I update mine every 15 mins but feel free to change this to whatever is appropriate.

sudo crontab -e

Enter the following line:

*/15 * * * * /usr/local/sbin/dyndns-updater.sh > /dev/null 2>&1

The ALLOWIN Chain

Add the following to your ip4tables.save file and alter as necessary:

######################################
##### ALLOWIN Chain ##################
######################################
#
-A ALLOWIN -s 1.2.3.4/32 ! -i lo -p tcp -m tcp -j ACCEPT
-A ALLOWIN -s 5.6.7.8/32 ! -i lo -p tcp -m tcp -m multiport --dports 10000,99 -j ACCEPT
#

Here I'm whitelisting certain addresses. In the first rule, I am whitelisting IP Address 1.2.3.4 to have access to the server on all ports. In the next 2 rules, I am setting 2 restricted ports - opening ports 99 and 10000 to Ip Address 5.6.7.8

The BLOCKLISTS Chain

Enter the following to your iptables.save file:

######################################
##### BLOCKLISTS Chain ###############
######################################
#
-A BLOCKLISTS -m set --match-set blacklist src -j DROP
#

Nice and simple! This rule simply uses an ipset called 'blacklist' which is created via a script.

Create the script:

sudo nano /usr/local/sbin/update-blacklist.sh

And enter the following:

#!/usr/bin/env bash
#
# usage update-blacklist.sh <configuration file>
# eg: update-blacklist.sh /etc/ipset-blacklist/ipset-blacklist.conf
# https://github.com/trick77/ipset-blacklist
#
function exists() { command -v "$1" >/dev/null 2>&1 ; }

if [[ -z "$1" ]]; then
  echo "Error: please specify a configuration file, e.g. $0 /etc/ipset-blacklist/ipset-blacklist.conf"
  exit 1
fi

# shellcheck source=ipset-blacklist.conf
if ! source "$1"; then
  echo "Error: can't load configuration file $1"
  exit 1
fi

if ! exists curl && exists egrep && exists grep && exists ipset && exists iptables && exists sed && exists sort && exists wc ; then
  echo >&2 "Error: searching PATH fails to find executables among: curl egrep grep ipset iptables sed sort wc"
  exit 1
fi

DO_OPTIMIZE_CIDR=no
if exists /usr/bin/iprange && [[ ${OPTIMIZE_CIDR:-yes} != no ]]; then
  DO_OPTIMIZE_CIDR=yes
fi

if [[ ! -d $(dirname "$IP_BLACKLIST") || ! -d $(dirname "$IP_BLACKLIST_RESTORE") ]]; then
  echo >&2 "Error: missing directory(s): $(dirname "$IP_BLACKLIST" "$IP_BLACKLIST_RESTORE"|sort -u)"
  exit 1
fi

# create the ipset if needed (or abort if does not exists and FORCE=no)
if ! /sbin/ipset list -n|grep -q "$IPSET_BLACKLIST_NAME"; then
  if [[ ${FORCE:-no} != yes ]]; then
    echo >&2 "Error: ipset does not exist yet, add it using:"
    echo >&2 "# ipset create $IPSET_BLACKLIST_NAME -exist hash:net family inet hashsize ${HASHSIZE:-16384} maxelem ${MAXELEM:-65536}"
    exit 1
  fi
  if ! /sbin/ipset create "$IPSET_BLACKLIST_NAME" -exist hash:net family inet hashsize "${HASHSIZE:-16384}" maxelem "${MAXELEM:-65536}"; then
    echo >&2 "Error: while creating the initial ipset"
    exit 1
  fi
fi

# create the iptables binding if needed (or abort if does not exists and FORCE=no)
if ! /sbin/iptables -nvL BLOCKLISTS|command grep -q "match-set $IPSET_BLACKLIST_NAME"; then
  # we may also have assumed that INPUT rule n°1 is about packets statistics (traffic monitoring)
  if [[ ${FORCE:-no} != yes ]]; then
    echo >&2 "Error: iptables does not have the needed ipset INPUT rule, add it using:"
    echo >&2 "# iptables -I BLOCKLISTS ${IPTABLES_IPSET_RULE_NUMBER:-1} -m set --match-set $IPSET_BLACKLIST_NAME src -j DROP"
    exit 1
  fi
  if ! /sbin/iptables -I BLOCKLISTS "${IPTABLES_IPSET_RULE_NUMBER:-1}" -m set --match-set "$IPSET_BLACKLIST_NAME" src -j DROP; then
    echo >&2 "Error: while adding the --match-set ipset rule to iptables"
    exit 1
  fi
fi

IP_BLACKLIST_TMP=$(mktemp)
for i in "${BLACKLISTS[@]}"
do
  IP_TMP=$(mktemp)
  (( HTTP_RC=$(curl -L -A "blacklist-update/script/github" --connect-timeout 10 --max-time 10 -o "$IP_TMP" -s -w "%{http_code}" "$i") ))
  if (( HTTP_RC == 200 || HTTP_RC == 302 || HTTP_RC == 0 )); then # "0" because file:/// returns 000
    command grep -Po '^(?:\d{1,3}.){3}\d{1,3}(?:/\d{1,2})?' "$IP_TMP" | sed -r 's/^0*([0-9]+)\.0*([0-9]+)\.0*([0-9]+)\.0*([0-9]+)$/\1.\2.\3.\4/' >> "$IP_BLACKLIST_TMP"
    [[ ${VERBOSE:-yes} == yes ]] && echo -n "."
  elif (( HTTP_RC == 503 )); then
    echo -e "\\nUnavailable (${HTTP_RC}): $i"
  else
    echo >&2 -e "\\nWarning: curl returned HTTP response code $HTTP_RC for URL $i"
  fi
  rm -f "$IP_TMP"
done

# sort -nu does not work as expected
sed -r -e '/^(0\.0\.0\.0|10\.|127\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|192\.168\.|22[4-9]\.|23[0-9]\.)/d' "$IP_BLACKLIST_TMP"|sort -n|sort -mu >| "$IP_BLACKLIST"
if [[ ${DO_OPTIMIZE_CIDR} == yes ]]; then
  if [[ ${VERBOSE:-no} == yes ]]; then
    echo -e "\\nAddresses before CIDR optimization: $(wc -l "$IP_BLACKLIST" | cut -d' ' -f1)"
  fi
  < "$IP_BLACKLIST" /usr/bin/iprange --optimize - > "$IP_BLACKLIST_TMP" 2>/dev/null
  if [[ ${VERBOSE:-no} == yes ]]; then
    echo "Addresses after CIDR optimization:  $(wc -l "$IP_BLACKLIST_TMP" | cut -d' ' -f1)"
  fi
  cp "$IP_BLACKLIST_TMP" "$IP_BLACKLIST"
fi

rm -f "$IP_BLACKLIST_TMP"

# family = inet for IPv4 only
cat >| "$IP_BLACKLIST_RESTORE" <<EOF
create $IPSET_TMP_BLACKLIST_NAME -exist hash:net family inet hashsize ${HASHSIZE:-16384} maxelem ${MAXELEM:-65536}
create $IPSET_BLACKLIST_NAME -exist hash:net family inet hashsize ${HASHSIZE:-16384} maxelem ${MAXELEM:-65536}
EOF

# can be IPv4 including netmask notation
# IPv6 ? -e "s/^([0-9a-f:./]+).*/add $IPSET_TMP_BLACKLIST_NAME \1/p" \ IPv6
sed -rn -e '/^#|^$/d' \
  -e "s/^([0-9./]+).*/add $IPSET_TMP_BLACKLIST_NAME \\1/p" "$IP_BLACKLIST" >> "$IP_BLACKLIST_RESTORE"

cat >> "$IP_BLACKLIST_RESTORE" <<EOF
swap $IPSET_BLACKLIST_NAME $IPSET_TMP_BLACKLIST_NAME
destroy $IPSET_TMP_BLACKLIST_NAME
EOF

/sbin/ipset -file  "$IP_BLACKLIST_RESTORE" restore

if [[ ${VERBOSE:-no} == yes ]]; then
  echo
  echo "Blacklisted addresses found: $(wc -l "$IP_BLACKLIST" | cut -d' ' -f1)"
fi

The script is originally from [https://github.com/trick77/ipset-blacklist] but I have listed it again here as I made a couple of minor amendments.

Make the script executable:

sudo chmod 700 /usr/local/sbin/update-blacklist.sh

And create the initial ipset:

sudo ipset create blacklist -exist hash:net family inet hashsize 16384 maxelem 131072

Now we'll create the config file:

sudo mkdir /etc/ipset-blacklist && sudo nano /etc/ipset-blacklist/ipset-blacklist.conf

Enter the following:

IPSET_BLACKLIST_NAME=blacklist # change it if it collides with a pre-existing ipset list
IPSET_TMP_BLACKLIST_NAME=${IPSET_BLACKLIST_NAME}-tmp

# ensure the directory for IP_BLACKLIST/IP_BLACKLIST_RESTORE exists (it won't be created automatically)
IP_BLACKLIST_RESTORE=/etc/ipset-blacklist/ip-blacklist.restore
IP_BLACKLIST=/etc/ipset-blacklist/ip-blacklist.list

VERBOSE=yes # probably set to "no" for cron jobs, default to yes
FORCE=no # will create the ipset-iptable binding if it does not already exist
let IPTABLES_IPSET_RULE_NUMBER=1 # if FORCE is yes, the number at which place insert the ipset-match rule (default to 1)

# Sample (!) list of URLs for IP blacklists. Currently, only IPv4 is supported in this script, everything else will be filtered.
BLACKLISTS=(
    "file:///etc/ipset-blacklist/ip-blacklist-custom.list" # optional, for your personal nemeses (no typo, plural)
    "http://danger.rulez.sk/projects/bruteforceblocker/blist.php" # BruteForceBlocker IP List
    #"https://www.spamhaus.org/drop/drop.lasso" # Spamhaus Don't Route Or Peer List (DROP)
    #"https://cinsscore.com/list/ci-badguys.txt" # C.I. Army Malicious IP List
    #"https://blocklist.greensnow.co/greensnow.txt" # GreenSnow
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset" # Firehol Level 1
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset" # Firehol Level 2
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level3.netset" # Firehol Level 3
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_abusers_1d.netset" # Firehol Abusers 1d
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_cryptowall_ps.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_feed.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_locky_c2.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_locky_ps.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_online.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_rw.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_teslacrypt_ps.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_torrentlocker_c2.ipset"
    "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/ransomware_torrentlocker_ps.ipset"
    # "http://ipverse.net/ipblocks/data/countries/xx.zone" # Ban an entire country, see http://ipverse.net/ipblocks/data/countries/
)
MAXELEM=131072

I have preconfigured this with a decent set of blocklists, mainly from the firehol github repository.

Do not run the script yet - the script checks for the iptables rule which we have not applied yet.

The script also makes use of iprange which can be installed with:

sudo apt update && sudo apt install iprange

The BLOCKLISTSOUT Chain

Add the following to your ip4tables.save file:

######################################
##### BLOCKLISTSOUT Chain ############
######################################
#
-A BLOCKLISTS -m set --match-set blacklist dst -j DROP
#

This ensures that no traffic from your server is destined for one of the addresses in your blocklist.

The DROPIN Chain

Add the following to your ip4tables.save file:

######################################
##### DROPIN Chain ###################
######################################
#
-A DROPIN -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-IN] *TCP Blocked* " --log-uid
-A DROPIN -p udp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-IN] *UDP Blocked* " --log-uid
-A DROPIN -p icmp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-IN] *ICMP Blocked* " --log-uid
-A DROPIN -j DROP
#

This chain logs and drops packets using custom log prefixes

The LOCALOUTPUT Chain

Add the following to your ip4tables.save file:

######################################
##### LOCALOUTPUT Chain ##############
######################################
#
-A LOCALOUTPUT ! -o lo -j ALLOWDYNOUT
-A LOCALOUTPUT ! -o lo -j ALLOWOUT
-A LOCALOUTPUT ! -o lo -j BLOCKLISTSOUT
#

This chain contains jumps to other custom chains and is the OUTPUT version of the previously inputted LOCALINPUT chain.

The ALLOWDYNOUT Chain

Add the following to your ip4tables.save file:

######################################
##### ALLOWDYNOUT Chain ##############
######################################
#
-A ALLOWDYNOUT -m set --match-set dyndns src -p tcp --sport 99 -j ACCEPT
#

Here we are using the dyndns ipset again, this time to allow outgoing traffic from port 99 to our dynamic DNS IP addresses.

The ALLOWOUT Chain

Add the following to your ip4tables.save file:

######################################
##### ALLOWOUT Chain #################
######################################
#
-A ALLOWOUT -d 1.2.3.4/32 ! -i lo -p tcp -m tcp -j ACCEPT
-A ALLOWOUT -d 5.6.7.8/32 ! -o lo -p tcp -m tcp -m multiport --sports 10000,99 -j ACCEPT
#

This chain is the opposite of the previously inputted ALLOWIN chain. In this example, we're allowing all outgoing traffic to IP address 1.2.3.4, and we are restricting outgoing traffic from ports 99 and 10000 to the specified IP address of 5.6.7.8

The DROPOUT Chain

Add the following to your ip4tables.save file:

######################################
##### DROPOUT Chain ##################
######################################
#
-A DROPOUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-OUT] *TCP Blocked* " --log-uid
-A DROPOUT -p udp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-OUT] *UDP Blocked* " --log-uid
-A DROPOUT -p icmp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-OUT] *ICMP Blocked* " --log-uid
-A DROPOUT -j REJECT
#
COMMIT

This chain logs and drops outgoing packets, and adds a custom log prefix.

I have also added the COMMIT command as this will be the last entry in the file. Save the file once complete.

IPv6 Rules

The layout of this is virtually the same as our IP4 rule set and so I won't repeat myself by explaining each individual chain. Here is a basic template based on our IP4 template, and assuming that the server is set up to be a mail server and a web server.

Create an ip6tables.save file:

sudo nano ip6tables.save

Enter the following, making any amendments as necessary.

#
##################################
##### Filter Table ###############
##################################
#
*filter
#
##################################
##### Create Chains ##############
##################################
#
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
:FAIL2BAN - [0:0]
:LOCALINPUT - [0:0]
:LOCALOUTPUT - [0:0]
:BLOCKLISTS - [0:0]
:BLOCKLISTSOUT - [0:0]
:DROPIN - [0:0]
:DROPOUT - [0:0]
#
##################################
##### Rules Start Here ###########
##################################
#
##################################
##### INPUT Chain ################
##################################
#
##### Jump to LOCALINPUT chain
##############################
#
-A INPUT ! -i lo -j LOCALINPUT
#
##### Accept all local traffic
##############################
#
-A INPUT -i lo -j ACCEPT
#
##### Limit connections per source IP
#####################################
#
-A INPUT -p tcp -m connlimit --connlimit-above 111 -j REJECT --reject-with tcp-reset
#
##### Good practise is to explicately reject AUTH traffic so that it fails fast
###############################################################################
#
-A INPUT ! -i lo -p tcp --dport 113 --syn -m conntrack --ctstate NEW -j REJECT --reject-with tcp-reset
#
##### Accept and rate-limit pings
#################################
#
-A INPUT ! -i lo -p ipv6-icmp -m conntrack --ctstate NEW -m limit --limit 1/sec --limit-burst 1 -j ACCEPT
#
##### Permit useful IMCP packet types for IPv6
##############################################
#
-A INPUT -p ipv6-icmp --icmpv6-type 1   -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 2   -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 3   -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 4   -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 133 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 134 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 135 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 136 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 137 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 141 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 142 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 148 -j ACCEPT
-A INPUT -p ipv6-icmp --icmpv6-type 149 -j ACCEPT
#
##### Accept established or related connections
###############################################
#
-A INPUT ! -i lo -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#
##### Accept traffic on these ports
###################################
#
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 25,465,587,993 -j ACCEPT -m comment --comment "Mail"
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 80,443 -j ACCEPT -m comment --comment "Web"
-A INPUT ! -i lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 432 -j ACCEPT -m comment --comment "SSH"
#
##### Log all traffic that hasn't been dropped yet
##################################################
#
-A INPUT -j LOG --log-prefix "[IPTABLES] " --log-tcp-options
#
##### Jump to DROPIN Chain and Drop all
#######################################
#
-A INPUT ! -i lo -j DROPIN
#
##################################
##### Forward Chain ##############
##################################
#
##### Log all forward traffic
#############################
#
-A FORWARD -j LOG --log-prefix "[IPTABLES] " --log-tcp-options
#
##################################
##### Output Chain ###############
##################################
#
##### Allow outgoing DNS requests
########################################################
#
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 53 -j ACCEPT
-A OUTPUT ! -o lo -p udp -m conntrack --ctstate NEW -m udp --dport 53 -j ACCEPT
#
##### Allow all outgoing local traffic
######################################
#
-A OUTPUT -o lo -j ACCEPT
#
##### Allow outgoing icmp requests
##################################
#
-A OUTPUT ! -o lo -p ipv6-icmp -j ACCEPT
#
##### Allow all outgoing established and related connections
############################################################
#
-A OUTPUT ! -o lo -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
#
##### Allow New outgoing connections on these ports
###################################################
#
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --sports 25,465,587,993 -j ACCEPT -m comment --comment "Mail"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 25 -j ACCEPT -m comment --comment "Mail"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 43 -j ACCEPT -m comment --comment "WhoIs"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 4321 -j ACCEPT -m comment --comment "Remote WhoIs"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --sports 80,443 -j ACCEPT -m comment --comment "Web"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp -m multiport --dports 80,443 -j ACCEPT -m comment --comment "Web"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --sport 432 -j ACCEPT -m comment --comment "SSH"
-A OUTPUT ! -o lo -p tcp -m conntrack --ctstate NEW -m tcp --dport 11371 -j ACCEPT -m comment --comment "GPG Server"
-A OUTPUT ! -o lo -p udp -m conntrack --ctstate NEW -m udp --sport 68 -j ACCEPT -m comment --comment "DHCP"
-A OUTPUT ! -o lo -p udp -m conntrack --ctstate NEW -m udp --dport 123 -j ACCEPT -m comment --comment "NTP"
#
##### Jump to DropOut Chain
###########################
#
-A OUTPUT ! -o lo -j DROPOUT
#
######################################
##### BLOCKLISTS Chain ###############
######################################
#
-A BLOCKLISTS -m set --match-set fullbogons-ipv6 src -j DROP
-A BLOCKLISTS -m set --match-set spamhaus-dropv6 src -j DROP
#
######################################
##### BLOCKLISTSOUT Chain ############
######################################
#
-A BLOCKLISTS -m set --match-set fullbogons-ipv6 dst -j DROP
-A BLOCKLISTS -m set --match-set spamhaus-dropv6 dst -j DROP
#
######################################
##### FAIL2BAN Chain #################
######################################
#
#
######################################
##### LOCALINPUT Chain ###############
######################################
#
-A LOCALINPUT ! -i lo -j BLOCKLISTS
-A LOCALINPUT ! -i lo -j FAIL2BAN
#
######################################
##### LOCALOUTPUT Chain ##############
######################################
#
#
######################################
##### DROPIN Chain ###################
######################################
#
-A DROPIN -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-IN] *IPv6 TCP Blocked* " --log-uid
-A DROPIN -p udp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-IN] *IPv6 UDP Blocked* " --log-uid
-A DROPIN -p icmp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-IN] *IPv6 ICMP Blocked* " --log-uid
-A DROPIN -j DROP
#
######################################
##### DROPOUT Chain ##################
######################################
#
-A DROPOUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-OUT] *IPv6 TCP Blocked* " --log-uid
-A DROPOUT -p udp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-OUT] *IPv6 UDP Blocked* " --log-uid
-A DROPOUT -p icmp -m limit --limit 30/min -j LOG --log-prefix "[IPTABLES-OUT] *IPv6 ICMP Blocked* " --log-uid
-A DROPOUT -j REJECT
#
COMMIT
#
##################################
##### Mangle Table ###############
##################################
#
*mangle
:PREROUTING ACCEPT [1545:418841]
:INPUT ACCEPT [1545:418841]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [1795:189462]
:POSTROUTING ACCEPT [1773:187702]
#
##################################
##### DOS Prevention #############
##################################
#
### 1: Drop invalid packets ### 
-A PREROUTING -m conntrack --ctstate INVALID -j DROP  
#
### 2: Drop TCP packets that are new and are not SYN ### 
-A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP 
#
### 3: Drop SYN packets with suspicious MSS value ### 
-A PREROUTING -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP  
#
### 4: Block packets with bogus TCP flags ### 
-A PREROUTING -p tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -j DROP 
-A PREROUTING -p tcp --tcp-flags FIN,SYN FIN,SYN -j DROP 
-A PREROUTING -p tcp --tcp-flags SYN,RST SYN,RST -j DROP 
-A PREROUTING -p tcp --tcp-flags FIN,RST FIN,RST -j DROP 
-A PREROUTING -p tcp --tcp-flags FIN,ACK FIN -j DROP 
-A PREROUTING -p tcp --tcp-flags ACK,URG URG -j DROP 
-A PREROUTING -p tcp --tcp-flags ACK,FIN FIN -j DROP 
-A PREROUTING -p tcp --tcp-flags ACK,PSH PSH -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL ALL -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL NONE -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL SYN,FIN,PSH,URG -j DROP 
-A PREROUTING -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP  
#
### 5: Block spoofed packets ### 
-A PREROUTING -s ::1/128 -j DROP 
-A PREROUTING -s FC00::/7 -j DROP 
#
COMMIT
#
##################################
##### Raw Table ##################
##################################
#
*raw
:PREROUTING ACCEPT [1545:418841]
:OUTPUT ACCEPT [1795:189462]
COMMIT
#
##################################
##### NAT Table ##################
##################################
#
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [1300:144470]
:POSTROUTING ACCEPT [1278:142710]
COMMIT

So here we have just a few changes.

I have left out the ALLOWIN, ALLOWDYNIN, ALLOWOUT, and ALLOWDYNOUT chains because I don't use any static IPv6 addresses to access my server.

I have removed the 6th ruleset from the Mangle PREROUTING chain as ip6tables doesn't support it. I have also altered the 5th ruleset so that it uses IPv6 local addresses.

I have added extra ICMP rules which are useful specifically for the IPv6 protocol

I have added 2 new IPv6 blocklists. At this moment, there aren't very many IPv6 blocklists freely available on the web so we're going to set up a script specifically for these two. We cannot use the previous script that we created as it only supports IP4 due to its usage of iprange.

Create the script:

sudo nano /usr/local/sbin/update-ip6blocklists.sh

and enter the following:

#!/bin/sh
URL="https://www.team-cymru.org/Services/Bogons/fullbogons-ipv6.txt"
FILEPATH="/etc/ip6blocklists"
FILE="fullbogons-ipv6.txt"
URL2="https://www.spamhaus.org/drop/dropv6.txt"
FILE2="spamhaus-dropv6-wip.txt"
FILE3="spamhaus-dropv6.txt"
CREATEARGS="family inet6 maxelem 2000000 hashsize 2048"
set -e

mkdir -p "${FILEPATH}"

curl -o "${FILEPATH}/${FILE}" "${URL}"
curl -o "${FILEPATH}/${FILE2}" "${URL2}"

grep -i SBL ${FILEPATH}/${FILE2} | cut -f 1 -d ';' | awk '{$1=$1;print}' > ${FILEPATH}/${FILE3}
rm "${FILEPATH}/${FILE2}"

for DROPLIST in ${FILEPATH}/*.txt; do
    [ -e "$DROPLIST" ] || continue
    /sbin/ipset create $(basename -- "${DROPLIST%.*}") hash:net ${CREATEARGS} -exist
    /sbin/ipset create $(basename -- "${DROPLIST%.*}-tmp") hash:net ${CREATEARGS} -exist
    /sbin/ipset flush $(basename -- "${DROPLIST%.*}-tmp")
    grep -v "^#" "${DROPLIST}" | while read NET; do
        echo "add $(basename -- "${DROPLIST%.*}-tmp") ${NET}"
    done | /sbin/ipset restore
    /sbin/ipset swap $(basename -- "${DROPLIST%.*}-tmp") $(basename -- "${DROPLIST%.*}")
    /sbin/ipset destroy $(basename -- "${DROPLIST%.*}-tmp")
done

exit 0

Make the script executable:

sudo chmod 700 /usr/local/sbin/update-ip6blocklists.sh

Run the script:

sudo /usr/local/sbin/update-ip6blocklists.sh

Applying the Rules and Testing

Before continuing, check that the two necessary ipsets have been created:

sudo ipset list -t

It should list the two previously created ipsets of blacklist and dyndns.

Also make sure that UFW is not running - if it is, it can be disabled with:

sudo systemctl stop ufw && sudo systemctl disable ufw

If a mistake has been made in the IP Tables rule set, it would be quite possible to lock yourself out of your own server after applying the rules. For this reason we test them first using iptables-apply and ip6tables-apply

First we'll apply the iptables rules:

sudo iptables-apply -t 60 ip4tables.save

This temporarily applies the rules for 60 seconds. Unless the rules are confirmed within the 60 seconds, iptables reverts back to its previous configuration. During the countdown, you should try and create a new ssh connection to your server. If it succeeds then you know that your new configuration is not going to lock you out. Once you've done this you should be able to safely confirm the new settings.

Now follow exactly the same procedure with:

sudo ip6tables-apply -t 60 ip6tables.save

Confirm that the rules have applied with sudo iptables -L and sudo ip6tables -L

Updating the Blocklists

You should now be able to successfully run the IP4 blocklist script:

sudo /usr/local/sbin/update-blacklist.sh /etc/ipset-blacklist/ipset-blacklist.conf

Once run, you should be able to confirm that the ipset has been populated with:

sudo ipset list -t

This command shows the basic information for each ipset. At this point, they should all be populated.

Now create a cron job to regularly update all the blocklists:

sudo crontab -e

Enter the following:

13 3,13 * * * /usr/local/sbin/update-blacklist.sh /etc/ipset-blacklist/ipset-blacklist.conf > /dev/null 2>&1
5 */6 * * * /usr/local/sbin/update-ip6blocklists.sh > /dev/null 2>&1

and save.

Persistent IPTables Rules

In order for the IPTables rules to persist across reboots, we install the iptables-persistent and netfilter-persistent packages using apt.

sudo apt install netfilter-persistent iptables-persistent

Select yes to save current rules (if you're happy with them). The saved rules can be updated at any time by running the following commands:

sudo iptables-save > /etc/iptables/rules.v4
sudo ip6tables-save > /etc/iptables/rules.v6

Enable the service:

sudo systemctl enable netfilter-persistent

Persistent Ipsets

If your iptables rules reference any ipsets then they won't be able to load after a reboot as, by default, ipsets are destroyed on shutdown. In order to get this working correctly, we need to make the ipsets persist after a reboot.

Save the ipsets:

sudo ipset save > /etc/ipset.conf

Create a service file:

sudo nano /etc/systemd/system/ipset-persistent.service

Enter the following:

[Unit]
Description=ipset persistent configuration
#
DefaultDependencies=no
Before=network.target

# ipset sets should be loaded before iptables
# Because creating iptables rules with names of non-existent sets is not possible
Before=netfilter-persistent.service

ConditionFileNotEmpty=/etc/ipset.conf

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ipset restore -file /etc/ipset.conf
# Uncomment to save changed sets on reboot
# ExecStop=/sbin/ipset save -file /etc/ipset.conf
ExecStop=/sbin/ipset flush
ExecStopPost=/sbin/ipset destroy

[Install]
WantedBy=multi-user.target

RequiredBy=netfilter-persistent.service

Reload and enable the service:

sudo systemctl daemon-reload
sudo systemctl enable ipset-persistent.service

This ensures that the ipsets are created before the iptables rules are loaded. If the ipsets are not created when the rules are loaded then they will fail.

Create separate log for IPTables

I prefer to separate my IPtables log messages so that they're in their own log file rather than syslog.

Create a configuration file for the new incoming traffic log:

sudo nano /etc/rsyslog.d/10-iptables.conf

and enter the following:

# log kernel generated IPTABLES log messages to file
# each log line will be prefixed by "[IPTABLES]", so search for that
:msg,contains,"[IPTABLES]" /var/log/iptables.log

# the following stops logging anything that matches the last rule.
# doing this will stop logging kernel generated IPTABLES log messages to the file
# normally containing kern.* messages (eg, /var/log/kern.log)
# older versions of ubuntu may require you to change stop to ~
& stop

Now follow a similar process to create seperate log files for dropped packets, both incoming and outgoing:

sudo nano /etc/rsyslog.d/10-iptables-dropin.conf
# log kernel generated IPTABLES log messages to file
# each log line will be prefixed by "[IPTABLES]", so search for that
:msg,contains,"[IPTABLES-IN]" /var/log/iptables-dropin.log

# the following stops logging anything that matches the last rule.
# doing this will stop logging kernel generated IPTABLES log messages to the file
# normally containing kern.* messages (eg, /var/log/kern.log)
# older versions of ubuntu may require you to change stop to ~
& stop
sudo nano /etc/rsyslog.d/10-iptables-dropout.conf
# log kernel generated IPTABLES log messages to file
# each log line will be prefixed by "[IPTABLES]", so search for that
:msg,contains,"[IPTABLES-OUT]" /var/log/iptables-dropout.log

# the following stops logging anything that matches the last rule.
# doing this will stop logging kernel generated IPTABLES log messages to the file
# normally containing kern.* messages (eg, /var/log/kern.log)
# older versions of ubuntu may require you to change stop to ~
& stop

Then, restart rsyslog to activate the changes:

sudo service rsyslog restart

Edit the logrotate configuration file for syslog:

sudo nano /etc/logrotate.d/rsyslog

Add a line for the iptables.log as follows:

...
/var/log/cron.log
/var/log/debug
/var/log/messages
/var/log/iptables.log
/var/log/iptables-dropin.log
/var/log/iptables-dropout.log
{
        rotate 4
        weekly
        missingok
        notifempty
        compress
        delaycompress
        sharedscripts
        postrotate
                /usr/lib/rsyslog/rsyslog-rotate
        endscript
}