FreeBSD.software
Home/Guides/FreeBSD DNS Administration Guide
guide·2026-04-09·10 min read

FreeBSD DNS Administration Guide

FreeBSD DNS administration: Unbound recursive resolver, BIND authoritative server, NSD, DNSSEC signing and validation, split DNS, caching, and performance tuning.

FreeBSD DNS Administration Guide

DNS is one of the workloads where FreeBSD genuinely excels. The networking stack is fast and predictable. The base system includes Unbound as a caching resolver. BIND and NSD are available as packages. The combination of FreeBSD's stability, ZFS for zone file storage, and jails for isolating DNS roles makes it a natural choice for DNS infrastructure.

This guide covers setting up Unbound as a recursive resolver, BIND as an authoritative server, NSD as a lightweight authoritative alternative, DNSSEC for both signing and validation, split DNS configurations, and performance tuning.

DNS Architecture on FreeBSD

Before configuring anything, understand the two fundamentally different DNS roles:

Recursive resolver -- Takes queries from clients, follows the DNS delegation chain from root servers to authoritative servers, caches results, and returns answers. Unbound is the best choice for this role on FreeBSD.

Authoritative server -- Holds zone data (your domain's DNS records) and answers queries about those zones directly. BIND or NSD are the choices here.

Running both roles on the same machine is possible but not recommended for production. Separate them into different jails or different machines.

Unbound: Recursive Resolver

Unbound is part of the FreeBSD base system since FreeBSD 10. It is a validating, recursive, caching DNS resolver designed for security and performance.

Basic Setup

Unbound is already installed. Enable and configure it:

sh
sysrc local_unbound_enable="YES"

Generate the initial configuration:

sh
local-unbound-setup

This creates /var/unbound/unbound.conf with sensible defaults. For custom configuration, edit /var/unbound/unbound.conf:

sh
cat > /var/unbound/unbound.conf << 'EOF' server: interface: 0.0.0.0 interface: ::0 port: 53 access-control: 10.0.0.0/8 allow access-control: 127.0.0.0/8 allow access-control: ::1/128 allow # Performance num-threads: 4 msg-cache-slabs: 4 rrset-cache-slabs: 4 infra-cache-slabs: 4 key-cache-slabs: 4 msg-cache-size: 128m rrset-cache-size: 256m outgoing-range: 8192 num-queries-per-thread: 4096 # Privacy hide-identity: yes hide-version: yes qname-minimisation: yes # Security harden-glue: yes harden-dnssec-stripped: yes harden-referral-path: yes use-caps-for-id: yes # DNSSEC auto-trust-anchor-file: "/var/unbound/root.key" # Logging verbosity: 1 log-queries: no logfile: "/var/log/unbound.log" # Root hints root-hints: "/var/unbound/root.hints" # Prefetch popular records before TTL expires prefetch: yes prefetch-key: yes # Serve stale data while refreshing serve-expired: yes serve-expired-ttl: 86400 remote-control: control-enable: yes control-interface: 127.0.0.1 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" EOF

Update root hints and start:

sh
fetch -o /var/unbound/root.hints https://www.internic.net/domain/named.cache unbound-control-setup service local_unbound start

Testing Unbound

sh
# Query through Unbound drill @127.0.0.1 example.com A drill @127.0.0.1 example.com AAAA # Check DNSSEC validation drill -D @127.0.0.1 example.com # Check cache statistics unbound-control stats_noreset # Flush cache unbound-control flush example.com unbound-control flush_zone example.com # Dump cache unbound-control dump_cache > /tmp/unbound-cache.txt

Forwarding Configuration

To forward queries to an upstream resolver instead of resolving recursively:

sh
cat >> /var/unbound/unbound.conf << 'EOF' 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: 9.9.9.9@853#dns.quad9.net EOF

This uses DNS-over-TLS for privacy. Unbound validates DNSSEC locally even when forwarding.

Local Zone Overrides

Add local DNS records for internal services:

sh
cat >> /var/unbound/unbound.conf << 'EOF' server: local-zone: "internal.example.com." static local-data: "app.internal.example.com. IN A 10.0.0.20" local-data: "db.internal.example.com. IN A 10.0.0.21" local-data: "cache.internal.example.com. IN A 10.0.0.22" # Block ad domains local-zone: "ads.example.net." always_refuse EOF service local_unbound reload

BIND: Authoritative DNS Server

BIND is the most widely deployed DNS server software. On FreeBSD, use it for authoritative zones when you need DNSSEC signing, dynamic updates, or complex zone configurations.

Installation

sh
pkg install bind918 sysrc named_enable="YES"

Authoritative Configuration

Configure BIND as an authoritative-only server (no recursion):

sh
cat > /usr/local/etc/namedb/named.conf << 'EOF' options { directory "/usr/local/etc/namedb/working"; pid-file "/var/run/named/pid"; dump-file "/var/dump/named_dump.db"; statistics-file "/var/stats/named.stats"; listen-on { any; }; listen-on-v6 { any; }; // Authoritative only -- no recursion recursion no; allow-transfer { none; }; // Security version "not disclosed"; hostname "not disclosed"; }; // Logging logging { channel default_log { file "/var/log/named/default.log" versions 5 size 10m; severity info; print-time yes; print-category yes; }; category default { default_log; }; category queries { default_log; }; }; // Primary zone zone "example.com" { type primary; file "/usr/local/etc/namedb/primary/example.com.zone"; allow-transfer { 10.0.1.2; }; // Secondary NS notify yes; }; // Reverse zone zone "0.0.10.in-addr.arpa" { type primary; file "/usr/local/etc/namedb/primary/10.0.0.rev"; allow-transfer { 10.0.1.2; }; notify yes; }; EOF

Zone File

sh
mkdir -p /usr/local/etc/namedb/primary cat > /usr/local/etc/namedb/primary/example.com.zone << 'EOF' $TTL 3600 @ IN SOA ns1.example.com. admin.example.com. ( 2026040901 ; Serial (YYYYMMDDNN) 3600 ; Refresh 900 ; Retry 604800 ; Expire 300 ; Negative TTL ) ; Name servers IN NS ns1.example.com. IN NS ns2.example.com. ; Mail IN MX 10 mail.example.com. ; A records @ IN A 10.0.0.10 ns1 IN A 10.0.0.2 ns2 IN A 10.0.1.2 mail IN A 10.0.0.30 www IN A 10.0.0.10 app IN A 10.0.0.20 db IN A 10.0.0.21 ; AAAA records @ IN AAAA 2001:db8::10 www IN AAAA 2001:db8::10 ; CNAME records ftp IN CNAME www.example.com. docs IN CNAME www.example.com. ; TXT records @ IN TXT "v=spf1 mx -all" _dmarc IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com" EOF

Secondary (Replica) Configuration

On the secondary DNS server:

sh
zone "example.com" { type secondary; file "/usr/local/etc/namedb/secondary/example.com.zone"; primaries { 10.0.0.2; }; };

Starting and Testing BIND

sh
# Check configuration syntax named-checkconf /usr/local/etc/namedb/named.conf # Check zone file syntax named-checkzone example.com /usr/local/etc/namedb/primary/example.com.zone # Start service named start # Test drill @10.0.0.2 example.com SOA drill @10.0.0.2 www.example.com A

NSD: Lightweight Authoritative Server

NSD (Name Server Daemon) is an authoritative-only DNS server from NLnet Labs (the same team that makes Unbound). It is lighter than BIND and focused solely on serving authoritative zones.

Installation and Configuration

sh
pkg install nsd sysrc nsd_enable="YES"
sh
cat > /usr/local/etc/nsd/nsd.conf << 'EOF' server: server-count: 4 ip-address: 0.0.0.0 ip-address: ::0 port: 53 hide-version: yes identity: "" zonesdir: "/usr/local/etc/nsd/zones" database: "" logfile: "/var/log/nsd.log" pidfile: "/var/run/nsd/nsd.pid" remote-control: control-enable: yes control-interface: 127.0.0.1 control-port: 8952 zone: name: "example.com" zonefile: "example.com.zone" notify: 10.0.1.2 NOKEY provide-xfr: 10.0.1.2 NOKEY EOF

NSD uses the same zone file format as BIND:

sh
mkdir -p /usr/local/etc/nsd/zones cp /usr/local/etc/namedb/primary/example.com.zone /usr/local/etc/nsd/zones/ # Check and start nsd-checkconf /usr/local/etc/nsd/nsd.conf nsd-checkzone example.com /usr/local/etc/nsd/zones/example.com.zone service nsd start

NSD vs BIND for Authoritative DNS

| Feature | NSD | BIND |

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

| Role | Authoritative only | Authoritative + recursive |

| Memory usage | Lower | Higher |

| Zone reloads | Fast (nsd-control reload) | Fast (rndc reload) |

| Dynamic updates | No | Yes (nsupdate) |

| DNSSEC signing | Offline (ldns-signzone) | Inline signing |

| Complexity | Low | High |

| Config format | Simple | Complex |

Choose NSD when you want a fast, simple authoritative server. Choose BIND when you need dynamic updates, inline DNSSEC signing, or complex zone policies.

DNSSEC

DNSSEC Validation (Unbound)

Unbound validates DNSSEC by default when configured with auto-trust-anchor-file. Test it:

sh
# This should return SERVFAIL (intentionally broken DNSSEC) drill @127.0.0.1 dnssec-failed.org # This should return a valid response with AD flag drill -D @127.0.0.1 example.com

DNSSEC Signing with BIND (Inline)

BIND can sign zones automatically using inline signing:

sh
# Generate KSK and ZSK cd /usr/local/etc/namedb/keys dnssec-keygen -a ECDSAP256SHA256 -f KSK example.com dnssec-keygen -a ECDSAP256SHA256 example.com

Update the zone configuration:

shell
zone "example.com" { type primary; file "/usr/local/etc/namedb/primary/example.com.zone"; inline-signing yes; auto-dnssec maintain; key-directory "/usr/local/etc/namedb/keys"; allow-transfer { 10.0.1.2; }; notify yes; };

Reload BIND:

sh
rndc reload example.com rndc signing -list example.com

DNSSEC Signing with NSD (Offline)

NSD does not sign zones itself. Use ldns-signzone (from the ldns package):

sh
pkg install ldns # Generate keys ldns-keygen -a ECDSAP256SHA256 -k example.com # KSK ldns-keygen -a ECDSAP256SHA256 example.com # ZSK # Sign the zone ldns-signzone -n example.com.zone Kexample.com.+013+*.private # The signed zone is example.com.zone.signed # Update NSD to use it

Update /usr/local/etc/nsd/nsd.conf:

shell
zone: name: "example.com" zonefile: "example.com.zone.signed"

Resign periodically (before signatures expire) using a cron job:

sh
echo '0 3 * * 0 root ldns-signzone -n /usr/local/etc/nsd/zones/example.com.zone /usr/local/etc/nsd/keys/Kexample.com.+013+*.private && nsd-control reload example.com' >> /etc/crontab

Split DNS

Split DNS serves different answers depending on where the query comes from -- internal clients get internal IPs, external clients get public IPs.

Split DNS with Unbound

sh
cat >> /var/unbound/unbound.conf << 'EOF' # Internal view -- override public DNS for internal clients server: local-zone: "app.example.com." redirect local-data: "app.example.com. IN A 10.0.0.20" local-zone: "db.example.com." transparent local-data: "db.example.com. IN A 10.0.0.21" EOF

This makes internal clients resolve app.example.com to the internal IP while external clients get the public IP from the authoritative DNS server.

Split DNS with BIND (Views)

BIND supports views for true split DNS:

shell
acl "internal" { 10.0.0.0/8; 172.16.0.0/12; 192.168.0.0/16; 127.0.0.0/8; }; view "internal" { match-clients { internal; }; recursion yes; zone "example.com" { type primary; file "/usr/local/etc/namedb/primary/example.com.internal.zone"; }; }; view "external" { match-clients { any; }; recursion no; zone "example.com" { type primary; file "/usr/local/etc/namedb/primary/example.com.external.zone"; }; };

The internal zone file contains private IPs. The external zone file contains public IPs. BIND serves the correct version based on the client's source address.

Performance Tuning

Unbound Tuning

sh
# Match threads to CPU cores num-threads: 4 # Size cache slabs to match threads (power of 2) msg-cache-slabs: 4 rrset-cache-slabs: 4 infra-cache-slabs: 4 key-cache-slabs: 4 # Large cache for busy resolvers msg-cache-size: 256m rrset-cache-size: 512m # Should be 2x msg-cache # Increase outgoing connections outgoing-range: 8192 num-queries-per-thread: 4096 # Prefetch before TTL expires prefetch: yes # Serve stale while refreshing serve-expired: yes

BIND Tuning

shell
options { // Worker threads // BIND auto-tunes this, but you can override // recursive-clients 10000; // Cache size max-cache-size 512m; // Minimize responses minimal-responses yes; // Disable unused features empty-zones-enable no; };

System-Level Tuning

sh
# Increase UDP buffer sizes sysctl net.inet.udp.maxdgram=65535 sysctl net.inet.udp.recvspace=262144 sysctl kern.ipc.maxsockbuf=8388608 # Make persistent cat >> /etc/sysctl.conf << 'EOF' net.inet.udp.maxdgram=65535 net.inet.udp.recvspace=262144 kern.ipc.maxsockbuf=8388608 EOF

Monitoring DNS Performance

sh
# Unbound statistics unbound-control stats # Key metrics unbound-control stats_noreset | grep total.num unbound-control stats_noreset | grep cache.count unbound-control stats_noreset | grep time.avg # BIND statistics rndc stats cat /var/stats/named.stats # NSD statistics nsd-control stats

Running DNS in Jails

Isolate DNS roles in separate jails:

sh
# Create resolver jail bastille create resolver 14.2-RELEASE 10.0.0.2 bastille pkg resolver install unbound bastille cmd resolver sysrc local_unbound_enable="YES" # Create authoritative jail bastille create authoritative 14.2-RELEASE 10.0.0.3 bastille pkg authoritative install nsd bastille cmd authoritative sysrc nsd_enable="YES"

This provides security isolation -- a compromise of the authoritative server does not affect the resolver, and vice versa.

FAQ

Should I use Unbound or BIND for a local resolver?

Unbound. It is designed specifically for recursive resolution, is part of the FreeBSD base system, uses less memory than BIND for this role, and validates DNSSEC by default. BIND can do recursive resolution but is overkill when you only need a resolver.

Can I run Unbound and BIND on the same machine?

Yes, on different ports or different IP addresses. A common pattern: Unbound on 127.0.0.1:53 for local resolution, BIND on the public IP:53 for authoritative responses. Or put each in a separate jail with its own IP address.

How do I troubleshoot DNS resolution failures?

sh
drill @127.0.0.1 example.com A # Test your resolver drill @8.8.8.8 example.com A # Test external resolver drill -T example.com # Trace the delegation chain unbound-control lookup example.com # Check Unbound's view

What is the difference between NSD and BIND for authoritative DNS?

NSD is smaller, faster for pure zone serving, and simpler to configure. BIND supports dynamic updates (nsupdate), inline DNSSEC signing, and complex policies (views, RPZ). If you just serve static zones, NSD is better. If you need dynamic features, use BIND.

How often should I resign DNSSEC zones?

DNSSEC signatures have an expiry date. The default signature validity in most tools is 30 days. Resign at least every 2 weeks to ensure signatures never expire. Automate this with a cron job or use BIND's inline signing which handles it automatically.

How much memory does Unbound need?

With default settings, Unbound uses about 50-100MB. With large caches (msg-cache-size: 256m, rrset-cache-size: 512m), it can use 800MB-1GB. Size your caches based on your query volume -- a busy resolver serving 10,000 queries per second benefits from large caches. A small office resolver does fine with defaults.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.