Building a firewall? Simple and easy!

I strive for simplicity since I am a strong believer that achieving a goal with the most simplest solution looks elegant, proves that you have deep knowledge on the subject, and overall is beautiful by itself. Additionally to this, a simple solution is easier to comprehend and to audit, hence it is much easier to ensure the security of such a solution.

Over the last decade, I stumbled upon numerous complicated firewalls erected on the NAT boxes with tens (sometimes, hundreds!) of rules describing the traffic flows and punched holes for some edge cases. Every time I wondered: what kind of a bug has bitten the person who composed such a convoluted ruleset that is a nightmare to manage?

In 99% of the cases, I was able to come up with a ruleset of usually less than 20 rules for the whole firewall to achieve exactly the same result. So, in this article I will explain my approach on building firewalls that are easy to support and to understand.

Before you dive into optimising (or creating) your firewall, there are some things you need to have a clear understanding of:

Before you start implementing the rules it often helps to describe your firewall ruleset in simple sentences, e.g.:

  1. deny any incoming traffic to our network through the firewall unless it is explicitly allowed by the rules;

  2. allow any outbound traffic from our network through the firewall unless it is explicitly denied by the rules;

  3. deny any forwarding of traffic through the firewall unless it is explicitly allowed by the rules;

  4. allow anyone on the Internet to connect to the webserver on ports 80/tcp and 443/tcp;

  5. allow our office to connect to the firewall via SSH on port 22/tcp;

  6. allow traffic from our network to pass the firewall toward the Internet;

  7. allow the back channel traffic from the resources on the Internet to our network;

And then research how each of these sentences can be implemented using the configuration language of the tool. The point here is that you need to take one step at a time and translate the logic exactly so you do not deviate from your goals – you will be able to optimise the result at a later stage.

Well, it is time for a real world example, I guess. The following snippet is used as the skeleton for the iptables ruleset on systems I manage (this snippet assumes that 192.168.0.0/16 is the internal network and that the ruleset is installed on the NAT instance which is a gateway to the Internet):

*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state INVALID -j DROP
# If it was already established pass bits through
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# Allow for the local traffic
-A INPUT -i lo -j ACCEPT
# Let's MTU discovery and other network management to work properly
-A INPUT -p icmp -m icmp --icmp-type any -j ACCEPT
# Allow remote SSH logins on the specified port (should be <1024 and would require sshd re-configuration if the port is not standard)
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
# Allow DHCP client to get its information
-A INPUT -i eth0 -p udp -m state --state NEW -m udp --sport 67:68 --dport 67:68 -j ACCEPT
# Reject anything that we did not specifically allow above
-A INPUT -j REJECT --reject-with icmp-host-prohibited
# This is a NAT box, so we accept packets from our network designated to the world, but reject any other forwarding attempts.
-A FORWARD -i eth0 -s 192.168.0.0/22 ! -d 192.168.0.0/22 -j ACCEPT
-A FORWARD -i eth0 ! -s 192.168.0.0/22 -d 192.168.0.0/22 -j ACCEPT
# If there is a MTU mismatch between the NAT box and hosts behind it we want to allow ICMP for the MTU discovery
-A FORWARD -i eth0 -p icmp -m icmp --icmp-type any -s 192.168.0.0/22 -d 192.168.0.0/22 -j ACCEPT
# We may be interested in anything that is rejected in this chain, so let's log it
-A FORWARD -j LOG
# We do not allow to forward anything else
-A FORWARD -j REJECT --reject-with icmp-host-prohibited
COMMIT
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 192.168.0.0/22 ! -d 192.168.0.0/22 -j MASQUERADE
COMMIT

There are a few things to note:

So, here you have it a solid firewall ruleset consisting of just 13 rules.