Chapter 2. PF Configuration Basics

PF Configuration Basics

In this chapter, we’ll create a very simple setup with PF. We’ll begin with the simplest configuration possible: a single machine configured to communicate with a single network. This network could very well be the Internet.

Your two main tools for configuring PF are your favorite text editor and the pfctl command-line administration tool. PF configurations, usually stored in /etc/pf.conf, are called rule sets because each line in the configuration file is a rule that helps determine what the packet-filtering subsystem should do with the network traffic it sees. In ordinary, day-to-day administration, you edit your configuration in the /etc/pf.conf file and then load your changes using pfctl. There are Web interfaces for PF administration tasks, but they’re not part of the base system. The PF developers aren’t hostile toward these options, but they’ve yet to see a graphical interface for configuring PF that’s clearly preferable to editing pf.conf and using pfctl commands.

The First Step: Enabling PF

Before you can get started on the fun parts of shaping your network with PF and related tools, you need to make sure that PF is available and enabled. The details depend on your specific operating system: OpenBSD, FreeBSD, or NetBSD. Check your setup by following the instructions for your operating system and then move on to A Simple PF Rule Set: A Single, Stand-Alone Machine.

The pfctl command is a program that requires higher privilege than the default for ordinary users. In the rest of this book, you’ll see commands that require extra privilege prefixed with sudo. If you haven’t started using sudo yet, you should. sudo is in the base system on OpenBSD. On FreeBSD, DragonFly BSD, and NetBSD, it’s within easy reach via the ports system or pkgsrc system, respectively, as security/sudo.

Here are a couple general notes regarding using pfctl:

  • The command to disable PF is pfctl -d. Once you’ve entered that command, all PF-based filtering that may have been in place will be disabled, and all traffic will be allowed to pass.

  • For convenience, pfctl can handle several operations on a single command line. To enable PF and load the rule set in a single command, enter the following:

    $ sudo pfctl -ef /etc/pf.conf

Setting Up PF on OpenBSD

In OpenBSD 4.6 and later, you don’t need to enable PF because it’s enabled by default with a minimal configuration in place.[9] If you were watching the system console closely while the system was starting up, you may have noticed the pf enabled message appear soon after the kernel messages completed.

If you didn’t see the pf enabled message on the console at startup, you have several options to check that PF is indeed enabled. One simple way to check is to enter the command you would otherwise use to enable PF from the command line:

$ sudo pfctl -e

If PF is already enabled, the system responds with this message:

pfctl: pf already enabled

If PF isn’t enabled, the pfctl -e command will enable PF and display this:

pf enabled

In versions prior to OpenBSD 4.6, PF wasn’t enabled by default. You can override the default by editing your /etc/rc.conf.local file (or creating the file, if it doesn’t exist). Although it isn’t necessary on recent OpenBSD versions, it doesn’t hurt to add this line to your /etc/rc.conf.local file:

pf=YES           # enable PF

If you take a look at the /etc/pf.conf file in a fresh OpenBSD installation, you get your first exposure to a working rule set.

The default OpenBSD pf.conf file starts off with a set skip on lo rule to make sure traffic on the loopback interface group isn’t filtered in any way. The next active line is a simple pass default to let your network traffic pass by default. Finally, an explicit block rule blocks remote X11 traffic to your machine.

As you probably noticed, the default pf.conf file also contains a few comment lines starting with a hash mark (#). In those comments, you’ll find suggested rules that hint at useful configurations, such as FTP passthrough via ftp-proxy (see Chapter 3) and spamd, the OpenBSD spam-deferral daemon (see Chapter 6). These items are potentially useful in various real-world scenarios, but because they may not be relevant in all configurations, they are commented out in the file by default.

If you look for PF-related settings in your /etc/rc.conf file, you’ll find the setting pf_rules=. In principle, this lets you specify that your configuration is in a file other than the default /etc/pf.conf. However, changing this setting is probably not worth the trouble. Using the default setting lets you take advantage of a number of automatic housekeeping features, such as automatic nightly backup of your configuration to /var/backups.

On OpenBSD, the /etc/rc script has a built-in mechanism to help you out if you reboot with either no pf.conf file or one that contains an invalid rule set. Before enabling any network interfaces, the rc script loads a rule set that allows a few basic services: SSH from anywhere, basic name resolution, and NFS mounts. This allows you to log in and correct any errors in your rule set, load the corrected rule set, and then go on working from there.

Setting Up PF on FreeBSD

Good code travels well, and FreeBSD users will tell you that good code from elsewhere tends to find its way into FreeBSD sooner or later. PF is no exception, and from FreeBSD 5.2.1 and the 4.x series onward, PF and related tools became part of FreeBSD.

If you read through the previous section on setting up PF on OpenBSD, you saw that on OpenBSD, PF is enabled by default. That isn’t the case on FreeBSD, where PF is one of three possible packet-filtering options. Here, you need to take explicit steps to enable PF, and compared to OpenBSD, it seems that you need a little more magic in your /etc/rc.conf. A look at your /etc/defaults/rc.conf file shows that the FreeBSD default values for PF-related settings are as follows:

pf_enable="NO"                 # Set to YES to enable packet filter (PF)
pf_rules="/etc/pf.conf"        # rules definition file for PF
pf_program="/sbin/pfctl"       # where pfctl lives
pf_flags=""                    # additional flags for pfctl
pflog_enable="NO"              # set to YES to enable packet filter logging
pflog_logfile="/var/log/pflog" # where pflogd should store the logfile
pflog_program="/sbin/pflogd"   # where pflogd lives
pflog_flags=""                 # additional flags for pflogd
pfsync_enable="NO"             # expose pf state to other hosts for syncing
pfsync_syncdev=""              # interface for pfsync to work through
pfsync_ifconfig=""             # additional options to ifconfig(8) for pfsync

Fortunately, you can safely ignore most of these—at least for now. The following are the only options that you need to add to your /etc/rc.conf configuration:

pf_enable="YES"       # Enable PF (load module if required)
pflog_enable="YES"    # start pflogd(8)

There are some differences between FreeBSD releases with respect to PF. Refer to the FreeBSD Handbook available from http://www.freebsd.org/—specifically the PF section of the “Firewalls” chapter—to see which information applies in your case. The PF code in FreeBSD 9 and 10 is equivalent to the code in OpenBSD 4.5 with some bug fixes. The instructions in this book assume that you’re running FreeBSD 9.0 or newer.

On FreeBSD, PF is compiled as a kernel-loadable module by default. If your FreeBSD setup runs with a GENERIC kernel, you should be able to start PF with the following:

$ sudo kldload pf
$ sudo pfctl -e

Assuming you have put the lines just mentioned in your /etc/rc.conf and created an /etc/pf.conf file, you could also use the PF rc script to run PF. The following enables PF:

$ sudo /etc/rc.d/pf start

And this disables the packet filter:

$ sudo /etc/rc.d/pf stop

Note

On FreeBSD, the /etc/rc.d/pf script requires at least a line in /etc/rc.conf that reads pf_enable="YES" and a valid /etc/pf.conf file. If either of these requirements isn’t met, the script will exit with an error message. There is no /etc/pf.conf file in a default FreeBSD installation, so you’ll need to create one before you reboot the system with PF enabled. For our purposes, creating an empty /etc/pf.conf with touch will do, but you could also work from a copy of the /usr/share/examples/pf/pf.conf file supplied by the system.

The supplied sample file /usr/share/examples/pf/pf.conf contains no active settings. It has only comment lines starting with a # character and commented-out rules, but it does give you a preview of what a working rule set will look like. For example, if you remove the # sign before the line that says set skip on lo to uncomment the line and then save the file as your /etc/pf.conf, your loopback interface group will not be filtered once you enable PF and load the rule set. However, even if PF is enabled on your FreeBSD system, we haven’t gotten around to writing an actual rule set, so PF isn’t doing much of anything and all packets will pass.

As of this writing (August 2014), the FreeBSD rc scripts don’t set up a default rule set as a fallback if the configuration read from /etc/pf.conf fails to load. This means that enabling PF with no rule set or with pf.conf content that is syntactically invalid will leave the packet filter enabled with a default pass all rule set.

Setting Up PF on NetBSD

On NetBSD 2.0, PF became available as a loadable kernel module that could be installed via packages (security/pflkm) or compiled into a static kernel configuration. In NetBSD 3.0 and later, PF is part of the base system. On NetBSD, PF is one of several possible packet-filtering systems, and you need to take explicit action to enable it.

Some details of PF configuration have changed between NetBSD releases. This book assumes you are using NetBSD 6.0 or later.[10]

To use the loadable PF module for NetBSD, add the following lines to your /etc/rc.conf to enable loadable kernel modules, PF, and the PF log interface, respectively.

lkm="YES" # do load kernel modules
pf=YES
pflogd=YES

To load the pf module manually and enable PF, enter this:

$ sudo modload /usr/lkm/pf.o
$ sudo pfctl -e

Alternatively, you can run the rc.d scripts to enable PF and logging, as follows:

$ sudo /etc/rc.d/pf start
$ sudo /etc/rc.d/pflogd start

To load the module automatically at startup, add the following line to /etc/lkm.conf:

/usr/lkm/pf.o - - - - BEFORENET

If your /usr filesystem is on a separate partition, add this line to your /etc/rc.conf:

critical_filesystems_local="${critical_filesystems_local} /usr"

If there are no errors at this point, you have enabled PF on your system, and you’re ready to move on to creating a complete configuration.

The supplied /etc/pf.conf file contains no active settings; it has only comment lines starting with a hash mark (#) and commented-out rules. However, it does give you a preview of what a working rule set will look like. For example, if you remove the hash mark before the line that says set skip on lo to uncomment it and then save the file, your loopback interface will not be filtered once you enable PF and load the rule set. However, even if PF is enabled on your NetBSD system, we haven’t gotten around to writing an actual rule set, so PF isn’t doing much of anything but passing packets.

NetBSD implements a default or fallback rule set via the file /etc/defaults/ pf.boot.conf. This rule set is intended only to let your system complete its boot process in case the /etc/pf.conf file doesn’t exist or contains an invalid rule set. You can override the default rules by putting your own customizations in /etc/pf.boot.conf.

A Simple PF Rule Set: A Single, Stand-Alone Machine

Mainly to have a common, minimal baseline, we will start building rule sets from the simplest possible configuration.

A Minimal Rule Set

The simplest possible PF setup is on a single machine that will not run any services and talks to only one network, which may be the Internet.

We’ll begin with an /etc/pf.conf file that looks like this:

block in all
pass out all keep state

This rule set denies all incoming traffic, allows traffic we send, and retains state information on our connections. PF reads rules from top to bottom; the last rule in a rule set that matches a packet or connection is the one that is applied.

Here, any connection coming into our system from anywhere else will match the block in all rule. Even with this tentative result, the rule evaluation will continue to the next rule (pass out all keep state), but the traffic will not even match the first criterion (the out direction) in this rule. With no more rules to evaluate, the status will not change, and the traffic will be blocked. In a similar manner, any connection initiated from the machine with this rule set will not match the first rule (once again, the wrong direction) but will match the second rule, which is a pass rule, and the connection is allowed to pass.

We’ll examine the way that PF evaluates rules and how ordering matters in a bit more detail in Chapter 3, in the context of a slightly longer rule set.

For any rule that has a keep state part, PF keeps information about the connection, including various counters and sequence numbers, as an entry in the state table. The state table is where PF keeps information about existing connections that have already matched a rule, and new packets that arrive are compared to existing state table entries to find a match first. Only when a packet doesn’t match any existing state will PF move on to a full rule set evaluation, checking whether the packet matches a rule in the loaded rule set. We can also instruct PF to act on state information in various ways, but in a simple case like this, our main goal is to allow return traffic for connections we initiate to return to us.

Note that on OpenBSD 4.1 and later, the default for pass rules is to keep state information,[11] and we no longer need to specify keep state explicitly in a simple case like this. This means the rule set could be written like this:

# minimal rule set, OpenBSD 4.1 onward keeps state by default
block in all
pass out all

In fact, you could even leave out the all keyword here if you like.

The other BSDs have mostly caught up with this change by now, and for the rest of this book, we’ll stick to the newer rules, with an occasional reminder in case you are using an older system.

It goes pretty much without saying that passing all traffic generated by a specific host implies that the host in question is, in fact, trustworthy. This is something you do only if this is a machine you know you can trust.

When you’re ready to use this rule set, load it with the following:

$ sudo pfctl -ef /etc/pf.conf

The rule set should load without any error messages or warnings. On all but the slowest systems, you should be returned to the $ prompt immediately.

Testing the Rule Set

It’s always a good idea to test your rule sets to make sure they work as expected. Proper testing will become essential once you move on to more complicated configurations.

To test the simple rule set, see whether it can perform domain name resolution. For example, you could see whether $ host nostarch.com returns information, such as the IP address of the host nostarch.com and the host-names of that domain’s mail exchangers. Or just see whether you can surf the Web. If you can connect to external websites by name, the rule set allows your system to perform domain name resolution. Basically, any service you try to access from your own system should work, and any service you try to access on your system from another machine should produce a Connection refused message.

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 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’ll 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 we check whether filtering is part of a problem we’re 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’ll add rules that cut our traffic more slack, but we’ll 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’ll 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’ll now edit slightly to look like this:

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

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 don’t 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’s 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 shouldn’t 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’s actually loaded.

Checking Your Rules

If you’ve 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’ll 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’ll run into trouble—your firewall might be in some kind of intermediate state that doesn’t match either the before or after state. That’s 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. This is often referred to as atomic rule set load and means that once a valid rule set has been loaded, there’s 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’ve 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 because it effectively puts your packet filter in a pass all mode, which in turn both opens the door to any comers and runs the risk of disrupting useful traffic while you’re 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 whether the rules you’ve written behave as expected. Testing name resolution with a command such as $ host nostarch.com, as we did earlier, should still work. However, it’s better to choose a domain you haven’t accessed recently, such as one for a political party you wouldn’t 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 forth—on any remote system should fail. And, as with our simple rule set, your system should refuse all connections that don’t match existing state table entries; only return traffic for connections initiated by this machine will be allowed in.

Displaying Information About Your System

The tests you’ve performed on your rule sets should have shown that PF is running and that your rules are behaving as expected. There are several ways to keep track of what happens in your running system. One of the more straightforward ways of extracting information about PF is to use the already familiar pfctl program.

Once PF is enabled and running, the system updates various counters and statistics in response to network activity. To confirm that PF is running and to view statistics about its activity, you can use pfctl -s, followed by the type of information you want to display. A long list of information types is available (see man 8 pfctl and look for the -s options). We’ll get back to some of those display options in Chapter 9 and go into further detail about some of the statistics they provide in Chapter 10, when we use the data to optimize the configuration we’re building.

The following shows an example of just the top part of the output of pfctl -s info (taken from my home gateway). The high-level information that indicates the system actually passes traffic can be found in this upper part.

$ sudo pfctl -s info
Status: Enabled for 24 days 12:11:27                Debug: err

Interface Stats for nfe0               IPv4               IPv6
  Bytes In                      43846385394                  0
  Bytes Out                     20023639992                 64
  Packets In
    Passed                          49380289                 0
    Blocked                            49530                 0
  Packets Out
    Passed                          45701100                 1
    Blocked                             1034                 0

State Table                            Total              Rate
  current entries                        319
  searches                         178598618            84.3/s
  inserts                            4965347             2.3/s
  removals                           4965028             2.3/s

The first line of the pfctl output indicates that PF is enabled and has been running for a little more than three weeks, which is equal to the time since I last performed a system upgrade that required a reboot.

The Interface Stats part of the display is an indication that the system’s administrator has chosen one interface (here, nfe0) as the log interface for the system and shows the bytes in and out handled by the interface. If no log interface has been chosen, the display is slightly different. Now would be a good time to check what output your system produces. The next few items are likely to be more interesting in our context, showing the number of packets blocked or passed in each direction. This is where we find an early indication of whether the filtering rules we wrote are catching any traffic. In this case, either the rule set matches expected traffic well, or we have fairly well-behaved users and guests, with the number of packets passed being overwhelmingly larger than the number of packets blocked in both directions.

The next important indicator of a working system that’s processing traffic is the block of State Table statistics. The state table current entries line shows that there are 319 active states or connections, while the state table has been searched (searches) for matches to existing states on average a little more than 84 times per second, for a total of just over 178 million times since the counters were reset. The inserts and removals counters show the number of times states have been created and removed, respectively. As expected, the number of insertions and removals differs by the number of currently active states, and the rate counters show that for the time since the counters were last reset, the rate of states created and removed matches exactly up to the resolution of this display.

The information here is roughly in line with the statistics you should expect to see on a gateway for a small network configured for IPv4 only. There’s no reason to be alarmed by the packet passed in the IPv6 column. OpenBSD comes with IPv6 built in. During network interface configuration, by default, the TCP/IP stack sends IPv6 neighbor solicitation requests for the link local address. In a normal IPv4-only configuration, only the first few packets actually pass, and by the time the PF rule set from /etc/ pf.conf is fully loaded, IPv6 packets are blocked by the block all default rule. (In this example, they don’t show up in nfe0’s statistics because IPv6 is tunneled over a different interface.)

Looking Ahead

You should now have a machine that can communicate with other Internet-connected machines, using a very basic rule set that serves as a starting point for controlling your network traffic. As you progress through this book, you’ll learn how to add rules that do various useful things. In Chapter 3, we’ll extend the configuration to act as a gateway for a small network. Serving the needs of several computers has some consequences, and we’ll look at how to let at least some ICMP and UDP traffic through—for your own troubleshooting needs if nothing else.

In Chapter 3, we’ll also consider network services that have consequences for your security, like FTP. Using packet filtering intelligently to handle services that are demanding, security-wise, is a recurring theme in this book.



[9] If you’re setting up your first PF configuration on an OpenBSD version earlier than this, the best advice is to upgrade to the most recent stable version. If for some reason you must stay with the older version, it might be useful to consult the first edition of this book as well as the man pages and other documentation for the specific version you’re using.

[10] For instructions on using PF in earlier releases, see the documentation for your release and look up supporting literature listed in Appendix A of this book.

[11] In fact, the new default corresponds to keep state flags S/SA, ensuring that only initial SYN packets during connection setup create state, eliminating some puzzling error scenarios. To filter statelessly, you can specify no state for the rules where you don’t want to record or keep state information. On FreeBSD, OpenBSD 4.1–equivalent PF code was merged into version 7.0. If you’re using a PF version old enough that it does not have this default, it is a very strong indicator that you should consider upgrading your operating system as soon as feasible.

[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 play 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.21.248.162