Skip to main content
RunBook Academy

← All labs in OPNsense

Lab · intermediate · ~60 min

Lab: Configure OPNsense Unbound as a validating recursive resolver

B · Nested virtualisationC · Simulation

Objectives

  • Enable Unbound in resolver mode on OPNsense
  • Point a LAN client at OPNsense for DNS and confirm queries resolve through OPNsense, not the ISP
  • Turn on DNSSEC validation and confirm it rejects a known-bad signature
  • Add a host override (split-horizon) for a local service
  • Inspect the Unbound cache to see what has been resolved

Prerequisites

This lab puts OPNsense’s Unbound resolver into production shape: recursive, validating, listening on the LAN interface, with a local host override that makes services.lab.local resolve to a private address without forwarding to the public DNS. By the end you will be able to read the cache, prove DNSSEC validation is working, and identify what the resolver is doing for every query that crosses it.

The lab assumes the LAN is 10.10.10.0/24 with OPNsense at 10.10.10.1, matching the DHCP-scope lab. If your topology differs, substitute the addresses in the steps below — the sequence is the same.

Objective

By the end of this lab, you can:

  • Enable Unbound on OPNsense in resolver mode and confirm the daemon is listening on the LAN interface.
  • Point a client at OPNsense for DNS and observe recursive resolution rather than forwarding.
  • Enable DNSSEC validation and confirm a deliberately bogus signature is rejected.
  • Add a host override so that an internal hostname resolves to a private address.
  • Read the Unbound cache to verify what has been resolved.

Requirements

You need:

  • An OPNsense instance with the default LAN configuration.
  • A client on the LAN with dig (Linux/macOS) or a way to inspect DNS resolution (Windows: Resolve-DnsName).
  • Shell access on OPNsense.
  • Outbound HTTPS to the root servers is required for true recursion. The lab’s nested mode uses a host with internet access; the simulation mode uses a local root mirror.

Tasks

Task 1: Enable Unbound in resolver mode

Services → Unbound DNS → General:

FieldValue
Enableon
Listen port53
Network interfacesLAN
System → DNS serversempty (do not fill)
DNSSECon
DNS over TLSoff (lab only)
Forwarding modeoff (resolver mode)

Click Save, then Apply.

OPNsense writes /var/unbound/unbound.conf and starts the daemon. Confirm:

sockstat -l -p 53
ps -axo pid,command | grep unbound

You should see unbound listening on 10.10.10.1:53 for the LAN interface, plus a localhost socket for unbound-control.

The trap is the System → Settings → General → DNS servers field. If you fill that, OPNsense configures Unbound in forwarder mode using those servers as upstreams. For resolver mode, leave it empty. This is the single most common reason an OPNsense install “has Unbound enabled” but is still forwarding.

Task 2: Validate the config before trusting it

Before touching any client, parse the config:

unbound-checkconf /var/unbound/unbound.conf

A clean output ends with unbound-checkconf: no errors in /var/unbound/unbound.conf. Anything else means the GUI accepted input that the daemon will refuse; fix the GUI, do not edit the file by hand.

Task 3: Point a client at OPNsense

On a LAN client, configure DNS to point at 10.10.10.1. On NetworkManager:

sudo nmcli connection modify "Wired connection 1" \
    ipv4.dns "10.10.10.1"
sudo nmcli connection down "Wired connection 1" && \
sudo nmcli connection up   "Wired connection 1"

On Windows, set the LAN adapter’s IPv4 DNS to 10.10.10.1 and ipconfig /flushdns. On macOS, System Settings → Network → [interface] → Details → DNS and add 10.10.10.1.

Confirm the resolver is being used:

resolvectl status | grep -A2 eth0
# Or, on a system without systemd-resolved:
cat /etc/resolv.conf

The first nameserver must be 10.10.10.1.

Task 4: Watch a recursive query end to end

The diagnostic that proves resolver mode is working: a query for a name the client has never asked before, captured at the firewall. From OPNsense:

sudo tcpdump -i em0 -nn -s 0 -c 30 port 53

From the client, ask for a name that is unlikely to be cached:

dig +short +noall +answer +stats @10.10.10.1 \
    opnsense-labs-are-cool.example.com

If the resolver is doing its job, the capture will show queries going out to the authoritative servers (a.gtld-servers.net, ns1.example.com, and so on) and answers coming back. If you configured OPNsense in forwarder mode by mistake, you will see exactly one query to the upstream resolver and one answer back — the recursion never happens on the firewall.

Stop the capture with Ctrl-C.

Task 5: Confirm DNSSEC validation

The test that proves DNSSEC is on: a name that is deliberately signed incorrectly, plus a name that is correctly signed.

# Correctly signed — DNSSEC must accept (status: NOERROR)
dig +dnssec @10.10.10.1 www.opnsense.org | grep -E 'status:|flags:'

# Deliberately broken signature — DNSSEC must reject (status: SERVFAIL)
dig +dnssec @10.10.10.1 www.dnssec-failed.org | grep -E 'status:|flags:'

dnssec-failed.org is a public test zone that is signed with a deliberately invalid signature. A validating resolver returns SERVFAIL for it. A non-validating resolver returns the A record anyway. If your dig returns an A record for www.dnssec-failed.org, DNSSEC is not validating — go back to Task 1 and confirm the checkbox is on, then unbound-checkconf and service unbound restart.

Inspect the validation chain:

dig +dnssec +multi @10.10.10.1 opnsense.org

You should see ad in the flags (authenticated data) and a chain of DNSKEY and DS records tracing back to the . trust anchor.

Task 6: Add a host override (split horizon)

For an internal service that must resolve only on the LAN, add a host override: Services → Unbound DNS → Overrides → + Add.

FieldValue
Hostservices
Domainlab.local
TypeA
Value10.10.10.20
Descriptioninternal services VIP

Save and apply.

Test from the client:

dig +short @10.10.10.1 services.lab.local

Must return 10.10.10.20. From a host outside the LAN, the same query must return NXDOMAIN or the public IP — never the private address. Test by querying 8.8.8.8 directly:

dig +short @8.8.8.8 services.lab.local

Should return nothing (status: NXDOMAIN). If it returns the private address, your host override leaked to the public DNS, which means you did not actually configure a host override but rather a forward zone or a delegation. Re-read the Overrides tab — it is distinct from Query Forwarding.

Task 7: Read the cache

unbound-control is the operator window into what the resolver has been doing:

# Top 20 entries by query count
unbound-control -c /var/unbound/unbound.conf stats_noreset | \
    grep -E '^total.num.queries'

# The actual cache contents (large; pipe through head)
unbound-control dump_cache | head -40

A healthy resolver shows:

  • total.num.queries climbing as clients resolve names.
  • total.num.cachehits larger than total.num.cachemiss for any name that is asked more than once — most popular names fall in this bucket.
  • total.num.dnsssec records non-zero if any DNSSEC-signed name has been resolved.

The cache itself looks like:

; services.lab.local 86400 IN A 10.10.10.20
; opnsense.org 3600 IN A 194.32.108.18
; opnsense.org 3600 IN A 194.32.108.19
; opnsense.org 3600 IN RRSIG A 8 2 3600 20260815000000 ...

Local-data entries (your host overrides) are present without a TTL-on-the-wire question. Authoritative answers come back with their real TTL. You can tell local-data and recursive answers apart by the comment column on the left.

Task 8: Clear the cache deliberately

There are times you need to flush — a public zone has changed and the cache is stale, a host override just edited, or you suspect cache poisoning. Use unbound-control flush deliberately, not as a fix-everything button:

# Flush a single name
unbound-control flush services.lab.local

# Flush everything
unbound-control flush_negative
unbound-control flush_zone .

flush_negative clears the NXDOMAIN cache. The next negative answer will be re-walked. This is useful when a zone was offline during a TTL window and you need clients to retry immediately.

Validation

  • sockstat -l -p 53 on OPNsense shows unbound listening on 10.10.10.1:53.
  • A client with 10.10.10.1 as its DNS server resolves public names through the firewall, not through an upstream.
  • dig +dnssec @10.10.10.1 www.opnsense.org returns status: NOERROR with the ad flag set.
  • dig +dnssec @10.10.10.1 www.dnssec-failed.org returns status: SERVFAIL.
  • dig +short @10.10.10.1 services.lab.local returns 10.10.10.20.
  • unbound-control dump_cache | head shows entries for both the override and a public name the client has queried.

Expected Result

OPNsense running Unbound in resolver mode, listening on the LAN, validating DNSSEC, and serving both public and local-host names. A LAN client whose only DNS path is the firewall, with DNSSEC proven to accept good signatures and reject bad ones, and a host override that resolves only on the LAN.

Troubleshooting

The client times out resolving anything. The GUI often binds Unbound to the interface address it knows about. If you recently changed the LAN IP, restart Unbound manually: service unbound restart. The GUI’s Apply changes button does not always pick up interface changes; the daemon keeps the old listen address until restarted.

unbound-checkconf reports “no errors” but queries still fail. The check tool does not exercise the listen sockets. Use sockstat -l -p 53 to confirm unbound is actually listening on the LAN address.

DNSSEC returns SERVFAIL for every signed zone. The trust anchor is missing or stale. Re-fetch:

unbound-anchor -a /var/unbound/root.key
service unbound restart

If the firmware version is older than mid-2025, upgrade — older builds ship trust anchors that predate the last root key rollover.

The host override returns the public IP from outside. You configured the override as a forward zone (DNS Query Forwarding) instead of a host override (Overrides). The override section in the GUI is below the forwarding section; they are not interchangeable.

unbound-control says “unbound was not running”. The control socket is enabled in Services → Unbound DNS → Advanced → unbound-control enable. Without it, the daemon refuses control commands. Restart after enabling.

Cleanup

Restore the resolver to the state the rest of the network expects:

# On OPNsense: disable Unbound and reset System → General → DNS
# Services → Unbound DNS → General → uncheck Enable → Apply
# System → Settings → General → DNS servers → restore ISP or "allow DHCP"
# System → Settings → General → DNS servers → leave empty if you want
#   forwarder mode off entirely

# Restore the client's DNS to whatever it was before
sudo nmcli connection modify "Wired connection 1" \
    ipv4.dns "" ipv4.ignore-auto-dns no
sudo nmcli connection down "Wired connection 1" && \
sudo nmcli connection up   "Wired connection 1"

The dnssec-failed.org test entry is fine to leave cached; it expires on its own TTL.

What you learned

  • Resolver mode means Unbound walks the DNS hierarchy itself. Forwarder mode means it sends every query to an upstream resolver. The two are configured in different GUI sections and look similar from a client.
  • DNSSEC validation is a daemon option, not a per-query argument. Once enabled, every signed name is validated and every bad signature is rejected with SERVFAIL.
  • Host overrides are local-data — they answer from the daemon’s config without any network query. They do not leak to the public DNS unless you mistakenly configured a delegation.
  • unbound-control dump_cache is the operator window into what the resolver is doing. If a name is misbehaving, the cache shows whether the daemon has an answer at all.

Deliverables

  • · Unbound running in resolver mode on the LAN interface
  • · A LAN client whose DNS path is OPNsense → root, validated with DNSSEC
  • · A host override for `services.lab.local`
  • · Cache dump showing recent recursive resolutions

Verification status

Last reviewed
2026-08-14
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.