My understanding of iptables is rudimentary and I thought it’s time to improve on it. I have an understanding of firewalls, nat and packet filtering, but putting this into iptables always seems hard work.

There are lots of online resources, but nothing seems to be comprehensive enough to cover everything I wanted and writing these posts also acts as a means of driving the material into my own brain. So I thought I’d document it myself in the way that I would typically use it.

A good place to start is list the rules you currently have.

# iptables --list
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination 

This shows that everything is permitted. Nothing has been configured on the firewall.

Incoming packets go through the INPUT chain and the default policy action is to ACCEPT them. Similarly for Outgoing packets go through OUTPUT and packets not destined for us go through FORWARD and all are ACCEPTED.

Because there are probably a lot of other tables and chains, to reset everything to a factory fresh, clean slate, you have to issue several commands:

# 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 FORWARD ACCEPT
# iptables -P OUTPUT ACCEPT

This flushes -F the rules from the chains and then deletes the chains -X, then sets the default chains to ACCEPT all traffic.

Incoming Rules

First steps are usually to allow remote access using ssh. We can APPEND a rule to our INPUT chain that ACCEPTs ANY TCP protocol traffic destined for our port 22 using:

# iptables -A INPUT -j ACCEPT -p tcp --dport 22

It’s probably best to get this in the table as early as you can, because if you then fudge the rest of the rules you can still at least use ssh to get access to the system.

# iptables --list                                                       
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Right now we have our ssh rule added, but we aren’t stopping anything else anyway. This is because the whole INPUT chain is set to ACCEPT packets. We can now change this behaviour to DROP packets using:

# iptables -P INPUT DROP

Pay note to the order we did this in. Create the ssh rule to accept packets and then set the input policy to drop everything. If we did this the other way around we’d have prevented ourselves from accessing the system by ssh as soon as we set the input policy to drop.

Let’s add another rule for a make believe web server. Our web server listens on both http and https using port 80 and 443. We want to accept any traffic to either of these ports. We could setup two rules, but there is a handy multiport option we can use to open both.

# iptables -A INPUT -j ACCEPT -p tcp --match multiport --dports 80,433
# iptables --list                                                       
Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere             multiport dports http,https

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination 

One rule, two ports. If you have an application that uses a range of ports we can use a colon : and specify the range as FROM:TO, eg for ports from 8080 to 8089:

# iptables -A INPUT -j ACCEPT --match multiports --dports 8080:8089

Saving Your Changes

To make your changes persistent between boots you need to write the rules into a file for both IPv4 and IPv6.

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

Or by using sudo:

$ sudo bash -c "iptables-save > /etc/iptables/rules.v4"
$ sudo bash -c "ip6tables-save > /etc/iptables/rules.v6"

More details: https://wiki.debian.org/iptables

Outgoing Rules

There’s no difference in the process for handling OUTPUT rules. But if you’re working on a server or workstation it’s not usually necessary to do anything with outgoing rules unless you’re in a paranoid environment that requires this level of security.

Inserting, Appending and Deleting Rules

You’ll have seen we used append above using the -A switch. They are self explanatory, -I = insert, -A = append and -D = delete.

On it’s own -I will insert a rule at the beginning of the specified chain, -A will add to the end.

Line Numbers

To delete a rule you have to be as specific as you were when you created it using the exact creation syntax to remove it. For this reason I find it easier to display line numbers to deal with editing lines more easily.

# iptables --list --line-numbers                                        
Chain INPUT (policy DROP)
num  target     prot opt source               destination         
1    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh
2    ACCEPT     tcp  --  anywhere             anywhere             multiport dports http,https

Now I can delete a specific line without typing everything in and I can be sure I nail the right one. Eg. If I wanted to delete the web server rule we added:

# iptables -D INPUT 2

Insert a rule before rule 2 using:

# iptables -I INPUT 2 -j ACCEPT -p tcp --dport 53

Log and Drop

Whilst the default policy is drop this isn’t helpful for auditing or debugging. We sometimes want to log and then drop the packet.

# iptables -A INPUT -j LOG --log-prefix "INPUT:DROP:" --log-level 6
# iptables -A INPUT -j DROP

As soon as I added this I could see output in syslog using:

# tail -f /var/log/syslog
Apr  7 16:19:37 node2 kernel: [ 2695.658497] INPUT:DROP:IN=enp1s0 OUT= MAC=ff:ff:ff:ff:ff:ff:52:54:00:bf:16:56:08:00 SRC=192.168.122.1 DST=192.168.122.255 LEN=160 TOS=0x00 PREC=0x00 TTL=64 ID=3646 DF PROTO=UDP SPT=17500 DPT=17500 LEN=140 
Apr  7 16:20:07 node2 kernel: [ 2725.665884] INPUT:DROP:IN=enp1s0 OUT= MAC=ff:ff:ff:ff:ff:ff:52:54:00:bf:16:56:08:00 SRC=192.168.122.1 DST=192.168.122.255 LEN=160 TOS=0x00 PREC=0x00 TTL=64 ID=6728 DF PROTO=UDP SPT=17500 DPT=17500 LEN=140 

Something to Think About

Write you basic rule set and save them to the files as stated above. But consider saving them to a file you can work on with a text editor and reorder things and add rules in a more manageable way, eg.

# iptables-save > ~/my.rules
# vi ~/.my.rules
# iptables-restore < ~/my.rules

But when you are done, don’t forget to save the final changes to the system persistent files as above. As you can see below the file is pretty self explanatory – based on what we learned above.

~/my.rules

# Generated by xtables-save v1.8.2 on Tue Apr  7 16:32:34 2020
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Tue Apr  7 16:32:34 2020
# Generated by xtables-save v1.8.2 on Tue Apr  7 16:32:34 2020
*filter
:INPUT DROP [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -p tcp -m udp --dport 53 -j ACCEPT
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
-A INPUT -j LOG --log-prefix "INPUT:DROP:" --log-level 6
-A INPUT -j DROP
COMMIT
# Completed on Tue Apr  7 16:32:34 2020
# Generated by xtables-save v1.8.2 on Tue Apr  7 16:32:34 2020
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT
# Completed on Tue Apr  7 16:32:34 2020

Conntrack and ctstate

One rule you’ll see a lot gets dropped into most input chains as early as you can is something like this:

-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

What this does is prevents repetitive trawls down your tables to find out if the connection is to be accepted or not. By keeping track of the connections the firewall is able to identify related and established connections and just accept them. One rule at the top means less processing for packets for connections the firewall already knows about.

In our little firewall set of tables this may not seem a lot, but when you start noticing how many rules get added to these tables by applications like Docker and libvirt, you begin to understand why picking out known connections early on must be a saving.

References

https://help.ubuntu.com/community/IptablesHowTo

https://wiki.debian.org/iptables

Coming in Part 2 – iptables and NAT