This guide is written for the person very new to firewalling. Please realize that the sample firewall we build should not be considered appropriate for actual use. I just try to cover a few basics, that took me awhile to grasp from the better known (and more detailed) documentation referenced below
It's my hope that this guide will not only get you started, but give you enough of a grasp of using pf so that you will then be able to go to those more advanced guides and perfect your firewalling skills.
The pf packet filter was developed for OpenBSD but is now included in FreeBSD, which is where I've used it. Having it run at boot and the like is covered in the various documents, however I'll quickly run through the steps for FreeBSD.
First one adds to /etc/rc.conf
pf_enable="YES" pf_rules="/etc/pf.conf" pflog_enable="YES" pflog_logfile="/var/log/pflog" |
This will start the pf filter at boot. We'll cover starting it while running after we put in some rules.
visudo -f /usr/local/etc/sudoers |
If your user name is john, then add a line (below any other lines giving you less privileges--sudo processes the sudoers file from top to bottom)
john ALL= NOPASSWD: /sbin/pfctl |
Now we edit the crontab.
crontab -e |
Then, in your crontab add
*/10 * * * * /usr/local/bin/sudo /sbin/pfctl -d |
I dislike the NOPASSWD option, as it makes it easier to make serious mistakes--however, when I'm testing a bunch of pf rules, I usually put it in there as I'm constantly using pfctl. When done, and the ruleset works, I remove the NOPASSWD part.
Ok, now that we've ensured we'll be able to get back in the box, let's create an /etc/pf.conf file. In FreeBSD, the file already exists. What I usually do is start in my home directory, create the rules, and then test them, loading them with sudo. Then, when it's working the way I want it to work, I backup the default pf.conf and copy it over.
mv /etc/pf.conf /etc/pf.conf.bak cp pf.conf /etc |
(As we haven't yet created the file, don't do it yet) :).
tcp_pass = "{ 80 22 }" |
We've created a macro. Later, when we make rules, rather than having to make one rule for port 80 and a second for port 22 we can simply do something like
pass out proto tcp to port $tcp_pass |
(Note that you need the $ in front of the macro's name.)
Then, if we decide we want to add port 123, the Network Time Protocol, rather than write a new rule, we can simply add 123 to our macro. We can also realize we forgot to allow it to get email so we'll add all three, pop3 to get email, smtp to send it and ntp to connect to time servers.
tcp_pass= "{ 80 22 25 110 123 }" |
The pf filter will read the rules we create from top to bottom. So, we start with a rule to block everything
block all |
This keeps the box pretty secure, but we won't be able to do anything. So, in order to access web pages, allow the Network Time Protocol to connect to a time server and to ssh to an outside machine, we can use the macro we defined. Our pf.conf looks like this.
tcp_pass = "{ 80 22 25 110 123 }" block all pass out on fxp0 proto tcp to any port $tcp_pass keep state |
Let's look at what we've done so far. First, we defined a macro. We used port numbers. While one can mix and match if they want, for clarity's sake, we try to use the same pattern for all ports. We can also put in commas if we want or the protocol name if it's defined in /etc/services. So, that macro could actually read
tcp_pass = "{ 80 ssh, ntp smtp 110}" |
and still work. However, one should strive for consistancy.
You can only use a name if the port is defined in /etc/services. For instance, if you decide to set ssh to only accept connections on port 1222, you would have to put 1222 in that macro, NOT ssh. Port 1222 is defined in /etc/services as nerv, the SNI R&D network, so if you check your rules with pfctl, it'll show that you have a rule to pass out to nerv. To avoid confusion, if you're going to use a non-standard port, use something that isn't listed in /etc/services.
You can also specify a range of ports with a colon. For instance, if I wanted to add samba, which uses ports 137, 138 and 139, I could have added 137:139.
Next, we block everything as our default. Now, we have to add rules to let things through. Remember, pf processes rules from top to bottom.
So, we are allowing out web traffic, ssh connections, sending mail, getting mail with pop3, and we're able to contact time servers.
So, we start with the action, pass. We're passing, not blocking. The order of syntax is important. I'm just giving the basic options here, again, this article can be considered a prep for more advanced tutorials.
pass out means we're allowing things out, not necessarily in. The next part, on fxp0 refers to the interface--in this case, a network card that in FreeBSD parlance is called fxp0. (An Intel card).
The to any part means that we're allowing it to go anywhere. Next, we specify the ports we're talking about--here we use our macro, tcp_pass, meaning we're allowing to the ports mentioned above.
The keep state part can be important. It means that once we've established the connection, pf is going to keep track of it, so the answer from, for example, a web page, doesn't have to go through checking each rule, but can just be opened. The same with pop3. One uses pop3 to contact a pop3 server (hrrm, obviously), and the server answers. As we have the keep state keywords, the server's answer can go right through once the connection is established.
Many of these are optional. For example, if we write pass on fxp0 rather than pass out on fxp0, then traffic will be allowed in both directions, in and out.
If you look through /etc/services, you'll see that some things, such as ipp, port 631 used with CUPS, use both tcp and udp. To deal with such things, we could insert another macro
udp_pass = "{ 631 }" |
We might find that sending email isn't working properly. Checking /etc/services we find that smtp can use udp.
grep smtp /etc/services smtp 25/tcp mail #Simple Mail Transfer smtp 25/udp mail #Simple Mail Transfer smtps 465/tcp #smtp protocol over TLS/SSL (was ssmtp) smtps 465/udp #smtp protocol over TLS/SSL (was ssmtp) |
However, with our new macro, this is simple--we simply add it to udp_pass
udp_pass = "{ 110 631 }" |
Now, let's add the udp_pass macro to our ruleset
tcp_pass = "{ 80 22 25 110 123 }" udp_pass = "{ 110 631 }" block all pass out on fxp0 proto tcp to any port $tcp_pass keep state pass out on fxp0 proto udp to any port $udp_pass keep state |
However, we find CUPS isn't working. A quick grep of ipp in /etc/services shows that it also uses tcp. So, we add 631 to our list of ports in the tcp_pass macro. Now we have
tcp_pass = "{ 80 22 25 110 123 631 }" udp_pass = "{ 110 631 }" block all pass out on fxp0 proto tcp to any port $tcp_pass keep state pass out on fxp0 proto udp to any port $udp_pass keep state |
The same holds true for any other ports that you've forgotten. For instance, these rules don't allow DNS, port 53. We grep domain in /etc/services, and see that it uses both tcp and udp so we add 53 to both macros
tcp_pass = "{ 80 22 25 53 110 123 631 }" udp_pass = "{ 53 110 631 }" |
In such cases, you can use the quick keyword. If a packet matches something in that line, it stops going through the rules and processes the packet. For example, let's say that you have a web server on your LAN behind a firewall, and you are sure that all requests for port 80 are coming from your internal network, so you want to quickly pass them through. (I can't see that being true in real life, but this is for an example of using quick).
pass in quick on fxp0 proto tcp to any port 80 keep state |
Put that above the other rules, right after the macro definitions. Now, requests coming in for the LAN webserver will be passed right though (note that in this case it was pass in) without being matched against the rest of the ruleset.
table <local> { 192.168.8.0/24, 192.168.9.0/24 } |
Insert that line above your rules. Now, to allow everything from those two networks (and we'll make use of the quick keyword as well) add a rule. Since we're using quick we want this rule towards the top. If it was at the end, there's no point in using the quick keyword, for the packets would have already been matched against every rule above this one
pass in quick from <local> to any keep state |
sudo pfctl -d |
To put the packet filter we've created into effect, assuming you are user john and you've created it in your home directory, as john
sudo pfctl -ef pf.conf |
The -f stands for file. Say we've checked it out and we don't like it so we want to go back to our default rules, in /etc/
sudo pfctl -Rf /etc/pf.conf |
There are a variety of uses for pfctl. Doing pfctl -s info gives you a quick look at what's going on. Doing pfctl -vs rules shows you your rules and what's happening, for example
pass out proto tcp from any to any port = http keep state [ Evaluations: 96 Packets: 906 Bytes: 496407 States: 0 ] pass out proto tcp from any to any port = pop3 keep state [ Evaluations: 96 Packets: 514 Bytes: 71260 States: 0 ] |
The man page gives a complete list.
Now it's time to test this. It's early morning, and I'm not thinking that clearly. Hopefully, however, I've remembered to add myself to sudoers as being able to run pfctl without a password and remember to quickly set up the cronjob so that if I make a mistake, pf will be disabled shortly. (VERY necessary if you're testing this on a machine to which you don't have physical access). So, I put these rules which I've saved into a file pf.conf in my home directory into operation.
sudo pfctl -ef pf.conf |
I find that I've locked myself out of the box. Oops. I forgot to allow ssh connections in, and the connection that I was using has just been blocked.
So, I wait a few minutes for the crontab to disable pf and log back into the remote machine. This time, I remember to add a rule
pass in proto tcp to port 22 keep state |
Now, these rules seem to be working. So, I copy them over to /etc/pf.conf.
sudo cp pf.conf /etc/ sudo pftcl -Rf /etc/pf.conf sudo pftcl -s rules |
Listing the rules will show me that it's doing what I want to do.
Now, of course, I remove the cronjob. If you're going to be experimenting with your rules frequently, and will be needing it again, just do crontab -e and comment the line out with a #.
There is also the scrub keyword. Again, while this is more fully explained in the references below in a nutshell it normalizes fragmented packets. The usual line is
srub in all |
This rule must also go above the filtering rules (and above the rdr to redirect ftp if you use it.) It is the "normalization" referred to in that error message. At the top of the ruleset you define your macros, tabes and the like. Then would come the scrub rule, then the rdr rule, then your filtering rules (the group that in our example begins with block all). Using the scrub keyword can help protect against certain kinds of attacks, and it's a good line to have.
In FreeBSD, you must once again add device pf to your kernel. (For awhile, it wasn't.) Check /usr/src/sys/conf/NOTES to see the various options, but I just have
device pf device pflog device pfsync |
I also have
options ALTQ |
in my kernel. Although I don't make use of queuing, otherwise, one gets a warning every time they reload their pf rules that ALTQ isn't enabled in the kernel.
If you use the module, it assumes the presence of device bpf, INET and INET6 in the kernel. (See the handbook page on pf.)
This page is only meant to be an introduction. However, if you've understood these simple rulesets, you're probably ready to look at the more sophisticated tutorials. The two that I use most frequently are the OpenBSD PF