Skip to main content
RunBook Academy

OPNsenseXVIII · DNS and UnboundSplit-horizon and overrides

DNS access lists — restricting which subnets can query the firewall resolver

Foundation⏱ ~10 minunbound-controldrillconfigctl

What you'll learn

  • Explain the Unbound access-list action model and matching order
  • Verify the auto-created entries for each listening interface
  • Restrict resolver use to specific subnets in production
  • Recognise the symptoms of an access-list misconfiguration
  • Apply the change-review discipline to the ACL page

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

The OPNsense Unbound access-list page is the operator’s lever for deciding which clients can query the firewall’s resolver and what they can do. A default install creates allow entries for every interface Unbound listens on, which is a workable starting point; a production configuration answers the question deliberately for every subnet that can reach the resolver. This lesson covers the access-list model, the actions, the auto-creation behaviour, and the discipline that keeps the ACL page aligned with the firewall’s intended segmentation.

How Unbound matches ACLs

OPNsense Unbound uses a most-specific-match model. When a query arrives, the resolver scans the access-list table for the entry whose source subnet most specifically matches the source IP of the query. The action of that entry — allow, deny, refuse, allow snoop, deny non-local, refuse non-local — is taken. If no entry matches, the default is deny.

The most-specific-match rule has a consequence: a /32 entry that allows a single admin host overrides a /24 entry that allows the LAN. This is the right behaviour — the operator adds specific entries precisely to make exceptions.

The six actions

The actions are not equally common in production. Understanding each is the operator’s job:

ActionEffect on the queryWhen to use
AllowPermit recursive queriesDefault for clients that should use the resolver
DenySilently drop the queryWhen the operator wants the source to retry elsewhere without an error
RefuseReturn REFUSED rcode, then dropWhen the operator wants the client to know it is not allowed
Allow SnoopPermit recursive and non-recursive queries (cache snooping)Restrict to admin hosts; cache snooping reveals query patterns
Deny Non-localPermit only local-data queriesWhen a subnet should resolve only the firewall’s local overrides, not the public DNS
Refuse Non-localPermit only local-data queries; REFUSED for everything elseSame intent as Deny Non-local, with explicit refusal

The two “non-local” actions are the right answer for restricted VLANs (guest networks, kiosk networks, IoT segments) where the operator wants the hosts to be able to resolve names the firewall knows about (a captive portal address, a printer override) but should not be able to use the firewall as a free public resolver.

Auto-creation: the page’s default behaviour

When the operator enables Unbound on an interface, OPNsense creates an Allow entry for that interface’s subnet automatically. The auto-entry is named after the interface (e.g. lan_subnet, guest_subnet) and is visible on the access-list page.

The auto-entry is a useful default and a frequent source of mistakes. Two production incidents recur:

Wrong subnet auto-created. The interface’s subnet is 10.0.0.0/24 but the operator intended to also serve 10.0.1.0/24 from the same firewall. The auto-entry allows 10.0.0.0/24; the 10.0.1.0/24 clients can reach the resolver but get REFUSED. The fix is to add an explicit entry for 10.0.1.0/24.

Auto-entry left in after interface change. The operator changes a VLAN’s subnet from 10.0.0.0/24 to 10.0.0.0/22 (a network redesign) and the auto-entry is still on the old subnet. The new subnet is allowed by default (no entry → deny), so the resolver becomes unreachable. The fix is to update the auto-entry or remove it and add an explicit one.

The discipline: the ACL page is part of every change that touches interfaces, subnets, or the Unbound listening list. The change record should explicitly say whether the ACL changed.

Read-only / Safelist local zones
$ unbound-control -c /var/unbound/unbound.conf list_local_zones
lan.internal
10.in-addr.arpa
168.192.in-addr.arpa
dmz.internal
guest.wifi

Illustrative output

Restricting resolver use in production

The pattern for a production deployment:

  1. Listening interfaces. Set the listening list to the interfaces that should serve DNS. WAN and management are typically off.
  2. Auto-entries. Verify the auto-created allow entries match the listening interfaces and the intended subnets.
  3. Restricted subnets. For guest, kiosk, IoT, or other restricted VLANs, set the action to Refuse Non-local so the segment can resolve firewall-known names but cannot use the firewall as a public resolver.
  4. Admin hosts. Add Allow Snoop entries for the operator’s workstation and the monitoring host so cache snooping (and the diagnostic queries that go with it) is permitted from those sources only.
  5. Default deny. Implicit. Any subnet not explicitly allowed is denied.

The verification: from a host on each subnet, query the firewall. Allowed subnets return answers; restricted subnets get REFUSED for non-local names; admin hosts can run cache-snooping queries that other hosts cannot.

Symptoms of an ACL misconfiguration

Three failure modes appear when the ACL page does not match the intended segmentation:

Resolvers stop answering for a subnet. A client on 10.0.1.50 queries the firewall and gets REFUSED. The ACL page either has no entry for 10.0.1.0/24 (default deny) or has a deny/refuse entry that should not be there. The fix is the ACL page.

Resolvers answer but recursion fails. The ACL allows the subnet but Unbound returns SERVFAIL or no-data. This is not an ACL problem; it is a DNSSEC, forwarder, or upstream problem. The first diagnostic is to query a known-good name (drill @192.0.2.1 example.com from the same subnet) and check the flags.

Guest subnet hosts can resolve anything. The guest VLAN ACL is Allow instead of Refuse Non-local. The fix is the ACL page. The diagnostic is to query a name the firewall does not have an override for from a guest host and see whether the public IP comes back.

Verifying the ACL

The first verification is configctl unbound check, which catches syntax errors. The second is unbound-control list_local_zones, which shows the zones the resolver is serving. The third is direct testing from each subnet:

Read-only / Safedrill from allowed subnet
$ drill @192.0.2.1 example.com
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 53127
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; example.com.   IN  A
;; ANSWER SECTION:
example.com.    300   IN  A   93.184.216.34
;; Query time: 32 msec
;; SERVER: 192.0.2.1#53(192.0.2.1)

Illustrative output

The test is read-only and safe.

Summary

  • Unbound matches ACLs most-specific-first; the action of the best match wins. The default for unmatched sources is deny.
  • The six actions: Allow, Deny, Refuse, Allow Snoop, Deny Non-local, Refuse Non-local. Each has a place; the choice depends on what the subnet should be able to do.
  • OPNsense auto-creates Allow entries for every listening interface. The auto-entry is the right starting point but should be verified as part of every change that touches interfaces or subnets.
  • Restricted subnets (guest, kiosk, IoT) should use Refuse Non-local so they can resolve local overrides but not use the firewall as a public resolver.
  • The ACL page is part of every change review that touches interfaces, subnets, or the Unbound listening list.
  • Verify ACL behaviour with configctl unbound check, unbound-control list_local_zones, and direct queries from each subnet.

Knowledge check · 4 questions

  1. Q1. A guest VLAN is configured with the action Refuse Non-local. A guest host queries the firewall for an internal service (printer.corp.internal) for which a host override exists. What does the host receive?

  2. Q2. Deny and Refuse are distinguishable from the client perspective: Deny drops the query silently, while Refuse returns a REFUSED rcode.

  3. Q3. Which of the following are valid production uses for the Unbound ACL page? Select all that apply.

  4. Q4. The operator changes the LAN subnet from 10.0.0.0/24 to 10.0.0.0/22 (a network redesign). After applying, hosts on the new larger subnet cannot resolve names. What is the most likely cause?

Passing score: 75%. Answers are checked in this browser.