Securing your linux system using iptables can be a daunting task. There are some utilities that can help, but when it comes to security a deep understanding is often very useful. Fundamentally, iptables are lists of rules, executed in order, to determine if a packet should be accepted, dropped, or forwarded along. It has some very powerful features which can help you defend your services, log potential attacks, and forward traffic between computers as well as between ports on the same computer. I will cover some of the basics below.

Listing the current rules

Listing and creating iptables rules requires root privileges. I will be prepending these commands with the sudo command so that the commands are executed as root. I recommend using the following command to list the current iptables settings on your system:

sudo iptables -L -v --line-numbers

You will get a response that looks something like the following:

Chain INPUT (policy DROP 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination         
1        0     0 ACCEPT     all  --  lo     any     anywhere             anywhere            
2        0     0 ACCEPT     all  --  any    any     anywhere             anywhere             state RELATED,ESTABLISHED
3     2104  116K ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:80 state NEW

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 308K packets, 163M bytes)
num   pkts bytes target     prot opt in     out     source               destination

There may or may not be existing firewall rules. There should be at least three "Chains" which are lists of rules that are evaluated for different conditions. The one we will focus on the most in this write up is the INPUT chain which is the basic rule set for internet packets sent to our system. The other chain we will spend a small bit of time looking at is the FORWARD chain which is the chain for packets which should be forwarded along to other computers.

I will not spend time talking about the OUTPUT chain, which contains rules for packets originating from the host computer. For a general purpose computer I tend to keep OUTPUT wide open, but there are many cases where you may want to restrict the traffic a computer can send.

Starting with a clean slate

Before wiping out your iptables rules, you may want to check if any other programs are running which have inserted rules of their own. VPN clients and servers can modify iptables as does docker. It is strongly recommended that you stop these programs as well as any other internet services you don't need.

When you are ready to proceed execute the following command which will open up your computer to all incoming connections:

sudo iptables -P INPUT ACCEPT     # By default accept all incoming packets
sudo iptables -P OUTPUT ACCEPT    # By default accept all outgoing packets
sudo iptables -P FORWARD ACCEPT   # By default accept all forwarded packets
sudo iptables -F                  # Flush out all existing rules
sudo iptables -X                  # Delete all existing chains

You are now ready to construct your new rules from a clean slate. I will be adding rules in the order I typically add them. Remember that order is important as these rules are evaluated in order.

Managing localhost connections

I generally do not restrict internet connections from one port to another port on the same computer. These will tend to use localhost as the destination address or 127.0.0.1. These are all sent over the lo internet interface, so we can add a rule to allow these connections using the command below.

sudo iptables -A INPUT -i lo -j ACCEPT

Let's dissect this command. The -A INPUT option tells iptables to append this rule to the INPUT chain. The -i lo option tells iptables to apply this rule to packets on the lo interface, and -j ACCEPT tells iptables to jump to the ACCEPT chain. The ACCEPT chain is not a chain of rules, like the input chain, but a virtual chain which tells the system to accept the packet. Another virtual chain we will be using is the DROP chain, which is used to tell the system which packets should be dropped. There are other virtual chains which we will not cover in this guide.

Managing existing connections

Once a connection has been established, I also generally want packets related to that connection to continue to be accepted by the system. This can be accomplished using the command below.

sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

Some of parts of this command should be familiar, however we add two new argument groups. The -m state arguments tell iptables to look at the state of the packet and match packets based on that state. The arguments --state ESTABLISHED,RELATED expands on this saying that this rule applies to packets whose state is ESTABLISHED or RELATED. This will filter out any packets which are related to an established connection. Finally the -j ACCEPT arguments tell iptables to accept these packets.

Allowing ICMP messages

ICMP protocol messages can be very useful. I like to be able to send a quick ping to my hosts to see if they are still on the network. Although not required, I generally accept ICMP packets by issuing the command below.

sudo iptables -A INPUT -p icmp -j ACCEPT

Opening a port to the world

There are several ports, for example ports 80 (HTTP) and 443 (HTTPS), which you may want to open up to anyone on the internet. Doing this is simple.

sudo iptables -A INPUT -p tcp --dport $port_number -m state --state NEW -j ACCEPT

This filters packets which meet the following criteria:

  • From -p tcp: Filter packets that use TCP protocol
  • From --dport $port_number: Filter packets that are going to port $port_number (which should be replaced with an integer).
  • From -m state --state NEW: Filter packets that are part of a NEW connection attempt.

If all the criteria are matched these packets are accepted in this example.

To do this for port 80 (HTTP) I would replace $port_number with 80, as I do below.

sudo iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT

Whitelisting connections

Another common pattern is to restrict access to a particular port to one or more authorize hosts. This is especially common for database servers. Let's say we would like to restrict connections to our MySQL server. We could do that by setting up a new chain for authorized hosts and route all TCP connections to the MySQL server port (3306) to that chain.

First we need to set up a new chain, which I will call MYSQL_WHITELIST.

sudo iptables -N MYSQL_WHITELIST

Then we tell iptables to send all new connection requests for port 3306 to that chain.

sudo iptables -A INPUT -p tcp --dport 3306 -m state --state NEW -j MYSQL_WHITELIST

By default we can also add a rule saying new connections to that port should be dropped.

sudo iptables -A INPUT -p tcp --dport 3306 -m state --state NEW -j DROP

Then we can add the ip addresses or ip address ranges which are allowed to access the MySQL server to the MYSQL_WHITELIST chain. To add an individual ip address, such as 192.168.3.25, we write a command like the one below.

sudo iptables -A MYSQL_WHITELIST -s 192.158.3.25 -j ACCEPT

To add a range, such as 10.3.*.* we can use the notation:

sudo iptables -A MYSQL_WHITELIST -s 10.3.0.0/16 -j ACCEPT

Rate limiting connection requests

These days any server on the internet will be subject to automated scanning for potential exploits. In some cases a system will try thousands and thousands of default passwords for a service, and some of these scanning tools will run these scans at very rapid rates. At best these are annoying, at worst they can cause a denial of service or even find a vulnerability in the system. One way to mitigate the potential denial of service is to limit the rate at which new connections can be established to a particular service.

One service prone to such exploitation is SSH (port 22). If you are connected to a remote server over SSH please keep in mind that adding rules to the firewall can affect your connectivity to the remote service. The rules I will detail below should not disrupt your existing connection, but be sure that you can ssh back to your server before you quit the existing shell.

Rate limiting connections to a port is a little more complex than the rules we have explored so far. In the example below I will use port 22, though these rules can apply to any TCP port.

First, we set up a recent packet filter and label all new connection packets coming into port 22 as "ssh" packets using the command below.

sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --name ssh --set

Next we apply our filters. I like to log when there are more than 4 connection attempts in 60 seconds. I can do this using the command below.

sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --name ssh --rcheck --seconds 60 --hitcount 4 --rttl -j LOG --log-prefix "SSH_brute_force "

I also want to drop packets that match this filter, so I use the command below.

sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --name ssh --rcheck --seconds 60 --hitcount 4 --rttl -j DROP

Make sure you add a rule accepted connections on that port which do not meet the excessive traffic criterion.

sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT

This will keep any new connection attempts down to 4 every 60 seconds from any particular host. Both hitcount and seconds can be adjusted to match the needs of your service. This can also be combined with the whitelisting rules above to allow certain hosts to exceed these limits.

Lock down anything else

There are two ways to reject any other packets not explicitly mentioned in the rules you have added. The one I find preferable is to set a default DROP policy using the command below.

sudo iptables -P INPUT DROP

The other possibility is to append a rule that all packets not otherwise accepted previously be dropped.

sudo iptables -A INPUT -j DROP

Some people add both to make sure their bases are covered, however if you use the latter rule, you need to remember that any additional rules you append will not be executed since all the rules are executed in order. You will need to insert new rules using the iptables -I command.

Saving the rules

The rules can be saved, in Ubuntu Linux, using the iptables-save command, which will write rules in an internal text format to stdout. You can restore these rules using the iptables-restore command. Keep in mind that iptables rules you set manually will not be restored on a reboot, so you will want to find a mechanism for restoring the rules when you reboot.

I personally add a script to the /etc/network/if-pre-up.d directory as follows.

#!/bin/sh
iptables-restore < /etc/iptables.rules
exit 0

I then store the rules in /etc/iptables.rules.

What about IPv6?

Unfortunately you have to go through the whole exercise again for IPv6 using the ip6tables, ip6tables-save, and ip6tables-restore commands. Fortunately the structure of the commands is exactly the same. There are currently many internet sites which have set up firewalls for their IPv4 interfaces, but have not done so for their IPv6 interfaces. It behooves you to do both, especially as IPv6 becomes more common.