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 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 the 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:
You do not need to be an expert or a guru, but you should know the protocols, standards, and internals of the networking related to the area you are going to secure with the firewall. When you are in doubt, do not rely on an assumption: use search engines and locate the corresponding RFC, standard, and/or any other authoritative source of information documenting the protocol you are working with. It is impossible to know everything, but the common fallacy is that people often create things based on wrong assumptions when it was a couple of clicks away to research and understand the subject. I have seen so many times when people are "designing" their firewalls without a slightest clue in regard to the standards covering interoperability of the hosts in the IP network. You can easily spot these "creations" if you look at the ruleset and see that there is a rule that drops ICMP packets unconditionally (or do not define any rules dealing with ICMP when the default policy is "deny")
- Traffic flows
Think of a firewall as a filter that manages traffic flows (a good example for those who prefer a visual representation here is a link to a very nice picture of how traffic passes through the Linux netfilter subsystem). Before you can make a decision in regard to whether to allow or deny something you must know what your are dealing with and what outcome you want to achieve. It would tremendously help you later if you could simply draw a diagram of your network and overlay it with a layer documenting the inbound and outbound flows of each node on the diagram. This "data flow" diagram is supposed to be a part of any solution documentation involving network infrastructure, but in reality most of the enterprises (at least the ones I worked with) simply forget to generate one. On such a diagram you would be able to see straight away what kind of legitimate traffic is going to cross your firewall and what traffic is not supposed to reach your firewall at all.
It may sound obvious, but one of the most important things is to know exactly what you are trying to achieve. You are not building a firewall for the sake of building a firewall, are you? Frankly speaking, there are just a handful number of scenarios for creating a firewall (I fail to come up with more than five at this point) and all of them are very simple.
If your network provides any services to the external network (e.g. Internet) there are just two options I can think of:
- Allow external entities to access services on defined public endpoints (inbound flow) and allow unrestricted outbound traffic from your network to the external one (outbound flow)
- Allow external entities to access services on defined public endpoints and restrict outbound traffic from your network to a defined set of external endpoints
If your network does not provide any services to the external network and only consumes resources from it, I see only two options:
- Disallow any access from the external entities to your resources, but allow your network to access the external endpoints and receive responses from there
- Same as above, but restrict the outbound traffic to a selected set of the external endpoints
The fifth scenario is the one I personally do not use and is the blacklist approach: allow everything in each direction and block communication for the specific endpoints only (be they external and/or internal)
Last, but not least, is the requirement to know the corresponding toolset you are going to utilise to achieve your goals. There are numerous high-level frameworks which are supposed to make systems administrator's life easier (e.g. RHEL/CentOS/Fedora are using firewalld on top of iptables), however, personally I prefer to work with iptables directly (thus, my systems do not have firewalld installed).
Before you start implementing the rules it often helps to describe your firewall ruleset in simple sentences, e.g.
- deny any incoming traffic to our network through the firewall unless it is explicitly allowed by the rules
- allow any outbound traffic from our network through the firewall unless it is explicitly denied by the rules
- deny any forwarding of traffic through the firewall unless it is explicitly allowed by the rules
- allow anyone on the Internet to connect to the webserver on ports 80/tcp and 443/tcp
- allow our office to connect to the firewall via SSH on port 22/tcp
- allow traffic from our network to pass the firewall toward the Internet
- allow the backchannel traffic from the resources on the Internet to our network
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):
# cat /etc/sysconfig/iptables *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:
- default policies for INPUT and FORWARD are set to ACCEPT while the last rule in each chain is set to REJECT. The reason for this is that the default policy can be either ACCEPT or DROP with the latter silently dropping packets. Dropping packets makes it hard to investigate network issues, hence I prefer REJECT instead.
- if you need to open more ports the best place to insert your rules would be between SSH and DHCP rules in the INPUT chain
- if you are setting up a firewall on the server (so no traffic forwarding is needed) you may want to either drop all rules from the FORWARD chain and set the default policy for that chain to DROP, or you can simply leave last two rules (LOG and REJECT) in the chain removing the rest. Also, the nat table is not needed on the servers, so should be omitted.
- the above ruleset is not strict enough in regard to filtering ICMP traffic and allows all types of ICMP, perhaps it could be further tightened to allow only ICMP types 0 (reply), 3 (destination unreachable), 8 (request), 11 (time exceeded), and 30 (traceroute) to pass through the firewall