How to Set Up BIND DNS Server on FreeBSD
BIND (Berkeley Internet Name Domain) is the most widely deployed DNS server software in the world. It handles authoritative DNS for the majority of domains on the internet and serves as a recursive resolver for millions of networks. On FreeBSD, BIND has been part of the base system for decades, though modern FreeBSD ships a trimmed-down version and the full-featured BIND 9 is available from packages.
This guide covers a complete BIND 9 deployment on FreeBSD 14.x: installing from packages, configuring authoritative zones for your domains, setting up recursive resolution, signing zones with DNSSEC, implementing split-horizon DNS for internal/external views, configuring structured logging, and hardening the server against common attacks.
If you need a caching-only recursive resolver without authoritative zones, consider Unbound instead -- it is simpler and ships in the FreeBSD base system.
When to Use BIND vs Unbound
BIND and Unbound serve different primary purposes:
- BIND: full-featured authoritative and recursive DNS server. Use it when you host DNS zones for your domains, need split-horizon DNS, or require DNSSEC zone signing.
- Unbound: recursive resolver and cache only. Use it for local DNS resolution, DNSSEC validation, and DNS-over-TLS forwarding.
You can run both: BIND as your authoritative server facing the internet, and Unbound as your internal recursive resolver. This guide focuses on BIND.
Installing BIND 9
The FreeBSD base system includes a limited DNS server. Install the full BIND 9 from packages:
shpkg install bind918
The package name includes the version. Check available versions with pkg search bind9 if you need a specific release.
Enable BIND at boot:
shsysrc named_enable="YES"
The configuration file is /usr/local/etc/namedb/named.conf. The package creates a default configuration and directory structure.
Directory Structure
BIND on FreeBSD uses this layout:
shell/usr/local/etc/namedb/ named.conf -- main configuration named.conf.local -- zone definitions (included by named.conf) named.conf.options -- options (included by named.conf) master/ -- authoritative zone files you manage slave/ -- zone files replicated from master servers dynamic/ -- dynamically updated zones keys/ -- TSIG and DNSSEC keys working/ -- runtime state (managed.keys, etc.)
Basic Configuration
Edit the main options:
shvi /usr/local/etc/namedb/named.conf.options
shelloptions { directory "/usr/local/etc/namedb/working"; pid-file "/var/run/named/pid"; // Listen on all interfaces listen-on { any; }; listen-on-v6 { any; }; // Allow queries from your networks allow-query { localhost; 10.0.0.0/8; 192.168.0.0/16; }; // Allow recursive queries only from internal networks allow-recursion { localhost; 10.0.0.0/8; 192.168.0.0/16; }; // Forward unresolved queries to upstream resolvers forwarders { 1.1.1.1; 9.9.9.9; }; // DNSSEC validation dnssec-validation auto; // Disable version reporting version "not disclosed"; // Rate limiting rate-limit { responses-per-second 10; window 5; }; // Query logging (disabled by default for performance) querylog no; };
Verify the configuration:
shnamed-checkconf /usr/local/etc/namedb/named.conf
Start BIND:
shservice named start
Test basic resolution:
shdig @127.0.0.1 freebsd.org
Configuring Authoritative Zones
To host DNS for your own domain, create zone files and add zone declarations.
Forward Zone
Declare the zone in named.conf.local:
shvi /usr/local/etc/namedb/named.conf.local
shellzone "example.com" { type master; file "/usr/local/etc/namedb/master/example.com.zone"; allow-transfer { 10.0.0.2; }; // secondary DNS server IP }; zone "0.0.10.in-addr.arpa" { type master; file "/usr/local/etc/namedb/master/10.0.0.rev"; allow-transfer { 10.0.0.2; }; };
Create the zone file:
shvi /usr/local/etc/namedb/master/example.com.zone
shell$TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( 2026040901 ; Serial (YYYYMMDDNN) 3600 ; Refresh (1 hour) 900 ; Retry (15 minutes) 604800 ; Expire (1 week) 86400 ; Minimum TTL (1 day) ) ; Name servers @ IN NS ns1.example.com. @ IN NS ns2.example.com. ; A records @ IN A 203.0.113.10 ns1 IN A 203.0.113.10 ns2 IN A 203.0.113.11 www IN A 203.0.113.10 mail IN A 203.0.113.12 ; AAAA records @ IN AAAA 2001:db8::10 www IN AAAA 2001:db8::10 ; CNAME records ftp IN CNAME www.example.com. blog IN CNAME www.example.com. ; MX records @ IN MX 10 mail.example.com. @ IN MX 20 mail2.example.com. ; TXT records @ IN TXT "v=spf1 mx a -all"
Reverse Zone
Create the reverse DNS zone:
shvi /usr/local/etc/namedb/master/10.0.0.rev
shell$TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( 2026040901 ; Serial 3600 ; Refresh 900 ; Retry 604800 ; Expire 86400 ; Minimum TTL ) @ IN NS ns1.example.com. @ IN NS ns2.example.com. 10 IN PTR example.com. 11 IN PTR ns2.example.com. 12 IN PTR mail.example.com.
Validate zone files:
shnamed-checkzone example.com /usr/local/etc/namedb/master/example.com.zone named-checkzone 0.0.10.in-addr.arpa /usr/local/etc/namedb/master/10.0.0.rev
Reload BIND to load the new zones:
shrndc reload
Test:
shdig @127.0.0.1 example.com A dig @127.0.0.1 www.example.com A dig @127.0.0.1 example.com MX dig @127.0.0.1 -x 203.0.113.10
Setting Up a Secondary (Slave) DNS Server
On the secondary server, install BIND and declare the zone as a slave:
shvi /usr/local/etc/namedb/named.conf.local
shellzone "example.com" { type slave; masters { 10.0.0.1; }; file "/usr/local/etc/namedb/slave/example.com.zone"; }; zone "0.0.10.in-addr.arpa" { type slave; masters { 10.0.0.1; }; file "/usr/local/etc/namedb/slave/10.0.0.rev"; };
Start BIND on the secondary. It will automatically transfer the zones from the primary:
shservice named start
Verify the transfer:
shdig @10.0.0.2 example.com SOA ls -la /usr/local/etc/namedb/slave/
Securing Zone Transfers with TSIG
Generate a shared secret:
shtsig-keygen -a hmac-sha256 transfer-key > /usr/local/etc/namedb/keys/transfer-key.conf
View the generated key:
shcat /usr/local/etc/namedb/keys/transfer-key.conf
Include the key on both primary and secondary:
shvi /usr/local/etc/namedb/named.conf
Add at the top:
shellinclude "/usr/local/etc/namedb/keys/transfer-key.conf";
On the primary, restrict transfers to the key:
shellzone "example.com" { type master; file "/usr/local/etc/namedb/master/example.com.zone"; allow-transfer { key transfer-key; }; };
On the secondary, use the key when transferring:
shellserver 10.0.0.1 { keys { transfer-key; }; };
Reload both servers:
shrndc reload
DNSSEC Zone Signing
DNSSEC adds cryptographic signatures to your DNS records, allowing resolvers to verify that responses have not been tampered with.
Generate Keys
Create the Zone Signing Key (ZSK) and Key Signing Key (KSK):
shmkdir -p /usr/local/etc/namedb/keys/example.com cd /usr/local/etc/namedb/keys/example.com dnssec-keygen -a ECDSAP256SHA256 -n ZONE example.com dnssec-keygen -a ECDSAP256SHA256 -n ZONE -f KSK example.com
This generates two key pairs (four files total). Move them or note their paths.
Sign the Zone
shcd /usr/local/etc/namedb/master dnssec-signzone -A -3 $(head -c 16 /dev/urandom | od -A n -t x1 | tr -d ' \n') \ -N INCREMENT \ -o example.com \ -t \ -K /usr/local/etc/namedb/keys/example.com \ example.com.zone
This creates example.com.zone.signed. Update the zone declaration to use the signed file:
shellzone "example.com" { type master; file "/usr/local/etc/namedb/master/example.com.zone.signed"; allow-transfer { key transfer-key; }; auto-dnssec maintain; inline-signing yes; };
With inline-signing yes and auto-dnssec maintain, BIND automatically re-signs the zone when records change or signatures approach expiration.
Publish the DS Record
Extract the DS record to publish at your registrar:
shdnssec-dsfromkey /usr/local/etc/namedb/keys/example.com/Kexample.com.+013+*.key
Submit the DS record to your domain registrar's DNSSEC settings page.
Verify DNSSEC is working:
shdig @127.0.0.1 example.com DNSKEY +dnssec dig @127.0.0.1 example.com A +dnssec
Check with an external validator:
shdig +trace +dnssec example.com A
Split-Horizon DNS
Split-horizon DNS returns different answers depending on who is asking. Internal clients see private IP addresses; external clients see public IPs.
Configure views in named.conf:
shvi /usr/local/etc/namedb/named.conf
shellacl internal { 10.0.0.0/8; 192.168.0.0/16; 127.0.0.1; }; view "internal" { match-clients { internal; }; zone "example.com" { type master; file "/usr/local/etc/namedb/master/example.com.internal.zone"; }; // Allow recursion for internal clients recursion yes; forwarders { 1.1.1.1; 9.9.9.9; }; }; view "external" { match-clients { any; }; zone "example.com" { type master; file "/usr/local/etc/namedb/master/example.com.external.zone"; allow-transfer { key transfer-key; }; }; // No recursion for external clients recursion no; };
Create the internal zone with private IPs:
shvi /usr/local/etc/namedb/master/example.com.internal.zone
shell$TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( 2026040901 3600 900 604800 86400 ) @ IN NS ns1.example.com. @ IN A 10.0.0.10 ns1 IN A 10.0.0.10 www IN A 10.0.0.10 mail IN A 10.0.0.12 gitlab IN A 10.0.0.20 jenkins IN A 10.0.0.21
The external zone contains public IPs and only publicly accessible records. Internal-only services like GitLab and Jenkins appear only in the internal view.
Logging
Configure structured logging for troubleshooting and audit:
shvi /usr/local/etc/namedb/named.conf
Add the logging section:
shelllogging { channel default_log { file "/var/log/named/default.log" versions 5 size 10m; severity info; print-time yes; print-severity yes; print-category yes; }; channel query_log { file "/var/log/named/queries.log" versions 3 size 50m; severity info; print-time yes; }; channel security_log { file "/var/log/named/security.log" versions 5 size 10m; severity info; print-time yes; print-severity yes; }; channel xfer_log { file "/var/log/named/transfers.log" versions 3 size 10m; severity info; print-time yes; }; category default { default_log; }; category queries { query_log; }; category security { security_log; }; category xfer-in { xfer_log; }; category xfer-out { xfer_log; }; };
Create the log directory:
shmkdir -p /var/log/named chown bind:bind /var/log/named
Enable query logging at runtime (for debugging):
shrndc querylog on
Disable when done:
shrndc querylog off
Security Hardening
Restrict Recursion
Never allow open recursion. Limit recursive queries to your own networks:
shallow-recursion { localhost; 10.0.0.0/8; 192.168.0.0/16; };
Rate Limiting
Prevent DNS amplification attacks:
shellrate-limit { responses-per-second 10; window 5; log-only no; };
Run BIND in a Chroot (Optional)
FreeBSD can run BIND in a chroot jail for additional isolation:
shsysrc named_chrootdir="/var/named"
The init script handles creating the chroot directory structure. Restart to apply:
shservice named restart
Restrict Version Query
Prevent attackers from fingerprinting your BIND version:
shelloptions { version "not disclosed"; hostname "not disclosed"; };
Firewall Rules
Allow DNS traffic only from expected sources. With PF:
shell# Allow DNS from internal networks pass in on $int_if proto { tcp, udp } from 10.0.0.0/8 to any port 53 # Allow DNS from internet (if authoritative) pass in on $ext_if proto { tcp, udp } from any to $ext_ip port 53 # Allow zone transfers only from secondary pass in on $ext_if proto tcp from 10.0.0.2 to $ext_ip port 53
Updating Zone Records
When you modify a zone file:
- Increment the serial number (format: YYYYMMDDNN)
- Validate the zone:
shnamed-checkzone example.com /usr/local/etc/namedb/master/example.com.zone
- Reload the zone:
shrndc reload example.com
For dynamic updates (e.g., from DHCP or automated scripts), use nsupdate:
shnsupdate -k /usr/local/etc/namedb/keys/update-key.private > server 127.0.0.1 > zone example.com > update add newhost.example.com 86400 A 10.0.0.50 > send > quit
Troubleshooting
BIND fails to start:
Check configuration syntax:
shnamed-checkconf /usr/local/etc/namedb/named.conf
Check zone files:
shnamed-checkzone example.com /usr/local/etc/namedb/master/example.com.zone
Review logs:
shtail -50 /var/log/named/default.log
Zone transfers fail:
Check that allow-transfer on the primary includes the secondary's IP or TSIG key. Verify connectivity:
shdig @10.0.0.1 example.com AXFR
Check firewall rules allow TCP port 53 between the servers.
DNSSEC validation failures:
Check that the DS record at the registrar matches the KSK:
shdig example.com DS dnssec-dsfromkey /usr/local/etc/namedb/keys/example.com/Kexample.com.+013+*.key
If signatures have expired, re-sign the zone or verify auto-dnssec maintain is set.
High CPU or memory usage:
Disable query logging if enabled. Check for DNS amplification attacks in the query log. Verify rate limiting is active. Consider reducing the maximum cache size:
shelloptions { max-cache-size 256m; };
FAQ
Should I use BIND or Unbound for internal DNS?
If you only need a caching recursive resolver, use Unbound -- it is simpler, faster for that specific task, and ships in the FreeBSD base system. Use BIND if you need authoritative zones, split-horizon views, dynamic updates, or DNSSEC zone signing.
How do I migrate from a hosted DNS provider to self-hosted BIND?
Export your zone records from the provider. Convert them to BIND zone file format. Set up BIND with those zones. Update the NS records at your registrar to point to your BIND servers. Keep the old provider active during the TTL transition period.
Can BIND and Unbound run on the same server?
Yes, but they cannot both listen on port 53 on the same IP. Run BIND on the external IP for authoritative zones and Unbound on 127.0.0.1 for recursive resolution. Or use different ports.
How often should I rotate DNSSEC keys?
ZSK should be rotated every 1-3 months. KSK rotation is less frequent -- every 1-2 years. With auto-dnssec maintain, BIND handles ZSK rotation automatically if you pre-generate keys. KSK rotation requires updating the DS record at your registrar.
What is the maximum number of zones BIND can handle?
BIND can handle tens of thousands of zones on modern hardware. Performance depends on query rate, zone size, and available memory. For very large deployments (10,000+ zones), consider using dlz (dynamically loadable zones) with a database backend.
Does BIND support DNS-over-HTTPS or DNS-over-TLS?
BIND 9.18+ supports DNS-over-TLS (DoT) natively with the tls configuration block. DNS-over-HTTPS (DoH) support is available in recent versions. For earlier BIND versions, use a TLS proxy like stunnel or haproxy in front of BIND.