Slightly Stricter: Using Lists and Macros for Readability

The rule set in the previous section is an extremely simple one—probably too simplistic for practical use. But it's a useful starting point to build from to create a slightly more structured and complete setup. We’ll start by denying all services and protocols, and then allow only those that we know that we need[12] using lists and macros for better readability and control.

A list is simply two or more objects of the same type that you can refer to in a rule set, such as this:

pass proto tcp to port { 22 80 443 }

Here, { 22 80 443 } is a list.

A macro is a pure readability tool. If you have objects that you will refer to more than once in your configuration, such as an IP address for an important host, it could be useful to define a macro instead. For example, you might define this macro early in your rule set:

external_mail = 192.0.2.12

Then you could refer to that host as $external_mail later in the rule set:

pass proto tcp to $external_mail port 25

These two techniques have great potential for keeping your rule sets readable, and as such, they are important factors that contribute to the overall goal of keeping you in control of your network.

A Stricter Baseline Rule Set

Up to this point, we’ve been rather permissive with regard to any traffic we generate ourselves. A permissive rule set can be very useful while we check that basic connectivity is in place, or to check whether filtering is part of a problem we are seeing. Once the “Do we have connectivity?” phase is over, it’s time to start tightening up to create a baseline that keeps us in control.

To begin, add the following rule to /etc/pf.conf:

block all

This rule is completely restrictive and will block all traffic in all directions. This is the initial baseline filtering rule that we’ll use in all complete rule sets over the next few chapters. We basically start from zero, with a configuration where nothing is allowed to pass. Later on, we will add rules that cut our traffic some more slack, but we will do so incrementally and in a way that keeps us firmly in control.

Next, we’ll define a few macros for later use in the rule set:

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

Here, you can see how the combination of lists and macros can be turned to our advantage. Macros can be lists, and as demonstrated in the example, PF understands rules that use the names of services as well as port numbers, as listed in your /etc/services file. We will take care to use all these elements and some further readability tricks as we tackle complex situations that require more elaborate rule sets.

Having defined these macros, we can use them in our rules, which we will now edit slightly to look like this:

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

Note

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

The strings $tcp_services and $udp_services are macro references. Macros that appear in a rule set are expanded in place when the rule set loads, and the running rule set will have the full lists inserted where the macros are referenced. Depending on the exact nature of the macros, they may cause single rules with macro references to expand into several rules. Even in a small rule set like this, the use of macros makes the rules easier to grasp and maintain. The amount of information that needs to appear in the rule set shrinks, and with sensible macro names, the logic becomes clearer. To follow the logic in a typical rule set, more often than not, we do not need to see full lists of IP addresses or port numbers in place of every macro reference.

From a practical rule set maintenance perspective, it is important to keep in mind which services to allow on which protocol in order to keep a comfortably tight regime. Keeping separate lists of allowed services according to protocol is likely to be useful in keeping your rule set both functional and readable.

Reloading the Rule Set and Looking for Errors

After we’ve changed our pf.conf file, we need to load the new rules, as follows:

$ sudo pfctl -f /etc/pf.conf

If there are no syntax errors, pfctl should not display any messages during the rule load.

If you prefer to display verbose output, use the -v flag:

$ sudo pfctl -vf /etc/pf.conf

When you use verbose mode, pfctl should expand your macros into their separate rules before returning you to the command-line prompt, as follows:

$ sudo pfctl -vf /etc/pf.conf
tcp_services = "{ ssh, smtp, domain, www, pop3, auth, https, pop3s }"
udp_services = "{ domain }"
block drop all
pass out proto tcp from any to any port = ssh flags S/SA keep state
pass out proto tcp from any to any port = smtp flags S/SA keep state
pass out proto tcp from any to any port = domain flags S/SA keep state
pass out proto tcp from any to any port = www flags S/SA keep state
pass out proto tcp from any to any port = pop3 flags S/SA keep state
pass out proto tcp from any to any port = auth flags S/SA keep state
pass out proto tcp from any to any port = https flags S/SA keep state
pass out proto tcp from any to any port = pop3s flags S/SA keep state
pass proto udp from any to any port = domain keep state
$ _

Compare this output to the content of the /etc/pf.conf file you actually wrote. Our single TCP services rule is expanded into eight different ones: one for each service in the list. The single UDP rule takes care of only one service, and it expands from what we wrote to include the default options. Notice that the rules are displayed in full, with default values such as flags S/SA keep state applied in place of any options you do not specify explicitly. This is the configuration as it is actually loaded.

Checking Your Rules

If you have made extensive changes to your rule set, check them before attempting to load the rule set by using the following:

$ pfctl -nf /etc/pf.conf

The -n option tells PF to parse the rules only, without loading them—more or less as a dry run and to allow you to review and correct any errors. If pfctl finds any syntax errors in your rule set, it will exit with an error message that points to the line number where the error occurred.

Some firewall guides advise you to make sure that your old configuration is truly gone, or you will run into trouble—your firewall might be in some kind of intermediate state that does not match either the before or after state. That is simply not true when you’re using PF. The last valid rule set loaded is active until you either disable PF or load a new rule set. pfctl checks the syntax, and then loads your new rule set completely before switching over to the new one. Once a valid rule set has been loaded, there is no intermediate state with a partial rule set or no rules loaded. One consequence is that traffic that matches states that are valid in both the old and new rule set will not be disrupted.

Unless you have actually followed the advice from some of those old guides and flushed your existing rules (that is possible, using pfctl -F all or similar) before attempting to load a new one from your configuration file, the last valid configuration will remain loaded. In fact, flushing the rule set is rarely a good idea, since it effectively puts your packet filter in a pass all mode, and with a rather high risk of disrupting useful traffic while you are getting ready to load your rules.

Testing the Changed Rule Set

Once you have a rule set that pfctl loads without any errors, it’s time to see if the rules you have written behave as expected. Testing name resolution with a command such as $ host nostarch.com (as we did earlier) should still work, but choose a domain you have not accessed recently (such as one for a political party you would not consider voting for) to be sure that you’re not pulling DNS information from the cache.

You should be able to surf the Web and use several mail-related services, but due to the nature of this updated rule set, attempts to access TCP services other than the ones defined (SSH, SMTP, and so on) on any remote system should fail. And, as with our simple rule set, your system should refuse all connections that do not match existing state table entries; only return traffic for connections initiated by this machine will be allowed in.



[12] Why write the rule set to default deny? The short answer is that it gives you better control. 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 called “The Six Dumbest Ideas in Computer Security” (http://www.ranum.com/security/computer_security/editorials/dumb/index.html).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.35.255