How to set up a stateful firewall with iptables

My old post to set up a small rules set for iptables is deprecated so I decided to update this post and improve some rules.
This time I tested this iptables setup on my Raspberry Pi. I connected to my Pi via SSH and the first time I tested the rules order I locked myself out and I had to connect the monitor and keyboard to fix this. The rule order in this post worked for me to set everything up via SSH.

I also tried this setup on a virtual machine and made a screencast of it. You can find the video at the end of this post.

1. Kernel modules
2. Important Rules
3. Chain Policies
4. Port Rules
5. Logging
6. Saving Rules
7. Appendix (Script and video)

1. Kernel modules:

The netfilter module state is deprecated, the new module is called conntrack. In my case the conntrack kernel module was not loaded. You can list all loaded kernel modules with:

lsmod

If you have to load the conntrack module, you can do it dynamically:

$ modprobe ip_conntrack
$ modprobe ip_conntrack_ftp

2. Important Rules:

The first thing we have to do is to allow incoming connections which are already established or related to a connection. This is why it is called a stateful firewall. The rules are not only to open or close ports you can also use the state of a connection to set up iptables rules. In this case we don’t want to allow new incoming connections (packets to establish a new connections to your computer). We can do this by using the conntrack module and the ctstate option.
This is the rule to allow packets which belongs to a connection which is established or related to a connection:

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

The next step is to allow outgoing DNS requests:

# iptables -A OUTPUT -o eth0 -p udp --dport 53 -m conntrack --ctstate \
  NEW,ESTABLISHED -j ACCEPT
# iptables -A OUTPUT -o eth0 -p tcp --dport 53 -m conntrack --ctstate \
  NEW,ESTABLISHED -j ACCEPT

I’m connected to my Pi, so I have to allow outgoing SSH traffic of established connections:

# iptables -A OUTPUT -o eth0 -p tcp --sport 22 -m conntrack --ctstate \
  ESTABLISHED,RELATED -j ACCEPT

In section 4.3 I will use port-knocking to accept incoming connections on port 22. If you don’t want to use port-knocking and want to accept incoming SSH connections use this rule:

# iptables -A INPUT -i eth0 -p tcp --dport 22 -m conntrack --ctstate \
  NEW,ESTABLISHED -j ACCEPT

3. Chain Policies

Now it’s time to change the chain policies to DROP, i.e. if a packet does not match a rule, it’ll be dropped.

# iptables -P FORWARD DROP
# iptables -P INPUT DROP
# iptables -P OUTPUT DROP

4. Port Rules

In this section I’ll show you some simple port rules, e.g. to accept incoming HTTP connections. But I will also show you a way to accept outgoing FTP connections. This is a bit tricky due to the FTP passive mode.

ICMP

The first rule accepts 10 incoming ping requests per second. If the limit is reached, this rule will not match and the packet will be dropped. The second rule allows outgoing ICMP packets, i.e. we can send ping request as much as we want.

# iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 10/second \ 
-j ACCEPT
# iptables -A OUTPUT -p icmp -j ACCEPT

4.1 FTP

To keep my Pi up to date I have to use FTP connections. But this is a problem, because the passive mode of the ftp protocol changes the port to a new one above 1024. I don’t know the exact port to insert a rule for this port. I read lots of forums and the following rules allow FTP connections:

# iptables -A OUTPUT -o eth0 -p tcp --dport 21 -m conntrack --ctstate \
  NEW,ESTABLISHED -j ACCEPT
# iptables -A INPUT -i eth0 -p tcp --sport 21 -m conntrack --ctstate \
  RELATED,ESTABLISHED -j ACCEPT

Some forums write that you need the following rules. In my case it works without these two rules.

$ iptables -A INPUT -i eth0 -p tcp --sport 20 -m conntrack --ctstate \
  ESTABLISHED,RELATED -j ACCEPT
$ iptables -A OUTPUT -o eth0 -p tcp --dport 20 -m conntrack --ctstate \
  ESTABLISHED -j ACCEPT

The FTP active mode works, but not the passive mode. Every time the client tried to switch to passive mode I got a connection timeout. To allow the passive mode I had to add the following rules:

# iptables -A INPUT -i eth0 -p tcp --sport 1024: --dport 1024: -m conntrack \
  --ctstate ESTABLISHED -j ACCEPT
# iptables -A OUTPUT -o eth0 -p tcp --sport 1024: --dport 1024: -m conntrack \
  --ctstate ESTABLISHED,RELATED -j ACCEPT

These rules open ports above 1024 only for established or related connections. If something tries to establish a new connection to a port above 1024, iptables will drop these packets.

4.2 HTTP

If you want to add more rules, e.g. for outgoing and incoming HTTP connections, you can use the following rules. Change the port and the protocol to anything you want.
These rules allow only new outgoing connections not incoming.

$ iptables -A OUTPUT -o eth0 -p tcp --dport 80 -m conntrack --ctstate \
  NEW,ESTABLISHED -j ACCEPT
$ iptables -A INPUT -i eth0 -p tcp --sport 80 -m conntrack --ctstate \
  RELATED,ESTABLISHED -j ACCEPT

Add more rules for all ports you want to use.

4.3 Port-Knocking

h0nk3ym0nk3y found a page which explains how to set up port knocking with iptables (link at the bottom of this post). It’s interesting so I decided to add it to this post. In this example I’ll use three UDP “knocks” on port 12345, 23456, 34567 to accept incoming connections on port 22.
The first step is to create a new chain STATE0 for the first knock.

# iptables -N STATE0

Now we have to set a “flag” named KNOCK1, if a UDP packet is send to port 12345. The packet will be dropped.

# iptables -A STATE0 -p udp --dport 12345 -m recent --name KNOCK1 --set -j DROP

If the packet doesn’t match the rule, it’ll be dropped.

# iptables -A STATE0 -j DROP

These steps has to be done for all three knocks. Here is the setup for KNOCK2.

# iptables -N STATE1
# iptables -A STATE1 -m recent --name KNOCK1 --remove
# iptables -A STATE1 -p udp --dport 23456 -m recent --name KNOCK2 --set -j DROP
# iptables -A STATE1 -j STATE0

The second rule removes the “flag” of KNOCK1.
Setup for KNOCK3:

iptables -N STATE2
iptables -A STATE2 -m recent --name KNOCK2 --remove
iptables -A STATE2 -p udp --dport 34567 -m recent --name KNOCK3 --set -j DROP
iptables -A STATE2 -j STATE0

Now we have all three knocks, but we have to set a rule to accept incoming connection on port 22. This is done in STATE3:

iptables -N STATE3
iptables -A STATE3 -m recent --name KNOCK3 --remove
iptables -A STATE3 -p tcp --dport 22 -j ACCEPT
iptables -A STATE3 -j STATE0

The second rule removes the KNOCK3 flag and rule three accepts incoming SSH connections. If no rule matches, iptables will jump back to chain STATE0.
The last step of port knocking is to check the flags for each incoming packet:

iptables -A INPUT -m recent --name KNOCK3 --rcheck -j STATE3
iptables -A INPUT -m recent --name KNOCK2 --rcheck -j STATE2
iptables -A INPUT -m recent --name KNOCK1 --rcheck -j STATE1
iptables -A INPUT -j STATE0

I highly recommend to insert these rules at the end of the INPUT chain, because the last rule will jump to chain STATE0 and drop the packet.

If you want to establish a SSH connection to the computer, you have to knock at the three ports. You can use netcat for this purpose. Here is a little bash script to do it automatically (source of the script is at the bottom of this post):

#!/bin/bash 

# CHANGE THIS
IP="192.168.1.1"

# Knock 1
echo -n "Knock" | nc -q1 -u $IP 12345
# Knock 2
echo -n "Knock" | nc -q1 -u $IP 23456
# Knock 3
echo -n "Knock" | nc -q1 -u $IP 34567
# establish ssh connection
ssh $IP

5. Logging

This step is optional, but it could be very useful for debugging if something isn’t working. Each packet which does not match a rule will be logged and then dropped.
I created a new chain named LOGDROP:

# iptables -N LOGDROP

Next we have to add the logging rules at the end of the INPUT chain (and OUTPUT if you want). Make sure that these logging rules are always the last rules within you chain. In this case I used the -A option to append the rules and they will be added at the end of the chain.

# iptables -A INPUT -p tcp -j LOGDROP
# iptables -A INPUT -p udp -j LOGDROP

Now each TCP and UDP packet will be redirected to the LOGDROP chain.
To log all packets could result in a very large logfile. I decided to limit the packets which will be logged to decrease the size of the logfile:

# iptables -A LOGDROP -m limit --limit 7/min --limit-burst 10 -j LOG
# iptables -A LOGDROP -j DROP

The first rule will log the first 10 packets. If the limit of 10 packets is reached, only 7 packets per minute will be logged. The last rule will drop the packet.

If you want to use logging with port-knocking, you have to modify the last rule of chain STATE0. First delete the last rule of chain STATE0.

# iptables -D STATE0 -j DROP

Add a rule at the end of STATE0 to jump to LOGDROP

# iptables -A STATE0 -j LOGDROP

Now logging is enabled and all packets that doesn’t match a rule will be logged in /var/log/syslog.

6. Saving Rules

To save all the rules you only have to enter the following command. It will create the directory /etc/iptables and saves all rules to the file /etc/iptables/firewall.conf

$ mkdir /etc/iptables
$ iptables-save > /etc/iptables/firewall.conf

I recommend to change the rights of firewall.conf to 600.

$ chmod 600 /etc/iptables/firewall.conf

But after a reboot you have to add the rules manually. If you want to add all rules automatically, you have to change some initscripts. I use Slackware on my Raspberry Pi and my computer, i.e. the following changes are related to Slackware.
You have to uncomment the line in /etc/rc.d/rc.inet2 to start the firewall script. Here is an excerpt of my /etc/rc.d/rc.inet2:

[...]
# If there is a firewall script, run it before enabling packet forwarding.
# See the HOWTOs on http://www.netfilter.org/ for documentation on
# setting up a firewall or NAT on Linux.  In some cases this might need to
# be moved past the section below dealing with IP packet forwarding.
if [ -x /etc/rc.d/rc.firewall ]; then
  /etc/rc.d/rc.firewall start
fi
[...]

It will start the /etc/rc.d/rc.firewall script during the boot process. Sometimes this script doesn’t exist, so you have to create it. This is my /etc/rc.d/rc.firewall:

#!/bin/sh 

RESTORE=/usr/sbin/iptables-restore
IPTABLES=/usr/sbin/iptables
STAT=/usr/bin/stat
IPSTATE=/etc/iptables/firewall.conf

case "$1" in
    start)
        test -x $RESTORE || exit 0
        test -x $STAT || exit 0
        if test `$STAT --format="%a" $IPSTATE` -ne "600"; then
            echo "Permissions for $IPSTATE must be 600 (rw-------)"
            exit 0
        fi
        if test `$STAT --format="%u" $IPSTATE` -ne "0"; then
            echo "The superuser must have ownership for $IPSTATE (uid 0)"
            exit 0
        fi
        $RESTORE < $IPSTATE
        ;;
    stop)
        # set all chain policies to ACCEPT
        $IPTABLES -P INPUT ACCEPT
        $IPTABLES -P OUTPUT ACCEPT
        $IPTABLES -P FORWARD ACCEPT
        # Flush all rules
        $IPTABLES -F
        ;;
    status)
        $IPTABLES -L
	;;
    *)
        echo "Usage: rc.iptables {start|stop|status}"
        exit 1
esac

exit 0

Now the rules will be added during the boot process automatically.

7. Appendix

Here is the complete bash script I used in the video:

#!/bin/bash

# incoming interface
INIF="eth1"
# outgoing interface
OUTIF="eth1"

# set chain policy of each chain to ACCEPT
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT

# flush all rules
iptables -F
# delete user-defined chains
iptables -X
# set packet counter to zero
iptables -Z

# accept established incoming connections
iptables -A INPUT -i $INIF -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# accept outgoing DNS-traffic
iptables -A OUTPUT -o $OUTIF -p udp --dport 53 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -o $OUTIF -p tcp --dport 53 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# accept outgoing SSH traffic of established connections
iptables -A OUTPUT -o $OUTIF -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# accept a specific limit of incoming icmp echo-requests
iptables -A INPUT -i $INIF -p icmp --icmp-type echo-request -m limit --limit 10/second -j ACCEPT
# accept outgoing icmp packets
iptables -A OUTPUT -o $OUTIF -p icmp -j ACCEPT
# accept traffic from loopback interface
iptables -A INPUT -s 127.0.0.0/8 -j ACCEPT
# accept incoming HTTP traffic
iptables -A INPUT -i $INIF -p tcp --dport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -o $OUTIF -p tcp --sport 80 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# set chain policy of each chain to DROP 
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

#
# Port Knocking for SSH port
#
# create new chain STATE0
iptables -N STATE0
# set "flag" KNOCK1 if a udp packet is send to port 12345 and drop the packet
iptables -A STATE0 -p udp --dport 12345 -m recent --name KNOCK1 --set -j DROP
# drop all packets
iptables -A STATE0 -j DROP

iptables -N STATE1
# remove "flag" KNOCK1"
iptables -A STATE1 -m recent --name KNOCK1 --remove
iptables -A STATE1 -p udp --dport 23456 -m recent --name KNOCK2 --set -j DROP
# jump to chain STATE0
iptables -A STATE1 -j STATE0

iptables -N STATE2
iptables -A STATE2 -m recent --name KNOCK2 --remove
iptables -A STATE2 -p udp --dport 34567 -m recent --name KNOCK3 --set -j DROP
iptables -A STATE2 -j STATE0

iptables -N STATE3
iptables -A STATE3 -m recent --name KNOCK3 --remove
# accept connections on port 22 after 3 correct knocks
iptables -A STATE3 -p tcp --dport 22 -j ACCEPT
# jump to chain STATE0
iptables -A STATE3 -j STATE0

# check packet for "flags"
iptables -A INPUT -m recent --name KNOCK3 --rcheck -j STATE3
iptables -A INPUT -m recent --name KNOCK2 --rcheck -j STATE2
iptables -A INPUT -m recent --name KNOCK1 --rcheck -j STATE1
iptables -A INPUT -j STATE0


#
# Logging
#
# create a new chain for logging
iptables -N LOGDROP
# log the first 10 packets, and after the limit is reached log 5 packets per minute
iptables -A LOGDROP -m limit --limit 5/min --limit-burst 10 -j LOG
# drop all packets
iptables -A LOGDROP -j DROP
# delete last rule in chain STATE0 
iptables -D STATE0 -j DROP
# jump to LOGDROP if no rule matches in STATE0
iptables -A STATE0 -j LOGDROP

Sources:
Logging: Arch Wiki
Port Knocking: micro Howto

Posted on December 17, 2013, in Command-Line, Configure, Network, Security, Software and tagged , , , , , , , , , , , , , . Bookmark the permalink. 3 Comments.

  1. Wonderful tutorial. But on the http rule, there is a mismatch between the code up and the code down. I presume that the code down is the correct because of the “sport” for the output rule.

  2. Do they have to be in this order? Because now my VPN clients won’t connect to the server….

  1. Pingback: Stateful firewall setup with IPtables | 0ddn1x: tricks with *nix

Leave a comment