Building a Firewall with OpenBSD 3.0


This document describes how to configure a firewall using OpenBSD Version 3.0 and the PF packet filter. The firewall is configured to sit between a DMZ containing web and mx hosts, and an internal network using RFC1918 ("private") IP address space.

This configuration should also work on 3.1-stable, but will likely fail with 3.1-current, as there are significant changes in the offing.

Revision: 23 July 2002

Revised modulate state clauses to reflect current thinking on TCP flags.

Revision: 18 March 2002

Added martian filtering
Modified hostname.xl0, nat.conf in anticipation of presenting ftp-proxy setup
Additional comments to clarify contents of nat.conf and pf.conf files

No Warranty

This example is presented without warranty. Note in particular that many believe that the "best of breed" design involves a three legged firewall design where one leg goes to the outside world, one leg goes to a "DMZ", and the final leg goes to the internal network. The design presented in this HowTo does not implement such a design, although I am contemplating a Part II explaining such a setup in the future.

Before You Start

Before you start with this document, it's a good idea to review Section 6 (network setup) of the OpenBSD FAQ.

Our Example

This example is derived from an actual network which I have worked on, although the IP addresses have been changed to protect the innocent. When I encountered this network, an all-in-one Router/Firewall/Web Server/Mail Server/DNS Server/Samba Server/Desert Topping/Floor Wax unit was in use. This type of setup can be very risky. It also accounts for the somewhat complex migration and haphazard IP address space layout.

Target Configuration

IP Addressing

For purposes of this example, the following IP address space is used:
Private Network172.16.0.0/16
Note that is not the space the DMZ really used when deployed. It's in the "test-net" (see Bill Manning's draft on special use prefixes for more information.)

The following assigments represent systems and interfaces in the DMZ and private networks. These aren't normally what I would have used, but the pre-existing situation and the need for an easy transition forced a lot of these choices.
DeviceDMZ IP AddressInternal IP Address
Cisco 1604 Ethernet192.0.2.222N/A
Firewall DMZ Ethernet/Outbound NAT IP192.0.2.211N/A
Firewall Private EthernetN/A172.16.0.1
Web Server192.0.2.219N/A
MX Server192.0.2.218N/A
Auth DNS Server192.0.2.193N/A
Backup Web Server192.0.2.216172.16.0.134
Internal Mail Server192.0.2.217172.16.0.135
IIS Web Server192.0.2.200172.16.0.91

Configuring the Firewall

Before going through this, familiarize yourself with sections 6.1 through 6.3 of the FAQ. Packet forwarding should be turned on (per FAQ 6.1.2) and packet filtering too (per FAQ 6.2). Additionally, bring your system up to date with the current patches, per The Errata and Patch List, as there may very well be updates to the base distribution that affect security and/or stability.

Unnecessary Services

Unnecessary services should be shut down. There are very few services that need to run on a firewall under any circumstances.

Edit /etc/inetd.conf and comment out everything but ident (and you don't strictly need to offer even that, but if you're going to get rid of ident, make sure that you refuse the connection, rather than simply dropping packets on the floor.)

Edit /etc/rc.conf and set sendmail= and portmap= to NO.

Edit the root crontab and comment out the sendmail client mqueue runner.


In this example, xl0 is the external interface and xl1 is the internal interface.

Aliases need to be set up for any IP that will map between the inside and outside. These go in /etc/hostname.xl0, like so:

inet NONE 
inet alias
inet alias
inet alias

/etc/hostname.xl1 is simpler:

inet NONE


nat.conf provides for internal hosts to access the outside world using a nat directive, and some external hosts to access certain internal services using the rdr directive. Note that in this file, DMZ IP addresses and internal IP addresses are used in the obvious way.

# nat: packets going out through xl0 with source address will get
# translated as coming from a state is created for such
# packets, and incoming packets will be redirected to the internal address.

nat on xl0 from to any ->

# rdr: packets coming in through xl0 with specific internal destinations
# will be redirected to the corresponding internal systems. a state is
# created for such packets, and outgoing packets will be translated as
# coming from the external address. access to the ssh ports is open to any
# but access to internal web and smtp is restricted only to permitted hosts
# in the DMZ.

# the first rdr rule handles the proxied web access from the DMZ web server
# to the internal IIS web server
rdr on xl0 proto tcp from to port http -> port http
#the second and third rdr rules handle ssh access from outside to two
#internal servers
rdr on xl0 proto tcp from any to port ssh -> port ssh
rdr on xl0 proto tcp from any to port ssh -> port ssh
#the fourth rdr rule handles smtp traffic from the external MX host to
#the internal mail host
rdr on xl0 proto tcp from to  port smtp -> port smtp


This is a little trickier. The redirections in nat.conf are done before these rules are applied, and so you see a mix of DMZ addresses (referencing the firewall itself) and internal addresses (referencing the servers on the internal network). Note that the variable expansion facility is used for readability and documentation.

#internal and external ethernet interfaces
ext_if = "xl0"
int_if = "xl1"
# (the martians list really should include the test network,
#  as well, but in our example, we're pretending it's globally routed space)
#,, and are RFC 1918 private space.
#,,,, and
# are special use space.
martians="{,,,,,, }"
#first group is internal hosts and the firewall itself, for controlling
#the target hosts for which some services are to be opened up.
#note that these can either be single hosts, or groups inside {} braces
ssh_hosts = "{,, }"
http_hosts = ""
smtp_hosts = ""
#the second group is external hosts for which we are granting
#access to internal hosts on a case by case basis. again, single
#hosts or groups in {} braces.
mx01 = ""
www = ""
#here now are the rules
#first block the martians
block in quick on $ext_if inet from $martians to any
block out quick on $ext_if inet from any to $martians
#next permit the desired services in a controlled manner
#these match up with 4 rdr rules from nat.conf, with the exception
#of ssh to the firewall itself.
pass in quick on $ext_if inet proto tcp from any to $ssh_hosts port = ssh flags S/SAFR modulate state
pass in quick on $ext_if inet proto tcp from $mx01 to $smtp_hosts port = smtp flags S/SAFR modulate state
pass in quick on $ext_if inet proto tcp from $www to $http_hosts port = http flags S/SAFR modulate state
#now block the rest, giving proper return messages for tcp and udp
#connections we don't wish to pass through
block return-rst in quick on $ext_if proto tcp all
block return-icmp in quick on $ext_if proto udp all
block in quick on $ext_if all
#finally, allow our internal users full access to the outside
pass out on $ext_if inet proto tcp all flags S/SAFR modulate state
pass out on $ext_if inet proto udp all keep state
pass out on $ext_if inet proto icmp all icmp-type 8 code 0 keep state