FreeBSD.software
Home/Blog/How to Set Up Unbound DNS Resolver on FreeBSD
tutorial2026-03-29

How to Set Up Unbound DNS Resolver on FreeBSD

Complete guide to setting up Unbound as a local DNS resolver on FreeBSD. Covers configuration, DNSSEC validation, DNS-over-TLS forwarding, local zones, ad blocking, and performance tuning.

# How to Set Up Unbound DNS Resolver on FreeBSD

Every DNS query your server makes travels to a third-party resolver by default. That resolver sees every domain you visit, every API you call, and every service you depend on. Running your own recursive DNS resolver with Unbound on FreeBSD eliminates that dependency. You get faster lookups from local caching, DNSSEC validation to prevent spoofing, DNS-over-TLS encryption for forwarded queries, and the ability to block ad and tracking domains at the network level.

Unbound ships in the FreeBSD base system. There is nothing to install. This guide covers the full setup from enabling the service to production-grade tuning.

Why Run Your Own DNS Resolver

There are four practical reasons to operate a local DNS resolver instead of relying on your ISP or a public resolver like Google (8.8.8.8) or Cloudflare (1.1.1.1).

**Privacy.** A third-party resolver sees every domain you resolve. Your ISP resolver may log and sell that data. Even privacy-focused public resolvers require trust in their logging policies. A local resolver queries authoritative nameservers directly or forwards over encrypted channels you control. No third party builds a profile of your DNS traffic.

**Speed.** A local caching resolver stores answers for their full TTL. The second query for the same domain returns in under a millisecond instead of the 10-50ms round trip to an upstream resolver. On a busy server or network, this compounds into measurable latency reduction for web applications, mail delivery, and package management.

**DNSSEC validation.** Unbound validates DNSSEC signatures by default once configured. This prevents cache poisoning and man-in-the-middle attacks on DNS responses. Most stub resolvers in /etc/resolv.conf perform no validation at all.

**Ad and tracker blocking.** A local resolver can return NXDOMAIN or a null address for known ad-serving and tracking domains. This blocks ads and telemetry for every device on the network without browser extensions or per-device configuration.

If you are building a [FreeBSD router](/blog/freebsd-router-gateway/) or running a [DHCP server](/blog/dhcp-server-freebsd/) for a local network, Unbound is the natural companion service.

Unbound in the FreeBSD Base System

Unbound has been part of the FreeBSD base system since FreeBSD 10.0. It lives under /var/unbound/ with its configuration, root hints, and trust anchor files. You do not need to install anything from pkg or the ports tree.

The base system includes:

- /usr/sbin/unbound -- the resolver daemon

- /usr/sbin/unbound-control -- the management and statistics utility

- /usr/sbin/unbound-anchor -- the DNSSEC trust anchor updater

- /usr/sbin/unbound-checkconf -- the configuration file validator

- /var/unbound/unbound.conf -- the main configuration file

- /var/unbound/root.hints -- the root nameserver list

- /var/unbound/root.key -- the DNSSEC root trust anchor

Verify Unbound is present on your system:

sh

unbound -V

This prints the version and compile-time options. You should see support for libevent, subnet, and tls-port among the features listed.

Basic Configuration

The default configuration file at /var/unbound/unbound.conf is minimal. Below is a complete working configuration that covers recursive resolution, DNSSEC, access control, and sensible caching defaults. Start from this template and adjust for your environment.

yaml

# /var/unbound/unbound.conf

server:

# Network interface to listen on

interface: 0.0.0.0

interface: ::0

# Port (default 53)

port: 53

# Protocols

do-ip4: yes

do-ip6: yes

do-udp: yes

do-tcp: yes

# Access control -- adjust to your network

access-control: 127.0.0.0/8 allow

access-control: 10.0.0.0/8 allow

access-control: 172.16.0.0/12 allow

access-control: 192.168.0.0/16 allow

access-control: ::1/128 allow

access-control: fd00::/8 allow

access-control: 0.0.0.0/0 refuse

access-control: ::0/0 refuse

# Run as unbound user

username: "unbound"

directory: "/var/unbound"

chroot: "/var/unbound"

# Logging

verbosity: 1

use-syslog: yes

log-queries: no

log-replies: no

# Privacy and security

hide-identity: yes

hide-version: yes

harden-glue: yes

harden-dnssec-stripped: yes

harden-referral-path: yes

harden-algo-downgrade: yes

use-caps-for-id: yes

qname-minimisation: yes

# DNSSEC trust anchor

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

# Root hints

root-hints: "/var/unbound/root.hints"

# Cache sizing

msg-cache-size: 64m

rrset-cache-size: 128m

key-cache-size: 32m

neg-cache-size: 16m

# Cache behavior

prefetch: yes

prefetch-key: yes

serve-expired: yes

serve-expired-ttl: 86400

# Performance

num-threads: 2

msg-cache-slabs: 4

rrset-cache-slabs: 4

infra-cache-slabs: 4

key-cache-slabs: 4

so-reuseport: yes

so-rcvbuf: 4m

so-sndbuf: 4m

outgoing-range: 8192

num-queries-per-thread: 4096

# Minimize unnecessary records

minimal-responses: yes

# Private address ranges -- block rebinding attacks

private-address: 10.0.0.0/8

private-address: 172.16.0.0/12

private-address: 192.168.0.0/16

private-address: 169.254.0.0/16

private-address: fd00::/8

private-address: fe80::/10

# Include additional configuration files

include: "/var/unbound/conf.d/*.conf"

remote-control:

control-enable: yes

control-interface: 127.0.0.1

control-port: 8953

server-key-file: "/var/unbound/unbound_server.key"

server-cert-file: "/var/unbound/unbound_server.pem"

control-key-file: "/var/unbound/unbound_control.key"

control-cert-file: "/var/unbound/unbound_control.pem"

Create the include directory for modular configuration:

sh

mkdir -p /var/unbound/conf.d

Validate the configuration before starting:

sh

unbound-checkconf /var/unbound/unbound.conf

If the output says no errors in /var/unbound/unbound.conf, the configuration is valid.

Enabling and Starting Unbound

Add Unbound to /etc/rc.conf so it starts at boot:

sh

sysrc unbound_enable="YES"

Generate the control keys for unbound-control:

sh

unbound-control-setup

This creates the TLS certificates in /var/unbound/ that allow unbound-control to communicate with the running daemon.

Fetch or update the root trust anchor for DNSSEC:

sh

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

Update the root hints file (the list of root nameservers):

sh

fetch -o /var/unbound/root.hints https://www.internic.net/domain/named.root

Start the service:

sh

service local_unbound start

Note: FreeBSD uses local_unbound as the service name in the base system, not unbound. If you installed Unbound from packages instead of using the base system version, the service name is unbound.

Now point your server at its own resolver by editing /etc/resolv.conf:


nameserver 127.0.0.1

To prevent dhclient or resolvconf from overwriting this file, add to /etc/dhclient.conf:


supersede domain-name-servers 127.0.0.1;

Or make resolv.conf immutable:

sh

chflags schg /etc/resolv.conf

Test that resolution works:

sh

drill freebsd.org @127.0.0.1

You should get an A record in the answer section with a status of NOERROR.

DNSSEC Validation

The configuration above enables DNSSEC through the auto-trust-anchor-file directive. Unbound automatically manages the root zone trust anchor using RFC 5011 automated updates.

Test DNSSEC validation with a known signed domain:

sh

drill -D freebsd.org @127.0.0.1

The -D flag requests DNSSEC records. You should see RRSIG records in the response and the ad (Authenticated Data) flag set in the header.

Test that DNSSEC validation rejects bad signatures using a deliberately misconfigured domain:

sh

drill dnssec-failed.org @127.0.0.1

This domain has intentionally broken DNSSEC. Unbound should return SERVFAIL instead of an answer, proving that validation is working.

You can also verify with dig if you have it installed (pkg install bind-tools):

sh

dig +dnssec freebsd.org @127.0.0.1

Look for the ad flag in the response header flags line.

To keep the root trust anchor current, add a periodic task. Create /etc/periodic/weekly/600.unbound-anchor:

sh

#!/bin/sh

/usr/sbin/unbound-anchor -a /var/unbound/root.key

Make it executable:

sh

chmod +x /etc/periodic/weekly/600.unbound-anchor

DNS-over-TLS Forwarding

By default, Unbound resolves queries recursively by querying root servers, TLD servers, and authoritative servers directly. This is the most private option because no single upstream sees all your queries. However, some environments require forwarding through an upstream resolver -- corporate networks, networks that block port 53 outbound, or situations where you want the upstream's anycast performance.

DNS-over-TLS (DoT) encrypts the forwarding connection so intermediate networks cannot snoop on or tamper with your DNS traffic. Add a forwarding configuration in /var/unbound/conf.d/forward.conf:

Forwarding to Cloudflare

yaml

# /var/unbound/conf.d/forward-cloudflare.conf

forward-zone:

name: "."

forward-tls-upstream: yes

forward-addr: 1.1.1.1@853#cloudflare-dns.com

forward-addr: 1.0.0.1@853#cloudflare-dns.com

forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com

forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com

Forwarding to Quad9

yaml

# /var/unbound/conf.d/forward-quad9.conf

forward-zone:

name: "."

forward-tls-upstream: yes

forward-addr: 9.9.9.9@853#dns.quad9.net

forward-addr: 149.112.112.112@853#dns.quad9.net

forward-addr: 2620:fe::fe@853#dns.quad9.net

forward-addr: 2620:fe::9@853#dns.quad9.net

Forwarding to Google

yaml

# /var/unbound/conf.d/forward-google.conf

forward-zone:

name: "."

forward-tls-upstream: yes

forward-addr: 8.8.8.8@853#dns.google

forward-addr: 8.8.4.4@853#dns.google

forward-addr: 2001:4860:4860::8888@853#dns.google

forward-addr: 2001:4860:4860::8844@853#dns.google

Only use one of these files at a time. The @853 specifies the TLS port, and the # prefix identifies the server's TLS authentication name.

After adding the file, check and reload:

sh

unbound-checkconf

service local_unbound reload

Verify TLS forwarding is active by temporarily increasing verbosity or checking that queries resolve through the upstream:

sh

drill google.com @127.0.0.1

Note: When forwarding, DNSSEC validation still happens locally. Unbound validates the signatures it receives from the upstream forwarder, so you retain protection against spoofed responses even though you are not querying authoritative servers directly.

Local Zones and Overrides

Local zones let you define DNS records for internal hostnames, override public DNS for split-horizon setups, or redirect domains to specific addresses. This is essential if you run services on your LAN that need proper DNS names.

Create /var/unbound/conf.d/local-zones.conf:

yaml

# /var/unbound/conf.d/local-zones.conf

# Internal domain for LAN services

server:

# Define a local zone for your internal domain

local-zone: "home.lab." static

# Internal hosts

local-data: "router.home.lab. A 10.0.0.1"

local-data: "nas.home.lab. A 10.0.0.10"

local-data: "web.home.lab. A 10.0.0.20"

local-data: "db.home.lab. A 10.0.0.30"

local-data: "mail.home.lab. A 10.0.0.40"

# PTR records for reverse DNS

local-data-ptr: "10.0.0.1 router.home.lab."

local-data-ptr: "10.0.0.10 nas.home.lab."

local-data-ptr: "10.0.0.20 web.home.lab."

local-data-ptr: "10.0.0.30 db.home.lab."

local-data-ptr: "10.0.0.40 mail.home.lab."

# Override a public domain for split DNS

# (e.g., internal app resolves to LAN IP instead of public IP)

local-zone: "app.example.com." redirect

local-data: "app.example.com. A 10.0.0.20"

The static zone type means only the records you define are returned; queries for names not listed get NXDOMAIN. Other useful zone types:

- transparent -- serves local data if defined, otherwise resolves normally

- redirect -- redirects all names in the zone to the defined data

- deny -- silently drops queries for names in the zone

- refuse -- returns REFUSED for queries in the zone

- inform -- like transparent, but logs queries to this zone

Reload after changes:

sh

service local_unbound reload

Test:

sh

drill nas.home.lab @127.0.0.1

drill -x 10.0.0.10 @127.0.0.1

Access Control

The access-control directives in the main configuration determine which clients can send queries to your resolver. This is critical for any resolver listening on a public interface. An open resolver will be abused for DNS amplification attacks within hours.

The configuration in the template above allows queries from localhost and RFC 1918 private ranges, and refuses everything else. Adjust based on your network:

yaml

server:

# Allow only your specific subnet

access-control: 10.0.0.0/24 allow

# Allow a trusted remote subnet (e.g., VPN clients)

access-control: 10.8.0.0/24 allow

# Refuse everything else

access-control: 0.0.0.0/0 refuse

access-control: ::0/0 refuse

Access control values:

- allow -- allow queries, apply DNSSEC validation

- deny -- silently drop the query

- refuse -- return REFUSED error

- allow_snoop -- allow queries including cache snooping (only for trusted debugging hosts)

- allow_setrd -- allow queries with the RD (recursion desired) bit set

For a server acting as a resolver for a local network behind a [FreeBSD router](/blog/freebsd-router-gateway/), allow for your LAN subnet and refuse for everything else is the correct policy.

If your server is also a [hardened FreeBSD system](/blog/hardening-freebsd-server/), combine Unbound access control with PF firewall rules that restrict port 53 access to your LAN:

# In /etc/pf.conf

pass in on $lan_if proto { tcp, udp } from $lan_net to self port 53

block in on $ext_if proto { tcp, udp } to self port 53

DNS-Based Ad Blocking

Unbound can block ads, trackers, and malware domains by returning NXDOMAIN or 0.0.0.0 for known bad domains. This works at the network level -- every device using your resolver gets ad blocking without installing anything.

Create a blocklist configuration file:

sh

touch /var/unbound/conf.d/blocklist.conf

Write a script that downloads and formats a blocklist. Create /var/unbound/update-blocklist.sh:

sh

#!/bin/sh

# /var/unbound/update-blocklist.sh

# Downloads ad/tracker blocklists and formats them for Unbound

BLOCKLIST_URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"

TMPFILE="/tmp/unbound-blocklist.tmp"

OUTFILE="/var/unbound/conf.d/blocklist.conf"

# Download the hosts file

fetch -q -o "$TMPFILE" "$BLOCKLIST_URL"

if [ $? -ne 0 ]; then

echo "Failed to download blocklist"

exit 1

fi

# Convert hosts file format to Unbound local-zone format

echo "server:" > "$OUTFILE"

grep '^0\.0\.0\.0' "$TMPFILE" | \

awk '{print $2}' | \

grep -v '0.0.0.0' | \

sort -u | \

while read domain; do

echo " local-zone: \"${domain}.\" always_nxdomain" >> "$OUTFILE"

done

rm -f "$TMPFILE"

# Validate configuration

/usr/sbin/unbound-checkconf > /dev/null 2>&1

if [ $? -eq 0 ]; then

# Reload Unbound to apply the new blocklist

/usr/sbin/unbound-control reload > /dev/null 2>&1

BLOCKED=$(grep -c 'local-zone' "$OUTFILE")

echo "Blocklist updated: ${BLOCKED} domains blocked"

else

echo "Configuration error after blocklist update"

echo "server:" > "$OUTFILE"

/usr/sbin/unbound-control reload > /dev/null 2>&1

exit 1

fi

Make the script executable and run it:

sh

chmod +x /var/unbound/update-blocklist.sh

/var/unbound/update-blocklist.sh

The StevenBlack consolidated hosts list blocks approximately 80,000-130,000 domains covering ads, trackers, malware, and fakenews sources. You can add additional lists by appending more fetch and processing steps.

Schedule weekly updates with a cron job:

sh

crontab -e

Add:


0 4 * * 0 /var/unbound/update-blocklist.sh >> /var/log/unbound-blocklist.log 2>&1

This updates the blocklist every Sunday at 4 AM.

To whitelist a domain that the blocklist incorrectly blocks, add it before the blocklist include in your main configuration or in a separate file:

yaml

# /var/unbound/conf.d/whitelist.conf

server:

local-zone: "example-allowed-domain.com." transparent

Whitelist entries loaded before the blocklist override the always_nxdomain directive.

Caching Tuning

DNS caching is where Unbound delivers its biggest performance win. Every cached response avoids a network round trip. The default cache sizes are conservative. On a server with available RAM, increase them.

Key cache parameters:

yaml

server:

# Message cache -- stores DNS response messages

msg-cache-size: 64m

# RRset cache -- stores individual resource record sets

# Should be roughly 2x msg-cache-size

rrset-cache-size: 128m

# Key cache -- stores DNSSEC keys

key-cache-size: 32m

# Negative cache -- stores NXDOMAIN and NODATA answers

neg-cache-size: 16m

# Prefetch almost-expired records in the background

prefetch: yes

# Prefetch DNSSEC keys ahead of expiry

prefetch-key: yes

# Serve stale cache entries while refreshing in the background

serve-expired: yes

serve-expired-ttl: 86400

# Minimum TTL -- never cache for less than this (seconds)

cache-min-ttl: 300

# Maximum TTL -- never cache for more than this (seconds)

cache-max-ttl: 86400

The prefetch option is particularly valuable. When a cached entry reaches 10% of its remaining TTL, Unbound proactively refreshes it in the background. The next client to request that domain gets an instant cache hit with fresh data instead of waiting for a full resolution.

The serve-expired option ensures that even if an upstream nameserver is temporarily unreachable, clients still get answers from cache (marked as stale). This dramatically improves reliability during upstream outages.

**Sizing guide:**

| Network size | msg-cache-size | rrset-cache-size | key-cache-size |

|---|---|---|---|

| Single server | 16m | 32m | 8m |

| Small LAN (1-10 devices) | 32m | 64m | 16m |

| Medium LAN (10-50 devices) | 64m | 128m | 32m |

| Large network (50+ devices) | 128m | 256m | 64m |

For the threading and slab parameters, set num-threads to match your CPU core count, and set all slab counts to a power of 2 close to num-threads:

yaml

server:

num-threads: 4

msg-cache-slabs: 4

rrset-cache-slabs: 4

infra-cache-slabs: 4

key-cache-slabs: 4

Integration with DHCP

If your FreeBSD machine serves as a router and [DHCP server](/blog/dhcp-server-freebsd/), configure DHCP to push the Unbound resolver address to all clients automatically.

For ISC DHCP (isc-dhcp44-server), add to /usr/local/etc/dhcpd.conf:


option domain-name-servers 10.0.0.1;

option domain-name "home.lab";

Replace 10.0.0.1 with the LAN IP of your FreeBSD resolver.

For dnsmasq setups, point dnsmasq at Unbound as the upstream:


server=127.0.0.1#53

For networks using DHCPv6, set the DNS server option in your RA (Router Advertisement) daemon or DHCPv6 server configuration to point at the Unbound host's IPv6 address.

After updating DHCP configuration, clients will receive your Unbound resolver on their next lease renewal. Force an immediate renewal on a test client:

sh

# On a FreeBSD client

dhclient -r em0 && dhclient em0

cat /etc/resolv.conf

Confirm the nameserver line points to your Unbound host.

Logging and Monitoring

Unbound provides detailed statistics through unbound-control. This requires the remote-control section to be enabled (included in the template above).

View current statistics:

sh

unbound-control stats_noreset

Key metrics to monitor:

- total.num.queries -- total queries received

- total.num.cachehits -- queries answered from cache

- total.num.cachemiss -- queries requiring upstream resolution

- total.num.recursivereplies -- completed recursive lookups

- total.requestlist.avg -- average outstanding queries (should be low)

- num.answer.rcode.NOERROR -- successful responses

- num.answer.rcode.SERVFAIL -- failures (watch for spikes)

- num.answer.rcode.NXDOMAIN -- non-existent domain responses

Calculate your cache hit ratio:

sh

unbound-control stats_noreset | grep 'total.num.cachehits\|total.num.cachemiss'

A healthy resolver should maintain a cache hit ratio above 70%. If it is lower, increase cache sizes or enable prefetch.

For temporary debugging, increase verbosity:

sh

unbound-control verbosity 3

Verbosity levels:

- 0 -- errors only

- 1 -- operational information (default)

- 2 -- detailed operational information

- 3 -- query-level information

- 4 -- algorithm-level information

- 5 -- cache miss information

Reset to normal after debugging:

sh

unbound-control verbosity 1

To log all queries permanently (generates significant I/O):

yaml

server:

log-queries: yes

log-replies: yes

log-tag-queryreply: yes

Logs go to syslog by default. View them with:

sh

grep unbound /var/log/messages | tail -50

For dedicated log files, configure syslog to route daemon.info to a separate file in /etc/syslog.conf.

Troubleshooting

Unbound Fails to Start

Check configuration syntax first:

sh

unbound-checkconf /var/unbound/unbound.conf

If this passes, check for port conflicts. Another DNS service may already be using port 53:

sh

sockstat -l -4 -6 | grep ':53 '

Common culprits: local_unbound (the default FreeBSD stub resolver), dnsmasq, or named. Stop the conflicting service before starting Unbound.

Queries Return SERVFAIL

SERVFAIL usually indicates a DNSSEC validation failure. Test without DNSSEC to confirm:

sh

drill -o CD freebsd.org @127.0.0.1

The -o CD flag sets the Checking Disabled bit. If this succeeds but a normal query fails, the issue is DNSSEC validation. Common causes:

- Stale root trust anchor. Run unbound-anchor -a /var/unbound/root.key and reload.

- System clock is wrong. DNSSEC signatures have validity periods. Run ntpdate pool.ntp.org or ensure ntpd is running.

- Upstream network blocks DNSSEC responses (rare but possible on some ISPs).

Slow Resolution

If initial queries are slow (but cached queries are fast), check connectivity to root servers:

sh

drill . NS @198.41.0.4

If this times out, your network may be blocking outbound port 53. Switch to forwarding mode with DNS-over-TLS (port 853) as described above.

Check Unbound statistics for abnormal query times:

sh

unbound-control stats_noreset | grep 'total.recursion.time'

Average recursion time should be under 100ms. If consistently higher, consider enabling forwarding to a nearby upstream resolver.

Testing DNSSEC End to End

Use these test domains to verify DNSSEC validation:

sh

# Should succeed (valid DNSSEC)

drill sigok.verteiltesysteme.net @127.0.0.1

# Should fail with SERVFAIL (invalid DNSSEC)

drill sigfail.verteiltesysteme.net @127.0.0.1

Checking Cache Contents

Dump the current cache for inspection:

sh

unbound-control dump_cache > /tmp/unbound-cache.txt

Search for a specific domain:

sh

grep "example.com" /tmp/unbound-cache.txt

Flush a single domain from cache:

sh

unbound-control flush example.com

Flush an entire zone:

sh

unbound-control flush_zone example.com

Complete Configuration Reference

For convenience, here is the full /var/unbound/unbound.conf from this guide as a single file ready to deploy:

yaml

server:

interface: 0.0.0.0

interface: ::0

port: 53

do-ip4: yes

do-ip6: yes

do-udp: yes

do-tcp: yes

access-control: 127.0.0.0/8 allow

access-control: 10.0.0.0/8 allow

access-control: 172.16.0.0/12 allow

access-control: 192.168.0.0/16 allow

access-control: ::1/128 allow

access-control: fd00::/8 allow

access-control: 0.0.0.0/0 refuse

access-control: ::0/0 refuse

username: "unbound"

directory: "/var/unbound"

chroot: "/var/unbound"

verbosity: 1

use-syslog: yes

log-queries: no

log-replies: no

hide-identity: yes

hide-version: yes

harden-glue: yes

harden-dnssec-stripped: yes

harden-referral-path: yes

harden-algo-downgrade: yes

use-caps-for-id: yes

qname-minimisation: yes

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

root-hints: "/var/unbound/root.hints"

msg-cache-size: 64m

rrset-cache-size: 128m

key-cache-size: 32m

neg-cache-size: 16m

prefetch: yes

prefetch-key: yes

serve-expired: yes

serve-expired-ttl: 86400

cache-min-ttl: 300

cache-max-ttl: 86400

num-threads: 2

msg-cache-slabs: 4

rrset-cache-slabs: 4

infra-cache-slabs: 4

key-cache-slabs: 4

so-reuseport: yes

so-rcvbuf: 4m

so-sndbuf: 4m

outgoing-range: 8192

num-queries-per-thread: 4096

minimal-responses: yes

private-address: 10.0.0.0/8

private-address: 172.16.0.0/12

private-address: 192.168.0.0/16

private-address: 169.254.0.0/16

private-address: fd00::/8

private-address: fe80::/10

include: "/var/unbound/conf.d/*.conf"

remote-control:

control-enable: yes

control-interface: 127.0.0.1

control-port: 8953

server-key-file: "/var/unbound/unbound_server.key"

server-cert-file: "/var/unbound/unbound_server.pem"

control-key-file: "/var/unbound/unbound_control.key"

control-cert-file: "/var/unbound/unbound_control.pem"

Frequently Asked Questions

Should I use recursive resolution or forwarding mode?

Use recursive resolution (the default) when your server has unrestricted outbound access on port 53. This is the most private option because no single upstream sees all your queries. Use forwarding with DNS-over-TLS when your network blocks outbound port 53, when you want the performance benefits of a major resolver's anycast network, or when you are behind a restrictive corporate firewall.

Does Unbound replace local_unbound on FreeBSD?

FreeBSD ships local_unbound as a lightweight stub resolver for the system itself. The local_unbound service uses the same Unbound binary but with an auto-generated minimal configuration. If you configure Unbound as a full resolver using this guide, disable local_unbound to avoid port conflicts:

sh

sysrc local_unbound_enable="NO"

service local_unbound stop

sysrc unbound_enable="YES"

service unbound start

Alternatively, you can use the local_unbound service name and simply replace its configuration with the full configuration from this guide. The binary is the same.

How much RAM does Unbound use?

With the cache sizes in this guide (64m message cache, 128m RRset cache, 32m key cache), Unbound typically uses 250-400 MB of RAM under load. On a minimal configuration with default cache sizes, it uses under 50 MB. Adjust cache sizes based on your available memory. Even 16m/32m provides significant caching benefit for a single server.

Can Unbound and PF work together for DNS filtering?

Yes. Use PF to enforce that all LAN clients use your resolver by redirecting any DNS traffic that tries to bypass it:


# In /etc/pf.conf -- redirect all DNS to local Unbound

rdr on $lan_if proto { tcp, udp } from $lan_net to !$self port 53 -> 127.0.0.1 port 53

This intercepts DNS queries from devices that have hardcoded DNS servers (like Google Home devices using 8.8.8.8) and forces them through your Unbound instance with its ad blocking and DNSSEC validation.

How do I update the root hints file?

The root hints file (/var/unbound/root.hints) lists the IP addresses of the 13 root nameservers. It changes rarely, but should be updated every six months:

sh

fetch -o /var/unbound/root.hints https://www.internic.net/domain/named.root

service local_unbound reload

Add this to a periodic task or cron job for automatic updates.

Can I use Unbound with jails?

Yes. Run Unbound on the host and configure jails to use the host's IP as their DNS resolver. In the jail configuration or within each jail's /etc/resolv.conf, set the nameserver to the host's loopback or bridge IP. Ensure Unbound's access-control permits queries from the jail subnet.

How do I monitor blocked domains from the ad blocklist?

Enable query logging temporarily to see what is being blocked:

sh

unbound-control verbosity 3

Then watch the logs:

sh

tail -f /var/log/messages | grep unbound

Blocked domains will show responses with NXDOMAIN. Reset verbosity after debugging to avoid excessive logging.

Summary

Unbound on FreeBSD gives you a private, validated, cached DNS resolver with zero package installations. The base system includes everything needed. Enable it in rc.conf, configure access control for your network, turn on DNSSEC validation, and optionally add DNS-over-TLS forwarding and ad blocking.

For a complete network stack, combine Unbound with a [FreeBSD router](/blog/freebsd-router-gateway/) for NAT and packet filtering, a [DHCP server](/blog/dhcp-server-freebsd/) to push DNS settings to clients automatically, and [server hardening](/blog/hardening-freebsd-server/) to lock down the host itself. This gives every device on your network fast, validated, ad-free DNS resolution controlled entirely by your own infrastructure.