The Tao of Ops: Taming iptables

One of the best tools to use every day for locking down your servers is iptables. (You do lock down your servers, right? ;-)

Not using iptables is akin to having fancy locks with a plywood door - sure it is secure but you just cannot know that someone won’t be able to break through.

To this end I use a small set of bash scripts that ensure I always have a baseline iptables configuration and items can be added or removed quickly.

Let me outline what they are before we get to the fiddly bits...

  • checkiptables.sh — A script to compare your saved iptables config in /etc/iptables.rules to what is currrently being used. Very handy to see if you have any local changes before modifying the global config.
  • iptables-pre-up — A Debian/Ubuntu centric script that runs when your network interface comes online to ensure that your rules are active on restart. RedHat/CentOS folks don’t need this.
  • iptables.sh — The master script that sets certain defaults and then loads any inbound/outbound scripts.
  • iptables_*.sh — A bash scripts that is very easy to generate using templates for each rule needed to allow inbound/outbound traffic. I use a naming pattern to make them unique within the directory.

These scripts should be placed into your favourite local binary directory, for example

/opt/sbin
  /checkiptables.sh
  /iptables.sh
  /iptables_conf.d/
    iptables_*.sh

checkiptables.sh

#!/bin/bash
# generate a list of active rules and remove all the cruft
iptables-save | sed -e ’/^[#:]/d’ > /tmp/iptables.check
if [ -e /etc/iptables.rules ]; then
  cat /etc/iptables.rules | sed -e ’/^[#:]/d’ > /tmp/iptables.rules
  diff -q /tmp/iptables.rules /tmp/iptables.check
else
  echo "unable to check, /etc/iptables.rules does not exist"
fi

That is really it - the magic is in the sed portion - it removes all of the stuff that iptables-save outputs that isn’t related to rules and often can change between runs. The remainder of the script is performing a diff against the saved state vs current state. If current state has been modified you will see as output:

Files /tmp/iptables.rules and /tmp/iptables.check differ

iptables.sh

#!/bin/bash
PUBLICNET=eth2

iptables -F
ip6tables -F
ip6tables -X
ip6tables -t mangle -F
ip6tables -t mangle -X

# Default policy is drop
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

ip6tables -P INPUT DROP
ip6tables -P OUTPUT DROP
ip6tables -P FORWARD DROP

iptables -A INPUT -p tcp -m multiport --dports 22 -j fail2ban-ssh
iptables -A fail2ban-ssh -j RETURN
# Allow localhost
iptables -A INPUT  -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
ip6tables -A INPUT  -i lo -j ACCEPT
ip6tables -A OUTPUT -o lo -j ACCEPT
# Allow inbound ipv6 ICMP so we can be seen by neighbors
ip6tables -A INPUT  -i ${PUBLICNET} -p ipv6-icmp -j ACCEPT
ip6tables -A OUTPUT -o ${PUBLICNET} -p ipv6-icmp -j ACCEPT
# Allow incoming SSH
iptables -A INPUT  -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow outbound DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT  -p udp --sport 53 -j ACCEPT
# Only allow NTP if it’s our request
iptables -A INPUT -s 0/0 -d 0/0 -p udp --source-port 123:123 -m state --state ESTABLISHED -j ACCEPT
iptables -A OUTPUT -s 0/0 -d 0/0 -p udp --destination-port 123:123 -m state --state NEW,ESTABLISHED -j ACCEPT

for s in /opt/sbin/iptables_conf.d/iptables_*.sh ; do
  if [ -e "${s}" ]; then
    source ${s}
  fi
done

There is a lot going on here - flushing all current rules, setting the default policy to DROP so nothing gets through until you explicitly allow it and then allowing all localhost traffic.

After the boilerplate code, the remainder is setting up rules for SSH, DNS and other ports that are common to all server deploys. It’s the last five lines where the fun is - they loop through the files found in the iptables_conf.d directory and load any iptables_*.sh script they find. Here’s an example rule that would be in iptables_conf.d/ - this one allows outbound Etcd:

# Allow outgoing etcd
iptables -A OUTPUT -o eth2 -p tcp --dport 4001 -m state --state NEW,ESTABLISHED -j ACCEPT
iptables -A INPUT  -i eth2 -p tcp --sport 4001 -m state --state ESTABLISHED -j ACCEPT

Having each rule defined by a script allows you to create the scripts using templates from your configuration system.

With the above you now have a very flexible way to manage iptables that is also self-documenting - how cool is that!

You might also enjoy reading: