Slightly stricter

For a slightly more structured and complete setup, we start by denying everything and then allowing only those things we know that we need[1]. This gives us the opportunity to introduce two of the features which make PF such a wonderful tool - lists and macros.

We'll make some changes to /etc/pf.conf, starting with

block all

Then we back up a little. Macros need to be defined before use:

tcp_services = "{ ssh, smtp, domain, www, pop3, auth, pop3s }"
udp_services = "{ domain }"

Now we've demonstrated several things at once - what macros look like, we've shown that macros may be lists, and that PF understands rules using port names equally well as it does port numbers. The names are the ones listed in /etc/services. This gives us something to put in our rules, which we edit slightly to look like this:

block all
pass out proto tcp to port $tcp_services
pass proto udp to port $udp_services

Please remember to add keep state to these rules if your system has a PF version older than OpenBSD 4.1.

At this point some of us will point out that UDP is stateless, but PF actually manages to maintain state information despite this. When you ask a name server about a domain name, it is reasonable to assume that you probably want to receive the answer. Retaining state information about your UDP traffic achieves this.

Since we've made changes to our pf.conf file, we load the new rules:

$ doas pfctl -f /etc/pf.conf

and the new rules apply. If there are no syntax errors, pfctl will not output any messages during the rule load. The -v flag will produce more verbose pfctl output.

If you have made extensive changes to your rule set, you may want to check the rules before attempting to load them. The command to do this is, pfctl -nf /etc/pf.conf. The -n option causes the rules to be interpreted only without loading the rules. This gives you an opportunity to correct any errors. Under any circumstances the last valid rule set loaded will be in force until you either disable PF or load a new rule set.

That is worth noting: When loading a new rule set, the last valid rule set stays loaded until the new one is fully parsed and loaded, and PF switches directly from one to the other. There is no intermediate stage with no rules loaded or a mixture of the two rule sets.

Notes

[1]

You may ask why do I write the rule set to default deny? The short answer is, it gives you better control at the expense of some thinking. The point of packet filtering is to take control, not to run catch-up with what the bad guys do. Marcus Ranum has written a very entertaining and informative article about this, The Six Dumbest Ideas in Computer Security, which comes highly recommended. It is a good read.