Simplifying Firewall Rules

I've been thinking a lot lately about how to manage firewalls in a maintainable way. The longer a firewall is around the more it gets out of hand.  A large amount of business logic is encoded in firewall configuration. When software developers think about maintainability, they start talking about smells, they start talking about refactoring.  This kind of thinking is starting to come to the field of system administration with tools like puppet.

I have not been able to find much similar discussion of the craft of building firewalls. Most philosophical discussions about firewalls that I've seen seem to be focused on tightening down the firewall as far as possible without regard to issues of maintainability. A search for "refactor firewall" turned up this interesting  LISA '07 talk entitled "Inferring Higher Level Policies from Firewall Rules." A search for "simplify firewall" turned up some discussion of the Ubuntu desktop firewall tool UFW, which is interested in usability issues not configuration management.


I have used a variety of tools to build firewalls.  I've done all sorts of Linux/iptables firewalls and worked with OpenBSD/pf firewalls. I've played with fwbuilder, a nice GUI for centralizing firewall configuration.  I've trained on checkpoint and helped with a couple of PIX firewalls.  So, I have a pretty good idea of the kinds of features firewall software gives you.

For years, my attempts to make firewall rules more manageable only resulted in additional complexity.  My first major breakthrough came when reading a firewall written by my colleague Theron.  It was a pf firewall and made good use of the feature that pf calls "tables."  The firewall was organized around sets of IP addresses.  The pf language also makes it natural to match multiple ports on the same line.  So a set of rules like:
  • allow everybody to access 192.168.1.2 on port 80
  • allow everybody to access 192.168.1.2 on port 443
  • allow everybody to access 192.168.1.3 on port 80
  • allow everybody to access 192.168.1.3 on port 443
boils down, in pf, to:
  • web hosts are 192.168.1.2 and 192.168.1.3
  • allow everybody to access web hosts on ports 80 and 443
This is a big win for firewall manageability for the simple reason that lots of systems on your network are likely to be listening on the same port.  Think of the SSH or RDP (terminal services) ports for the most dramatic example.

So, why did this kind of grouping only occur to me after seeing Theron's work and the pf table feature?  After some head scratching I realize that the table feature provides similar grouping functionality to grouping by network.  Without the table feature I could still have a rule like "allow everybody to access ports 80 and 443 on the DMZ network."  Two realizations followed:  1. firewall features that allow increased granularity allow you to make some kinds of simplifications to your firewall without security trade-offs.  2. you can simplify a firewall by making security trade-offs.

I champion simplicity for the sake of maintainability, but also for the sake of security.  I have seen so many mistakes in firewall rules that were made precisely because the firewall was too complex for a human being to wrap their mind around.  What I'm saying is: you should make some security trade-offs simply for the sake of readability and maintainability but sometimes when you loosen your firewall rules for the sake of maintainability you will gain as much security back through simplicity as you lost because of reduced granularity.

Seeing the benefits of grouping servers that run the same protocols the first thought I had was to try to group clients based on what services they need access to.  To make any headway here, I believe you have to define a conceptual policy.  One pattern I've seen in firewalls that I have configured can be expressed in terms of security levels: Admins have the highest level of access, developers need more access than business users, some business partner needs needs special access beyond what everyone else on the Internet needs.  So I have the security levels ordered in sensitivity from lowest to highest: INTERNET, PEER, BUSINESS, DEVELOPER, ADMIN.  I often need to give a client who has access to one security level access to all lower security levels as well.  For example, admin users might need to SSH into all systems.  But they probably need to be able to view the corporate web page too, just like any user from the Internet.  A trade-off I'm pretty comfortable with is to make that a blanket policy:  whatever security level a client has access to, they are allowed to access everything of a lower security level as well.  Let's call this a cascading firewall policy.

Another example of a policy simplification I have developed based on a trade-off comes in the context of a firewall on a WAN between various peers with no access to the Internet and a fairly high expectation of security.  I started with pin-hole rules, each rule matching on all of client, client port, server and server port.  Even with a small number of peers this got out of hand and became unmanageable.  A compromise I'm comfortable with is to list all known clients (based on IP and port) on my network or on peer networks, list all known servers (based on IP and port) on my network or on a peer network and pass any traffic from a known client to a known server.  The compromise is that even if peer A only needs to see my server B and peer C only needs to see my server D, peer C with this policy peer C will be able to see server A and peer A will be able to see server D.  I'm cool with that.

One firewall system can implement multiple conceptually different firewalls simultaneously.  For example if I have a firewall between my networks and the Internet, that firewall is applying the policies I want to apply to my outbound traffic, how I want to control my network's access to the Internet, as well as the policies I want to apply to my inbound traffic, how I want to control the Internet's access to my network.  I also need rules to secure the firewall system itself and am likely to have rules to control access between different internal networks.  It's easy to create a lot of complexity trying to treat each of these conceptual sub-firewalls differently.  It's also tempting to deploy multiple firewall systems.  I'm always trying to find ways to eliminate the need for extra systems; I advocate piling as much firewall logic into one place, thinking about the patterns that emerge and simplifying the combined firewall as much as is practical.

Naming is an important part of readability which is in turn an important part of maintainability.  An easy win I made recently was to start using text protocol names.  Even though I know the numeric port numbers better than the protocol names in many cases, looking at a firewall that specifies "ssh" rather than "22" allows me to skip some mental indirection and makes reading the rules easier.   Other admins with less background in networking will be able to understand that firewall rule without recourse to a google search for "port 22".  When I know a port but not the protocol name I now grep /etc/services to so that I can use something a little more human readable. Using host names instead of ip addresses poses some problems. Firewall tools look up dns names when they start up which means your firewall can get out of sync with your DNS servers.  For now I'm still using IP addresses in my firewall rules (and I've seen advice to do so in several places).  If I can develop a process to ensure that my firewall is restarted every time DNS entries are updated, I will start using host names in firewall rules.  In iptables scripts, which I usually implement in BASH, I've tried setting variables as a way of associating pet names with ip addresses.  That approach hasn't been very successful, but perhaps the trouble I had was because of other complexities in that particular firewall. 

Iptables firewalls are slightly lower level than pf firewalls.  There are still plenty of features to simplify your firewalls that you should be sure you are aware of.  Using the core iptables features, you can match a port range (4000:4010) in the --dport option just like you would match a single port.  You can also match a network (192.168.0.0/26) in the -d option just like you would match a single IP.  You can use the custom chain feature to test against a list of hosts -- this is a clunky way to work around the lack of a feature equivalent to pf's table feature.  In fact iptables has such a feature if you can use the ipset command.  Sadly, the kernel support for this isn't even available as a pre-built module on my Ubuntu 10.4 laptop.  Never mind my Centos 5 servers. The iprange module allows you to match against ranges of ip addresses with match options like this:

-m iprange -p tcp --src-range 192.168.1.5-192.168.2.3

The smallest CIDR network that I could match that would include that whole range would give a match like this, which matches about 750 more hosts than I want:

-d 192.168.0.0/22

The multiport module allows you to match against lists of ports like this (note the "s" on the --dports switch):

-m multiport -p tcp --dports http,https

All of the firewall tools I'm aware of stick with the convention of ordered lists of matching rules.  Putting iptables rules in a shell script allows you to mix it up a little, but is still a procedural approach to firewall configuration.  Influenced by projects like puppet, I want my firewall configs to be declarative.  I have written a usable prototype application I'm calling gypsum that takes a YAML document with a particular structure (see the example policy simplepolicy.yml) as input and can spit out iptables (see the example output simplepolicy.sh) rules suitable for running as a shell script.  The top level of the YAML document is a list of access levels.  Each access level has a list of servers and a list of clients.  Gypsum firewalls match packets first by client, continuing checks in a chain specific to that access level where packets are accepted if they match the servers in that access level, otherwise the checks continue in a table specific to the next lower access level.  In this way the policy cascades from the highest security level down to the lowest.  Note that the highest security level is the one at the end of the file.  To further clarify: the clients in the highest security level have access to every server specified in the entire firewall and the servers in the highest security level are accessible by the fewest clients.  Conversely, the clients in the lowest security level have access only to the servers in that lowest security level while every client listed in any access level in the firewall has access to that lowest security level.



While iptables supports lots of different match options, gypsum only matches using IP's.  Things I've written by hand tend to match against other things as well, like system network interfaces.  Using only IP's keeps all the matching logic in one name space.  I think that is a desirable quality for maintainability.  I suspect it will also lead to better flexibility in some situations.  For example, I can move network cards around without having to change my firewall code.  Similarly, in the absence of NAT, I could deploy the same matching logic to each host on my network and run host based firewalls all with one configuration.  I have long been a strong detractor of host based firewalls.  If the configuration of host based firewalls could be centralized I would feel a lot more positively about them.

You can see that gypsum is designed first to implement the less restrictive firewall with multiple access levels I describe above.  I thought at first that I would have to change it significantly if I wanted to be able to implement the more restrictive peers on a WAN example I described next.  It turns out that I can use gypsum as is by writing a gypsum configuration with only one access level and putting all the servers and clients together whether they are on my network or on a peer network.

So now you know where I am in my quest to simplify firewall rules.  I've looked at ways that firewall software features can be used to reduce complexity — approaches that I've had success with in grouping  and consolidating firewall rules.  I've talked about higher level policies I've used to make security trade-offs that I can be comfortable with.  I've shown you gypsum, my prototypical declarative firewall configuration tool.  I would love to hear your thoughts on how to make simpler firewalls.  It's a subject I continue to brood over.
comments powered by Disqus