Welcome back.
I'd like to thank you for your feedback on the first part of "Securing Small Networks with OpenBSD." You asked many interesting questions that prompted me to write another article in which I'll try to answer questions regarding the new packet filter, pf, introduced in OpenBSD 3.0.
pf?That's easy: Download and install OpenBSD 3.0 or 3.1 and it's there. :-) To control pf, use the pfctl tool.
*
* Start pf — pfctl -e
* Stop pf — pfctl -d
* Upload new pf rules — pfctl -R /etc/pf.conf
Upload new nat rules — pfctl –N /etc/nat.conf
As you can see, the names of the configuration files have changed as well: packet filtering rules are now stored in the pf.conf file located in the /etc directory. The network address translation rules are stored in the nat.conf file located in the same directory. When pfctl complains about syntax errors, use the -v option to display the rules as they are processed by pfctl. For example, when the packet filtering rules contain errors, use pfctl –v –R /etc/pf.conf | less to browse the output and locate lines with errors; then edit the configuration file and try uploading the new rules again.
Note that pfctl will complain if you try to upload new configuration rules while pf is not running. When that happens, start pf as described earlier and try again.
For more information about pfctl, read man pfctl.
ipf rules into pf rules?Administrators new to pf will be glad to know that its syntax is very similar to that of ipfilter. Simple rules can be translated without any changes whatsoever, while more complicated statements will have to be slightly adjusted to match the new syntax. This is only a small inconvenience, as the new rule syntax is easier to read and manage. In general, you can expect to halve the length of the configuration file while retaining all previous functionality.
Let's have a closer look at what changes have been made. First, a simple example using the design described in the original article:
lo0: all inbound and outbound packets can pass through ()
ipfilter:
pass out quick on lo0 all
pass in quick on lo all
pf:
pass out quick on lo0 all
pass in quick on lo all
As you can see, nothing has changed here. Such simple rules can be copied verbatim. The situation changes when we try to rewrite more complex rules, like the ones shown below. (The tun0 interface connects our network to the Internet.)
tun0: outbound packets sent from any network address to the private address space cannot pass through
ipfilter:
block out quick on tun0 from any to 192.168.0.0/16
block out quick on tun0 from any to 172.16.0.0/12
block out quick on tun0 from any to 127.0.0.0/8
block out quick on tun0 from any to 10.0.0.0/8
block out quick on tun0 from any to 0.0.0.0/8
block out quick on tun0 from any to 169.254.0.0/16
block out quick on tun0 from any to 192.0.2.0/24
block out quick on tun0 from any to 204.152.64.0/23
block out quick on tun0 from any to 224.0.0.0/3
pf:
block out quick on tun0 from any to { 192.168.0.0/16, 172.16.0.0/12, 127.0.0.0/8, 10.0.0.0/8, 0.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, 204.152.64.0/23, 224.0.0.0/3 }
Now, that's a refreshing change! We've just shrunk nine lines into one. As you can see, we listed all network addresses inside a pair of curly braces: {}. This simple trick can be used to list multiple arguments for the proto, from, to, port, and icmp-type keywords.
The rest of the syntax is unchanged, but watch out for the port and proto syntax.
tun0: incoming packets sent from any network address to port 80 can pass through to the mail and HTTP servers located in the DMZ
ipfilter:
pass in quick on tun0 proto tcp/udp from any to x.x.x.x/32 port = 25 pass in quick on tun0 proto tcp/udp from any to 192.168.2.4/32 port = 25pass in quick on tun0 proto tcp/udp from any to x.x.x.x/32 port = 80 pass in quick on tun0 proto tcp/udp from any to 192.168.2.3/32 port = 80
pf:
pass in on tun0 inet proto { tcp, udp } from any to x.x.x.x/32 port { 25, 80 }
pass in on tun0 inet proto { tcp, udp } from any to 192.168.2.3/32 port 80
pass in on tun0 inet proto { tcp, udp } from any to 192.168.2.4/32 port 25
As you can see, in pf rules there is no = character after the port keyword. We do not use the tcp/udp notation to specify both tcp and udp protocols, but we list them in curly braces instead. Forgetting to change this is a common mistake when transferring rules from ipfilter to pf. Fortunately, pfctl spots such mistakes and refuses to upload them to pf.
The name of the port can be replaced by the name of the service assigned to that port. For example:
pass in on tun0 inet proto { tcp, udp } from any to x.x.x.x/32 port { smtp, www }
The names of services can be found in /etc/services.
Other interesting changes include the scrub action, which normalizes malformed packets. This action uses additional CPU cycles, but it's well worth using to ensure that the packets arriving in our network are well formed and won't cause problems to applications running on your internal network. Should you use it? You decide. Try running your firewall with and without scrub and see if there is a difference in network performance. The following rule tells pf to normalize all incoming packets on all interfaces; add it at the beginning of your pf ruleset.
scrub in all
Further improvements made to pf include enhanced stateful filtering. Not only can you ask pf to keep state, the same feature available in ipfilter, but you can also improve security by generating more secure initial sequence numbers with modify state. To enable this feature, replace keep state with modify state. This feature puts additional load on the firewall machine, and you might want to compare firewall performance with and without modify state to see if and how it affects performance. To use it, replace keep state with modify state in your ruleset (using modify state implies keep state). state modification works only with TCP packets.
|
Yet another useful feature is variable substitution. It allows us to define variables and reuse them in different places in our ruleset. For example, we can store the name of the external interface (tun0), and the list of non–routable network addresses can be stored in the variables ExtIF and NoGoIPs. Use them as follows:
ExtIF="tun0"
NoGoIPs="{ 192.168.0.0/16, 172.16.0.0/12, 127.0.0.0/8, 10.0.0.0/8, 0.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, 204.152.64.0/23, 224.0.0.0/3 }"
# prevent spoofing non-routable addresses
block in quick on $ExtIF from $NoGoIPs to any
block out quick on $ExtIF from any to $NoGoIPs
The final ruleset that implements the security policy presented in the earlier article looks like this:
#################################################################
# define variables
ExtIF="tun0"
PrvIF="ne1"
DMZIF="ne2"
NoGoIPs="{ 192.168.0.0/16, 172.16.0.0/12, 127.0.0.0/8, 10.0.0.0/8, 0.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, 204.152.64.0/23, 224.0.0.0/3 }"
PrivateIPs="192.168.1.0/24"
DMZIPs="192.168.2.0/24"
#################################################################
# normalize packets
scrub in all
#################################################################
# stop all IPv6 traffic
block in quick inet6 all
block out quick inet6 all
#################################################################
# pass everything on loopback (lo0)
pass in quick on lo0 all
pass out quick on lo0 all
#################################################################
# Internet (tun0)
# prevent spoofing of non-routable addresses
block in quick on $ExtIF from $NoGoIPs to any
block out quick on $ExtIF from any to $NoGoIPs
# stop all incoming packets
block in on $ExtIF all
pass in on $ExtIF inet proto { tcp, udp } from any to 192.168.2.4/32 port smtp keep state
pass in on $ExtIF inet proto { tcp, udp } from any to 192.168.2.3/32 port www keep state
# block all outgoing packets
block out on $ExtIF all
# allow TCP IPv4 connections to the outside world, keep state
pass out on $ExtIF inet proto tcp all flags S/SA modulate state
pass out on $ExtIF inet proto { udp, icmp } all keep state
#################################################################
# private network (ne1)
# prevent spoofing of non-routable addresses
block in quick on $PrvIF from ! $PrivateIPs to any
block out quick on $PrvIF from any to ! $PrivateIPs
# stop all incoming and outgoing packets
block in on $PrvIF all
block out on $PrvIF all
# allow TCP IPv4 connections to the outside world, keep state
pass in on $PrvIF inet proto tcp from $PrivateIPs to any flags S/SA modulate state
pass in on $PrvIF inet proto { udp, icmp } from $PrivateIPs to any keep state
#################################################################
# DMZ network (ne2)
# prevent spoofing of non-routable addresses
block in quick on $DMZIF from ! $DMZIPs to any
block out quick on $DMZIF from any to ! $DMZIPs
# stop all incoming and outgoing packets
block in on $DMZIF all
block out on $DMZIF all
# allow TCP IPv4 connections to the outside world, keep state
pass in on $DMZIF inet proto tcp from $DMZIPs to any flags S/SA modulate state
pass in on $DMZIF inet proto { udp, icmp } from $DMZIPs to any keep state
block in on $DMZIF inet from $DMZIPs to $PrivateIPs
pass out on $DMZIF inet proto tcp from any to $DMZIPs flags S/SA modulate state
pass out on $DMZIF inet proto { udp, icmp } from any to $DMZIPs keep state
There are now only 27 rules, compared to 58 in the ipfilter ruleset presented in the previous article. That’s quite a savings, and it makes managing firewalls so much easier. Careful readers will notice the use of the inet keyword. It tells pf that a particular rule applies to IPv4 packets. To apply the rule to IPv6 packets, use the inet6 argument.
If you want to apply the above ruleset to your own network, simply modify the names of the interfaces and network addresses. If you are running the mail and WWW servers on the same machine, you can compress the rules:
pass in on $ExtIF inet proto { tcp, udp } from any to 192.168.2.4/32 port smtp keep state
pass in on $ExtIF inet proto { tcp, udp } from any to 192.168.2.3/32 port www keep state
into
pass in on $ExtIF inet proto { tcp, udp } from any to 192.168.2.4/32 port { smtp, www } keep state
But remember to replace 192.168.2.4/32 with the actual address of the server.
ipfilter NAT rules into pf NAT rules?Changes made to the NAT rules syntax are small; administrators switching from the NAT found in OpenBSD 2.8 and 2.9 should feel right at home. When translating the old rules, remember that the map keyword has been replaced with nat on followed by the name of the external interface. Also, the portmap keyword is gone. So, instead of the old
map tun0 192.168.1.0/24 -> x.x.x.x/32 portmap tcp/udp 10000:20000
map tun0 192.168.1.0/24 -> x.x.x.x/32
map tun0 192.168.2.0/24 -> x.x.x.x/32 portmap tcp/udp 20001:30000
map tun0 192.168.2.0/24 -> x.x.x.x/32
we can now use a simple set of rules. (Note that there is no /32 at the end of the external address.)
nat on tun0 from 192.168.1.0/24 to any -> x.x.x.x
nat on tun0 from 192.168.2.0/24 to any -> x.x.x.x
More radical changes have been made to the rdr rules to make their syntax resemble pf filtering rules. The old rdr rules
rdr tun0 x.x.x.x/32 port 80 -> 192.168.1.3 port 80 tcp
rdr tun0 x.x.x.x/32 port 80 -> 192.168.1.3 port 80 udp
become
rdr on tun0 proto tcp from any to x.x.x.x/32 port 80 -> 192.168.254.2 port 80
rdr on tun0 proto udp from any to x.x.x.x/32 port 80 -> 192.168.254.2 port 80
As you can see, the differences are only in the arrangement of keywords in the rules. The new NAT rules set is shown below.
#################################################################
# NAT
nat on tun0 from 192.168.255.0/24 to any -> x.x.x.x
nat on tun0 from 192.168.254.0/24 to any -> x.x.x.x
#################################################################
# Internet (tun0)
rdr on tun0 proto tcp from any to x.x.x.x/32 port 25 -> 192.168.2.4 port 25
rdr on tun0 proto udp from any to x.x.x.x/32 port 25 -> 192.168.2.4 port 25
rdr on tun0 proto tcp from any to x.x.x.x/32 port 80 -> 192.168.2.3 port 80
rdr on tun0 proto udp from any to x.x.x.x/32 port 80 -> 192.168.2.3 port 80
#################################################################
# private network (ne1)
rdr on ne1 proto tcp from 192.168.1.0/24 to x.x.x.x/32 port 25 -> 192.168.2.4 port 25
rdr on ne1 proto udp from 192.168.1.0/24 to x.x.x.x/32 port 25 -> 192.168.2.4 port 25
rdr on ne1 proto tcp from 192.168.1.0/24 to x.x.x.x/32 port 80 -> 192.168.2.3 port 80
rdr on ne1 proto udp from 192.168.1.0/24 to x.x.x.x/32 port 80 -> 192.168.2.3 port 80
#################################################################
# DMZ network (ne2)
rdr on ne2 proto tcp from 192.168.2.0/24 to x.x.x.x/32 port 25 -> 192.168.2.4 port 25
rdr on ne2 proto udp from 192.168.2.0/24 to x.x.x.x/32 port 25 -> 192.168.2.4 port 25
rdr on ne2 proto tcp from 192.168.2.0/24 to x.x.x.x/32 port 80 -> 192.168.2.3 port 80
rdr on ne2 proto udp from 192.168.2.0/24 to x.x.x.x/32 port 80 -> 192.168.2.3 port 80
If you want to use that ruleset on your own firewall, remember to replace the IP numbers with the actual IP addresses on your network. You can find out more about these, the pf, and the syntax of pf and NAT rules in these man pages: pf, pf.conf, and nat.conf.
OpenBSD 3.1 will bring an interesting enhancement to pf in the form of authpf, which provides a mechanism for enabling and disabling access by users in addition to IP number control. The future is bright.
That was the question I got asked most of the time. There were two reasons: the mechanics of the publishing industry and my conservative approach to new releases of software. Let me explain this in a little more detail.
First, a little information about the time it takes to publish something. It is very rare that something you write will be published the day you submit it. Once an author sends an article to an editor, he loses direct control over his work and has to wait for the editor to decide if, and when, the article is going to be published. That may happen on the same day, but it could just as well be a few weeks or months before the general public can see it, and the fact that you submit your piece to an online publisher does not matter that much.
To put things in perspective, I once had to wait fourteen months for a publisher to decide that she wanted to publish my article! Of course, that is on the extreme side of things and it did not take as long to publish Securing Small Networks with OpenBSD, but the article did have to wait for its turn. And it's not the editor's fault either, as there are many issues beyond his or her control. That's how the publishing world works, even if something you write is published online.
Second, when the article was accepted (not published, but put on a list of articles awaiting publication), OpenBSD 3.0 had been available for only a week or so. There was no time to test it, and I wanted to wait for things to settle after the switch from ipfilter to pf. (Theo's decision to switch had a lot to do with ipf licensing issues and politics, which you can find out more about at http://slashdot.org/search.pl?query=ipfilter.)
To make things a bit more complicated, Darren Reed, the author of ipf, published his own OpenBSD 3.0 fork that uses ipf instead of pf. You can find it on Darren's site. (Of course, all information from "Securing Small Networks with OpenBSD" still applies to that release.) But you should remember that this is Darren's own fork, and it's not supported by the main OpenBSD team.
Oh, the politics, licenses, and egos. . . .
Jacek Artymiak started his adventure with computers in 1986 with Sinclair ZX Spectrum. He's been using various commercial and Open Source Unix systems since 1991. Today, Jacek runs devGuide.net, writes and teaches about Open Source software and security, and tries to make things happen.
Return to the BSD DevCenter.
Copyright © 2007 O'Reilly Media, Inc.