FreeBSD.software
Home/Guides/How to Set Up BIND DNS Server on FreeBSD
tutorial·2026-04-09·11 min read

How to Set Up BIND DNS Server on FreeBSD

Set up BIND 9 DNS server on FreeBSD: authoritative zones, recursive resolver, DNSSEC signing, split-horizon DNS, logging, and security hardening.

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:

sh
pkg 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:

sh
sysrc 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:

sh
vi /usr/local/etc/namedb/named.conf.options
shell
options { 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:

sh
named-checkconf /usr/local/etc/namedb/named.conf

Start BIND:

sh
service named start

Test basic resolution:

sh
dig @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:

sh
vi /usr/local/etc/namedb/named.conf.local
shell
zone "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:

sh
vi /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:

sh
vi /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:

sh
named-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:

sh
rndc reload

Test:

sh
dig @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:

sh
vi /usr/local/etc/namedb/named.conf.local
shell
zone "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:

sh
service named start

Verify the transfer:

sh
dig @10.0.0.2 example.com SOA ls -la /usr/local/etc/namedb/slave/

Securing Zone Transfers with TSIG

Generate a shared secret:

sh
tsig-keygen -a hmac-sha256 transfer-key > /usr/local/etc/namedb/keys/transfer-key.conf

View the generated key:

sh
cat /usr/local/etc/namedb/keys/transfer-key.conf

Include the key on both primary and secondary:

sh
vi /usr/local/etc/namedb/named.conf

Add at the top:

shell
include "/usr/local/etc/namedb/keys/transfer-key.conf";

On the primary, restrict transfers to the key:

shell
zone "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:

shell
server 10.0.0.1 { keys { transfer-key; }; };

Reload both servers:

sh
rndc 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):

sh
mkdir -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

sh
cd /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:

shell
zone "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:

sh
dnssec-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:

sh
dig @127.0.0.1 example.com DNSKEY +dnssec dig @127.0.0.1 example.com A +dnssec

Check with an external validator:

sh
dig +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:

sh
vi /usr/local/etc/namedb/named.conf
shell
acl 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:

sh
vi /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:

sh
vi /usr/local/etc/namedb/named.conf

Add the logging section:

shell
logging { 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:

sh
mkdir -p /var/log/named chown bind:bind /var/log/named

Enable query logging at runtime (for debugging):

sh
rndc querylog on

Disable when done:

sh
rndc querylog off

Security Hardening

Restrict Recursion

Never allow open recursion. Limit recursive queries to your own networks:

sh
allow-recursion { localhost; 10.0.0.0/8; 192.168.0.0/16; };

Rate Limiting

Prevent DNS amplification attacks:

shell
rate-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:

sh
sysrc named_chrootdir="/var/named"

The init script handles creating the chroot directory structure. Restart to apply:

sh
service named restart

Restrict Version Query

Prevent attackers from fingerprinting your BIND version:

shell
options { 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:

  1. Increment the serial number (format: YYYYMMDDNN)
  2. Validate the zone:
sh
named-checkzone example.com /usr/local/etc/namedb/master/example.com.zone
  1. Reload the zone:
sh
rndc reload example.com

For dynamic updates (e.g., from DHCP or automated scripts), use nsupdate:

sh
nsupdate -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:

sh
named-checkconf /usr/local/etc/namedb/named.conf

Check zone files:

sh
named-checkzone example.com /usr/local/etc/namedb/master/example.com.zone

Review logs:

sh
tail -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:

sh
dig @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:

sh
dig 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:

shell
options { 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.

Get more FreeBSD guides

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