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:
shsysrc local_unbound_enable="YES"
Generate the initial configuration:
shlocal-unbound-setup
This creates /var/unbound/unbound.conf with sensible defaults. For custom configuration, edit /var/unbound/unbound.conf:
shcat > /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:
shfetch -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:
shcat >> /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:
shcat >> /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
shpkg install bind918 sysrc named_enable="YES"
Authoritative Configuration
Configure BIND as an authoritative-only server (no recursion):
shcat > /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
shmkdir -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:
shzone "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
shpkg install nsd sysrc nsd_enable="YES"
shcat > /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:
shmkdir -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:
shellzone "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:
shrndc 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):
shpkg 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:
shellzone: name: "example.com" zonefile: "example.com.zone.signed"
Resign periodically (before signatures expire) using a cron job:
shecho '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
shcat >> /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:
shellacl "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
shelloptions { // 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?
shdrill @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.