OpenBSD's DNS Firewall
2026-03-06

Why not Pi-hole?

Pi-hole needs no introduction; it has been a homelab staple for years. However, its popularity has also led to it being used in situations where it is not needed. If your goal is to self-host a recursive DNS resolver with ad blocking, you’ll need to run Unbound too. Instead of playing with cute web interfaces, we can simplify our networks by leveraging Response Policy Zones (RPZ). On top of that, Pi-hole only runs on Linux.

This isn’t for everyone; it requires familiarity with the command line. My goal is just to show a better integrated alternative to Pi-Hole for OpenBSD routers.

Configuring Unbound

Since Unbound is included in base, we do not need to install anything. The configuration takes place in /var/unbound/etc/unbound.conf because it runs in a chroot for extra separation.

Modify the config to something like:

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

server:
    interface: 127.0.0.1
    interface: ::1

    hide-identity: yes
    hide-version: yes

    root-hints: "/var/unbound/db/named.cache"
    prefetch: yes

    auto-trust-anchor-file: "/var/unbound/db/root.key"
    prefetch-key: yes

    # The order of these matters!
    module-config: "respip validator iterator"

    define-tag: "ads nsfw"
    # You might want to change these to your subnets
    access-control-tag: 0.0.0.0/0 "ads nsfw"
    access-control-tag: ::/0 "ads nsfw"

rpz:
    name: oisd-ads
    url: https://small.oisd.nl/rpz
    tags: "ads"

rpz:
    name: oisd-nsfw
    url: https://nsfw-small.oisd.nl/rpz
    tags: "nsfw" 

I’m not going to break this down line by line. If you need help understanding the config, start with the man page.

Validate the config with unbound-checkconf, because rcctl configtest unbound doesn’t work for whatever reason.

Add the root hints to the chroot

Recursive resolvers need to know where to start looking for DNS records. The root hints file provides the IP addresses of the 13 authoritative root nameservers that anchor the global DNS hierarchy.

cd /var/unbound/db/
ftp https://www.internic.net/domain/named.cache

Starting Unbound

Enable the service and increase the startup timeout. Larger RPZ files will increase the time it takes for the daemon to initialize, so we tell rc that the wait is expected.

rcctl set unbound timeout 60
rcctl enable unbound
rcctl start unbound

Redirect all DNS traffic on your network to Unbound

Then add this little trick to /etc/pf.conf so that all incoming local traffic on port 53 is routed to unbound. This doesn’t include the router itself.

pass in on !egress inet proto { udp tcp } from any to self \
    port domain rdr-to 127.0.0.1
pass in on !egress inet6 proto { udp tcp } from any to self \
    port domain rdr-to ::1

Now run pfctl -f /etc/pf.conf and you should be done!

Alternatively, you could advertise the resolver in dhcpd.conf and rad.conf like a normal person.

Testing

Modern web browsers often try to handle DNS internally (DoH), which can make command-line tests deceiving. So I recommend using web-based tests check to see if DNSSEC and the RPZ for ad blocking are working.

If these tests fail, check your browser’s “Secure DNS” settings; it might be bypassing the system resolver.

RPZ files

If you have the available memory, I suggest using the big lists from OISD.nl. Another source I’ve found is hagezi/dns-blocklists. Writing your own RPZ should also be self explanatory after you look at the syntax of an already existing zone.

Additional notes