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.
| Package | Description | Install |
| bsd | The kernel | True |
| bsd.mp | The multi-processor kernel (only on some platforms) | True |
| bsd.rd | The ramdisk kernel | True |
| base78.tgz | The base system | True |
| comp78.tgz | The compiler collection, headers and libraries | True |
| man78.tgz | Manual pages | True |
| game78.tgz | Text-based games | False |
| xbase78.tgz | Base libraries and utilities for X11 (requires xshare78.tgz) | False |
| xfont78.tgz | Fonts used by X11 | False |
| xserv78.tgz | X11’s X servers | False |
| xshare78.tgz | X11’s man pages, locale settings and includes | False |
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 <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 <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 <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 <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.