OpenBSD Firewalls

These days, there are quite a few open source solutions for firewalls including pfSense and OPNsense, which provide easy to use web interfaces to configure tools as the PF Firewall, and Squid web proxy.

However, it’s relatively easy to configure these services directly using OpenBSD.

OpenBSD is an open-source operating system that focuses on security, code correctness, and simplicity. It has a strong focus on code auditing and ensures that the code base is as secure as possible. Every part of the operating system is subjected to scrutiny to avoid vulnerabilities.

As such, it makes a good fit for a firewall system.


Installing OpenBSD

Installing OpenBSD is straightforward, provided your hardware supports it. The official documentation provides detailed installation instructions, although for the most part it just consists of writing the install image to a USB drive, and selecting next on most of the default options. The only selection of note is the packages installed. Installing X11 and games increase the attack surface of the system, so are deselected.

PackageDescriptionInstall
bsdThe kernelTrue
bsd.mpThe multi-processor kernel (only on some platforms)True
bsd.rdThe ramdisk kernelTrue
base78.tgzThe base systemTrue
comp78.tgzThe compiler collection, headers and librariesTrue
man78.tgzManual pagesTrue
game78.tgzText-based gamesFalse
xbase78.tgzBase libraries and utilities for X11 (requires xshare78.tgz)False
xfont78.tgzFonts used by X11False
xserv78.tgzX11’s X serversFalse
xshare78.tgzX11’s man pages, locale settings and includesFalse

Networking

My firewall has 4 network interfaces, em0 – em3. Create /etc/hostname.<interface> files for each of them, and include the required IP addresses.

puffy# cat /etc/hostname.em0 
inet 192.168.1.105 255.255.255.0 192.168.1.255 description "WAN"
puffy# cat /etc/hostname.em1 
inet 172.16.1.1 255.255.255.0 172.16.1.255 description "LAN"
puffy# cat /etc/hostname.em2 
inet 172.16.2.1 255.255.255.0 172.16.2.255 description "DMZ1"
puffy# cat /etc/hostname.em3 
inet 172.16.3.1 255.255.255.0 172.16.3.255 description "DMZ2"

Add a default gateway into /etc/mygate.

puffy# cat /etc/mygate                                                                                                                                                                                                                                                                                                                         
192.168.1.254

DNS Configuration

With the network interfaces online we can configure a DNS server. Unbound DNS can be be configured to ensure DNS over TLS is used. Modify /var/unbound/etc/unbound.conf to allow access from our subnets, and set a forward-zone to point to Cloudflare’s (1.1.1.1) DNS over TLS servers.

server:
	interface: 127.0.0.1
	interface: 172.16.1.1
	interface: 172.16.2.1
	interface: 172.16.3.1
	interface: ::1

    access-control: 127.0.0.0/8 allow
    access-control: 172.16.1.0/24 allow
    access-control: 172.16.2.0/24 allow
    access-control: 172.16.3.0/24 allow

	access-control: 0.0.0.0/0 refuse
	access-control: 127.0.0.0/8 allow
	access-control: ::0/0 refuse
	access-control: ::1 allow

	hide-identity: yes
	hide-version: yes

	auto-trust-anchor-file: "/var/unbound/db/root.key"
	val-log-level: 2

	aggressive-nsec: yes

remote-control:
	control-enable: yes
	control-interface: /var/run/unbound.sock

forward-zone:
    	name: "."
    	forward-tls-upstream: yes
    	forward-addr: 1.1.1.1@853
    	forward-addr: 1.0.0.1@853

Enable the unbound and start the unbound service using rcctl.

puffy# rcctl enable unbound
puffy# rcctl start unbound

NTP

Modify /etc/ntpd.conf to listen on all interfaces.

# $OpenBSD: ntpd.conf,v 1.16 2019/11/06 19:04:12 deraadt Exp $
#
# See ntpd.conf(5) and /etc/examples/ntpd.conf

listen on *

# Configure NTP servers to synchronize with
servers pool.ntp.org

Then enable and start the service.

puffy# rcctl enable ntpd                                                                                                                                                                                                                                                                                                                       
puffy# rcctl start ntpd  
ntpd(ok)

The ntpctl command can be used to verify the clock is correctly synchronised.

puffy# ntpctl -s all 
4/4 peers valid, clock synced, stratum 3

peer
   wt tl st  next  poll          offset       delay      jitter
176.58.127.131 from pool pool.ntp.org
 *  1 10  2   18s   33s         0.347ms    14.658ms     8.413ms
176.58.115.34 from pool pool.ntp.org
    1 10  2    5s   31s         0.003ms    15.764ms     5.552ms
193.57.159.118 from pool pool.ntp.org
    1 10  2   15s   30s        -9.474ms    41.747ms    58.618ms
109.74.197.50 from pool pool.ntp.org
    1 10  2    9s   30s        -2.041ms    18.651ms    11.991ms

DHCP Services

Next, we can configure a DHCP server to operate on our internal interfaces. Modify /etc/dhcpd.conf to configured DHCP (which also configures the firewall as the subnets DNS & NTP server).

subnet 172.16.1.0 netmask 255.255.255.0 {
    range 172.16.1.10 172.16.1.100;
    option routers 172.16.1.1;
    option domain-name-servers 172.16.1.1;
}

subnet 172.16.2.0 netmask 255.255.255.0 {
    range 172.16.2.10 172.16.2.100;
    option routers 172.16.2.1;
    option domain-name-servers 172.16.2.1;
}

subnet 172.16.3.0 netmask 255.255.255.0 {
    range 172.16.3.10 172.16.3.100;
    option routers 172.16.3.1;
    option domain-name-servers 172.16.3.1;
}

Modify rc.conf.local to include the interface addresses for the DHCP daemon, and start the service.

puffy# cat /etc/rc.conf.local  
dhcpd_flags="em1 em2 em3"
puffy# rcctl start dhcpd          
dhcpd(ok)

Firewall Configuration

Modify sysctl.conf to enable IP Forwarding so the firewall can pass traffic, and disable IPv6.

puffy# cat /etc/sysctl.conf  
net.inet.ip.forwarding=1
net.inet6.ip6.forwarding=0
net.inet6.ip6.use_tempaddr=0
net.inet6.ip6.auto_linklocal=0

Modify /etc/pf.conf to include your firewall rules. The ruleset should be self evident. Rules are processed top down. The quick keyword will prevent further rule processing.

wan_if="em0"
lan_if="em1"  	 
dmz1_if="em2"   
dmz2_if="em3" 

lan_net="172.16.1.0/24"
dmz1_net="172.16.2.0/24"
dmz2_net="172.16.3.0/24"

set block-policy drop
set skip on lo

# LAN/SSH > FIREWALL
pass in quick on $lan_if proto tcp from $lan_net to ($lan_if) port ssh keep state

# WAN outbound NAT
match out on $wan_if inet from {$lan_net $dmz1_net $dmz2_net} to any nat-to ($wan_if)
pass out quick on $wan_if from {$lan_net $dmz1_net $dmz2_net} to any keep state

# DNS > FIREWALL
pass in quick on $lan_if proto { udp tcp } from $lan_net to ($lan_if) port domain keep state
pass in quick on $dmz1_if proto { udp tcp } from $dmz1_net to ($dmz1_if) port domain keep state
pass in quick on $dmz2_if proto { udp tcp } from $dmz2_net to ($dmz2_if) port domain keep state

# NTP > FIREWALL
pass in quick on $lan_if proto udp from $lan_net to ($lan_if) port ntp keep state
pass in quick on $dmz1_if proto udp from $dmz1_net to ($dmz1_if) port ntp keep state
pass in quick on $dmz2_if proto udp from $dmz2_net to ($dmz2_if) port ntp keep state

# LAN > NET
pass in quick on $lan_if proto tcp from $lan_net to any port {http https} keep state
pass in quick on $lan_if proto udp from $lan_net to any port {51820} keep state

# LAN > DMZ1
pass in quick on $lan_if proto tcp from $lan_net to 172.16.2.200 port {8006} keep state
pass in quick on $lan_if proto tcp from $lan_net to $dmz1_net port {ssh rdp} keep state

# DMZ1 > NET
block in quick on $dmz1_if from $dmz1_net to { $lan_net $dmz2_net }
pass in quick on $dmz1_if proto tcp from $dmz1_net to any port {http https} keep state

# DMZ2 > NET
block in log quick on $dmz2_if from $dmz2_net to { $lan_net $dmz1_net }
pass in quick on $dmz2_if proto tcp from $dmz2_net to any port {domain http https} keep state
pass in quick on $dmz2_if proto udp from $dmz2_net to any port {domain https ntp} keep state

# Allow traffic from firewall
pass out quick inet keep state

# Default deny
block drop log all

Use pfctl to load, and view the current ruleset.

pfctl -f /etc/pf.conf
puffy# pfctl -sr
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to (em1) port = 22 flags S/SA
match out on em0 inet from 172.16.1.0/24 to any nat-to (em0) round-robin
match out on em0 inet from 172.16.2.0/24 to any nat-to (em0) round-robin
match out on em0 inet from 172.16.3.0/24 to any nat-to (em0) round-robin
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to 172.16.2.0/24 port = 22 flags S/SA
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to 172.16.2.0/24 port = 3389 flags S/SA
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to (em1) port = 53 flags S/SA
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to any port = 80 flags S/SA
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to any port = 443 flags S/SA
pass in quick on em1 inet proto tcp from 172.16.1.0/24 to 172.16.2.200 port = 8006 flags S/SA
pass in quick on em1 inet proto udp from 172.16.1.0/24 to (em1) port = 53
pass in quick on em1 inet proto udp from 172.16.1.0/24 to (em1) port = 123
pass in quick on em1 inet proto udp from 172.16.1.0/24 to any port = 51820
pass in quick on em2 inet proto udp from 172.16.2.0/24 to (em2) port = 53
pass in quick on em2 inet proto udp from 172.16.2.0/24 to (em2) port = 123
pass in quick on em3 inet proto udp from 172.16.3.0/24 to (em3) port = 53
pass in quick on em3 inet proto udp from 172.16.3.0/24 to (em3) port = 123
pass in quick on em2 inet proto tcp from 172.16.2.0/24 to (em2) port = 53 flags S/SA
pass in quick on em3 inet proto tcp from 172.16.3.0/24 to (em3) port = 53 flags S/SA
pass out quick on em0 inet from 172.16.1.0/24 to any flags S/SA
pass out quick on em0 inet from 172.16.2.0/24 to any flags S/SA
pass out quick on em0 inet from 172.16.3.0/24 to any flags S/SA
block drop in quick on em2 inet from 172.16.2.0/24 to 172.16.1.0/24
block drop in quick on em2 inet from 172.16.2.0/24 to 172.16.3.0/24
pass in quick on em2 inet proto tcp from 172.16.2.0/24 to any port = 80 flags S/SA
pass in quick on em2 inet proto tcp from 172.16.2.0/24 to any port = 443 flags S/SA
block drop in log quick on em3 inet from 172.16.3.0/24 to 172.16.1.0/24
block drop in log quick on em3 inet from 172.16.3.0/24 to 172.16.2.0/24
pass in quick on em3 inet proto tcp from 172.16.3.0/24 to any port = 53 flags S/SA
pass in quick on em3 inet proto tcp from 172.16.3.0/24 to any port = 80 flags S/SA
pass in quick on em3 inet proto tcp from 172.16.3.0/24 to any port = 443 flags S/SA
pass in quick on em3 inet proto udp from 172.16.3.0/24 to any port = 53
pass in quick on em3 inet proto udp from 172.16.3.0/24 to any port = 443
pass in quick on em3 inet proto udp from 172.16.3.0/24 to any port = 123
pass out quick inet all flags S/SA
block drop log all

Firewall logs are stored in binary form. To read them, use TCPDump.

tcpdump -n -e -ttt -r /var/log/pflog  | head
tcpdump: WARNING: snaplen raised from 116 to 160
Feb 18 18:23:06.188951 rule 4/(match) block out on em1: 172.16.1.1.22 > 172.16.1.10.43084: P 471680753:471680797(44) ack 3882900921 win 271 &lt;nop,nop,timestamp 4265840327 3060963154> [tos 0xb8]
Feb 18 18:23:06.191893 rule 4/(match) block out on em1: 172.16.1.1.22 > 172.16.1.10.43084: F 44:44(0) ack 1 win 271 &lt;nop,nop,timestamp 4265840327 3060963154> [tos 0xb8]
Feb 18 18:23:06.192534 rule 4/(match) block in on em1: 172.16.1.10.43084 > 172.16.1.1.22: P 1:37(36) ack 0 win 85 &lt;nop,nop,timestamp 3060963173 4265840307> (DF) [tos 0x10]
Feb 18 18:23:06.211997 rule 4/(match) block in on em1: 172.16.1.10.43084 > 172.16.1.1.22: P 37:73(36) ack 0 win 85 &lt;nop,nop,timestamp 3060963193 4265840307> (DF) [tos 0x10]

Suricata Configuration

Suricata is an open-source Network IDS (Intrusion Detection System). It is designed to detect and prevent security threats on networks by monitoring network traffic and analysing it for malicious activity.

Add the suricata package using the following command.

pkg_add suricata

Then run suricata-update to download the latest ruleset. Edit /etc/suricata/suricata.yaml to ensure the rulepath matches with the one you just downloaded.

default-rule-path: /var/lib/suricata/rules
rule-files:
  - suricata.rules

Modify rc.conf.local to add the interfaces Suricata will be listening on.

cat /etc/rc.conf.local                                                                                                                                                                                                                                
suricata_flags="-i em1 em2"

Finally, enable the service.

puffy# rcctl enable suricata
puffy# rcctl start suricata

Plaintext alerts are stored in fast.log.

puffy# cat /var/log/suricata/fast.log  | grep Clou     
02/19/2026-11:36:13.658213  [**] [1:2027695:5] ET INFO Observed Cloudflare DNS over HTTPS Domain (cloudflare-dns .com in TLS SNI) [**] [Classification: Misc activity] [Priority: 3] {TCP} 172.16.1.12:55906 -> 172.64.41.3:443

In Conclusion

OpenBSD provides a solid foundation for a firewall platform. It’s worth noting when your looking at online documentation that the version of PF shipped with OpenBSD differs from the FreeBSD variant.