Managing the Iptables Firewall

Your firewall is an important first line of defense on any publicly-accessible server. In previous articles I listed how to set up a firewall without getting into any detail. This article goes into depth with configuring your iptables firewall.

Basics and commands

There are a few things to know about firewall rules with iptables before diving in and creating rules.

INPUT, FORWARD, OUTPUT

There are three "chains". Each chain is a list of rules for packets (traffic) that are followed in order.

  • INPUT - rules to determine which inbound traffic will be accepted or denied
  • OUTPUT - rules to determine which outbound traffic will be accepted or denied
  • FORWARD - rules to determine which traffic to be forwarded will be accepted or denied

Defaults

You can define a default behaviour for each chain - either to ACCEPT all traffic, or DENY all traffic. Debian/Ubuntu servers tend to come with all chains open to all traffic - set to ACCEPT. RedHat/CentOS servers tend to DENY all traffic in each chain by default.

Commands

You can manage firewall rules in each chain by using the commands to append, insert or remove rules.

Since each rule in a chain followed in order, it's important that the rules are setup in proper order. The first rule that matches the type of traffic will be used.

Appending Rules

Let's append a rule to the INPUT chain. This will allow incoming SSH (port 22) traffic:

sudo iptables -A INPUT -p tcp --dport ssh -j ACCEPT

To review what we did:

  • -A INPUT - Append a rule to the "input" chain
  • -p tcp - Apply the rule to the tcp protocol
  • --dport ssh - Apply the rule to the port used by SSH (22)
  • -j ACCEPT - Set it to accept traffic to the input chain when using tcp on the ssh port

Next, we can allow regular web traffic (port 80). The only thing different we'll do here is set port 80 manually (although we could use "http" instead of defining "80").

$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

Finally, let's append a rule to drop any other traffic:

$ sudo iptables -A INPUT -j DROP

We can look at our rules so far:

$ sudo iptables -L

Chain INPUT (policy ACCEPT)
target     prot opt source               destination             
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http
DROP       all  --  anywhere             anywhere 

Since these rules are applied in order, we can see how this will work.

If the server has a web request on port 22 (ssh) or 80 (http), it will match the first or second rule (respectively) which says to accept the traffic.

If a request is given that's NOT tcp over the SSH or HTTP ports, then it will reach (and match) the last rule in this chain, which says to drop the traffic. Any rule after the DROP rule will not be met. This is why the order of rules is so important.

Inserting Rules

Now let's say we need to add a rule to allow https traffic (port 443). If we just append this rule, then it will never get reached and matched, since there would be a blanket DROP rule in place before it. So, instead of appending, we need to insert a rule in a specific slot, before the blanket DROP rule:

$ sudo iptables -I INPUT 3 -p tcp --dport 443 -j ACCEPT

Let's review what we did:

  • -I INPUT 3 - Insert a rule to the "input" chain in the 3rd slot
  • -p tcp - Apply the rule to the tcp protocol
  • --dport 443 - Apply the rule to the port used by https (443)
  • -j ACCEPT - Set it to accept traffic to the input chain when using tcp on port 443

We can see this now:

$ sudo iptables -L

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:https
DROP       all  --  anywhere             anywhere

Https traffic will now be allowed through!

Another common need is to allow traffic to the loopback interface (localhost, 127.0.0.1). Many programs use the loopback interface to talk to eachother, so we shouldn't block traffic amongst them. Similarly to the HTTPS rule, we need to insert this one (preferable near the beginning), rather than append it to the end. Here's how:

$ sudo iptables -I INPUT 1 -i lo -j ACCEPT

This command is very similar to the ones above:

  • -I INPUT 1 - Insert a rule to the "input" chain in the 1st slot
  • -i lo - Apply the rule to the loopback interface
  • -j ACCEPT - Set it to accept traffic to the input chain when using tcp on port 443

We can see if that works now. This time, add the -v flag when reviewing the rules:

$ sudo iptables -L -v

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target     prot opt in     out     source               destination         
   0     0 ACCEPT     all  --  lo     any     anywhere             anywhere            
 320 18032 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:ssh
   0     0 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:http
   0     0 ACCEPT     tcp  --  any    any     anywhere             anywhere             tcp dpt:https
  14  1720 DROP       all  --  any    any     anywhere             anywhere

Great, we can see that "lo", the loopback interface, now accepts incoming traffic, and the rule was placed at the top of the INPUT chain.

Current and Established Connections

I didn't write this first, but the first rule that should be in place in your firewall is one allowing already established (and related) connections:

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

# When inserting
$ iptables -I INPUT 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

This should go before the rule allowing loopback access as well. Ideally, it's the first rule you add to any empty chain.

Deleting Rules

Lastly, let's delete a rule. Let's say we no longer want to allow HTTPS traffic. For that, we can use the delete command:

# Delete based on what the rule does:
$ sudo iptables -D INPUT -p tcp --dport 443 -j ACCEPT

# Or delete based on its position:
$ sudo iptables -D INPUT 3  # Assumes the "https" rule is the third in the list

Two Methods

So far, we've seen one method of using iptables. The default for each chain is to ACCEPT traffic. Notice that when we list the rules, we can see that - "policy ACCEPT":

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)

Debian/Ubuntu servers usually start with the chains defaulting to ACCEPT. However, Redhat/CentOS servers often start with their chains defaulting to DROP traffic. Defaulting to DROP can often be easier (and safer), so let's use that.

Let's change the INPUT chain to default to DROP:

$ sudo iptables -P INPUT DROP

Then we can remove the last line used above, which DROPs any remaining unmatched rules:

$ sudo iptables -D INPUT -j DROP

If we run iptables -L, we can see the INPUT chain now defaults to DROP (also note the first line which accepts already established/related connections):

Chain INPUT (policy DROP)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     all  --  anywhere             anywhere            
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http

Great, so now everything is going to be dropped unless explicitly opened.

The general rule of thumb for the three chains are to:

  • Drop traffic on the INPUT chain by default
  • Drop traffic on the FORWARD chain by default
  • Allow traffic on the OUTPUT chain by default

Persistence

The rules for iptables live in memory. If you restart your server, these rules will be lost. You can save them out to a file, however:

# Save rules to /etc/iptables/iptables.rules
$ sudo iptables-save | sudo tee /etc/iptables/iptables.rules

You can then restore those rules when you need to:

$ sudo iptables-restore < /etc/iptables/iptables.rules

On some OSes, you can use systemd or others tool to add these rules on boot. In Debian/Ubuntu, there's a package called iptables-persistent to do that for you. Let's install that:

$ sudo apt-get install -y iptables-persistent

# And then start the service:
$ sudo service iptables-persistent start

After installing this, you'll see new files in /etc/iptables:

/etc/iptables/rules.v4
/etc/iptables/rules.v6 # ipv6

After any changes you make, you can save to those files:

sudo iptables-save | sudo tee /etc/iptables/rules.v4
sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 # for ipv6

Then you can start the service to read those in:

$ sudo service iptables-persistent start # reload or restart should also work

Logging Dropped Packets

You might find it useful to log dropped packets (traffic). To do this, we'll actually create another chain. Here's the basic steps:

  1. Create a new chain
  2. Ensure any unmatched traffic jumps to the new chain
  3. Log the packets with a searchable prefix
  4. Drop those packets

Let's start!

# Create new chain
$ sudo iptables -N LOGGING

# Ensure unmatched packets jump to new chain
$ sudo iptables -A INPUT -j LOGGING

# Log the packets with a prefix
$ sudo iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7

# Drop those packets
$ sudo iptables -A LOGGING -j DROP

Here's what that'll look like when we run iptables -L:

Chain INPUT (policy DROP)
target     prot opt source               destination         
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
ACCEPT     all  --  anywhere             anywhere            
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:ssh
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http
LOGGING    all  --  anywhere             anywhere            

Chain LOGGING (1 references)
target     prot opt source               destination         
LOG        all  --  anywhere             anywhere             limit: avg 2/min burst 5 LOG level debug prefix "IPTables Packet Dropped: "
DROP       all  --  anywhere             anywhere 

By default, this will go to the kernel log. In Ubuntu, you can watch this:

sudo tail -f /var/log/kern.log

I see entries like this when attempting connections which get dropped (I set HTTPS traffic to get dropped):

Dec  5 02:27:51 precise64 kernel: [ 2101.687289] IPTables Packet Dropped: IN=eth1 OUT= MAC=08:00:27:4f:82:c9:0a:00:27:00:00:00:08:00 SRC=192.168.33.1 DST=192.168.33.10 LEN=64 TOS=0x00 PREC=0x00 TTL=64 ID=59982 DF PROTO=TCP SPT=51765 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0

Resources