ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Securing Small Networks With OpenBSD, Part 2

by Jacek Artymiak
04/11/2002

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.

How do I use 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.

How do I translate 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.

How do I translate 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.

Coda, or "Why did you write about OpenBSD 2.8 and not 3.0?"

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 © 2009 O'Reilly Media, Inc.