FreeBSD

Install BIND 9 DNS Server on FreeBSD 15

BIND 9 remains the de facto DNS server for the internet, ISC’s reference implementation, now in its 9.18 long-term support branch. On FreeBSD 15, it ships as a first-class ports package and integrates cleanly with the base system without the kernel patches or SELinux policies that complicate Linux deployments. One command to install, a handful of config files, and you have a fully functional recursive resolver and authoritative server running under the bind user with chroot disabled by default in the modern pkg builds.

Original content from computingforgeeks.com - post 166430

This guide walks through a complete BIND 9 setup: recursive caching with forwarders, an authoritative primary zone for lab.local, a reverse zone, DNSSEC signing with ECDSAP256SHA256 keys, ACL-based query controls, and query logging. All steps tested on FreeBSD 15.0-RELEASE with BIND 9.18.48. If you’re new to FreeBSD, see the guide on installing FreeBSD 15 on Proxmox/KVM first, then come back here.

Verified working: April 2026 on FreeBSD 15.0-RELEASE (kernel releng/15.0-n280995), BIND 9.18.48 from the FreeBSD ports tree.

Prerequisites

  • FreeBSD 15.0-RELEASE with root access
  • Working internet connection for pkg and upstream DNS forwarding
  • Firewall open on UDP/TCP port 53 for DNS clients; TCP 953 for rndc (localhost only)
  • Tested on: BIND 9.18.48, FreeBSD 15.0-RELEASE, amd64

Install BIND 9

The FreeBSD ports tree offers several BIND versions. BIND 9.18 is the current extended support version (ESV) with security fixes through mid-2026:

pkg search bind9

You’ll see bind918, bind916, and others. Install the 9.18 ESV package:

pkg install -y bind918

Confirm the installed version:

named -v

The output confirms the installed build:

BIND 9.18.48 (Extended Support Version) <id:>

The package installs named, rndc, named-checkconf, named-checkzone, dnssec-keygen, and dnssec-signzone under /usr/local/sbin. Zone data and configuration live in /usr/local/etc/namedb/.

Enable named in rc.conf

Enable the service and disable the legacy chroot mode. Modern BIND on FreeBSD runs fine without it, and chroot adds complexity without meaningful security benefit when the daemon already drops privileges to the bind user:

sysrc named_enable=YES
sysrc named_chrootdir=""

Both settings are confirmed in the sysrc output:

named_enable: YES -> YES
named_chrootdir:  ->

Configure named.conf

The default config at /usr/local/etc/namedb/named.conf listens only on localhost. Replace it with a configuration that opens the listener, defines ACLs, adds forwarders, and declares the authoritative zones. Open the file:

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

Replace the options section and add the zone stanzas:

// ACL for trusted clients
acl "trusted_clients" {
    localhost;
    192.168.1.0/24;
};

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; };

    allow-query      { trusted_clients; };
    allow-recursion  { trusted_clients; };

    forwarders {
        1.1.1.1;   // Cloudflare
        9.9.9.9;   // Quad9
    };
    forward first;

    dnssec-validation auto;
};

logging {
    channel query_log {
        file "/var/log/named/queries.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
        print-severity yes;
        print-category yes;
    };

    category queries { query_log; };
    category default { default_syslog; default_debug; };
};

// Root hints
zone "." { type hint; file "/usr/local/etc/namedb/named.root"; };

// Standard RFC zones
zone "localhost"        { type primary; file "/usr/local/etc/namedb/primary/localhost-forward.db"; };
zone "127.in-addr.arpa" { type primary; file "/usr/local/etc/namedb/primary/localhost-reverse.db"; };
zone "255.in-addr.arpa" { type primary; file "/usr/local/etc/namedb/primary/empty.db"; };

// Authoritative zone: lab.local
zone "lab.local" {
    type primary;
    file "/usr/local/etc/namedb/primary/lab.local.db";
    allow-update  { none; };
    allow-transfer { none; };
};

// Reverse zone for 192.168.1.0/24
zone "1.168.192.in-addr.arpa" {
    type primary;
    file "/usr/local/etc/namedb/primary/192.168.1.rev";
    allow-update  { none; };
};

forward first lets BIND try forwarders first and fall back to recursive resolution if they fail. Use forward only if you want to force all queries through the forwarders without any fallback.

Create the Authoritative Zone for lab.local

Create the zone file at /usr/local/etc/namedb/primary/lab.local.db. This zone includes A, AAAA, CNAME, MX, and TXT (SPF) records for a typical lab environment:

vi /usr/local/etc/namedb/primary/lab.local.db

Paste in the zone data:

$TTL 3600
@   IN  SOA ns1.lab.local. admin.lab.local. (
            2026041501  ; Serial YYYYMMDDNN
            3600        ; Refresh
            900         ; Retry
            604800      ; Expire
            300 )       ; Negative TTL

@       IN  NS      ns1.lab.local.
@       IN  NS      ns2.lab.local.

; A records
ns1     IN  A       192.168.1.10
ns2     IN  A       192.168.1.11
web01   IN  A       192.168.1.20
db01    IN  A       192.168.1.30
mail    IN  A       192.168.1.40
monitor IN  A       192.168.1.50

; AAAA records
ns1     IN  AAAA    fd00::1
web01   IN  AAAA    fd00::20

; CNAME records
www     IN  CNAME   web01.lab.local.
ftp     IN  CNAME   web01.lab.local.

; MX record
@       IN  MX  10  mail.lab.local.

; TXT - SPF policy
@       IN  TXT "v=spf1 mx ip4:192.168.1.40 -all"

The SOA serial format YYYYMMDDNN makes it easy to track when the zone was last modified. Bump the serial every time you change the zone file. Skipping this is a common source of “zone lab.local/IN: loaded serial 0” log messages when the serial was accidentally left at zero, causing secondaries to ignore the update.

Create the Reverse Zone

PTR records let BIND answer reverse DNS lookups for the 192.168.1.0/24 subnet. Create /usr/local/etc/namedb/primary/192.168.1.rev:

vi /usr/local/etc/namedb/primary/192.168.1.rev

Add the SOA, NS, and PTR records:

$TTL 3600
@   IN  SOA ns1.lab.local. admin.lab.local. (
            2026041501
            3600
            900
            604800
            300 )

@   IN  NS  ns1.lab.local.
@   IN  NS  ns2.lab.local.

10  IN  PTR ns1.lab.local.
11  IN  PTR ns2.lab.local.
20  IN  PTR web01.lab.local.
30  IN  PTR db01.lab.local.
40  IN  PTR mail.lab.local.
50  IN  PTR monitor.lab.local.

Fix the log directory permissions before starting the service. The named daemon runs as bind:bind:

mkdir -p /var/log/named
chown bind:bind /var/log/named
chown bind:bind /usr/local/etc/namedb/primary/lab.local.db
chown bind:bind /usr/local/etc/namedb/primary/192.168.1.rev

Syntax Check and Start

Validate the configuration before starting. A silent exit confirms no errors:

named-checkconf /usr/local/etc/namedb/named.conf && echo "Configuration OK"

Then validate both zone files:

named-checkzone lab.local /usr/local/etc/namedb/primary/lab.local.db

Both zones should report a clean load with their serial numbers:

zone lab.local/IN: loaded serial 2026041501
OK

Then check the reverse zone:

named-checkzone 1.168.192.in-addr.arpa /usr/local/etc/namedb/primary/192.168.1.rev

The reverse zone should pass cleanly as well:

zone 1.168.192.in-addr.arpa/IN: loaded serial 2026041501
OK

Start the service and verify it’s running:

service named start
service named status

The service reports its PID when running:

named is running as pid 1984.

Check that BIND reports healthy with rndc:

rndc status

rndc should report the zone count and confirm the server is healthy:

version: BIND 9.18.48 (Extended Support Version) <id:>
running on localhost: FreeBSD amd64 15.0-RELEASE
boot time: Wed, 15 Apr 2026 21:48:25 GMT
number of zones: 103 (93 automatic)
query logging is ON
recursive clients: 0/900/1000
server is up and running

The screenshot below confirms the named-checkconf pass, zone load confirmation, and rndc status output from this test system:

BIND 9 named service status and configuration check output on FreeBSD 15

If rndc fails with “connect failed: 127.0.0.1#953: connection refused”, the rndc.key was not generated. Run rndc-confgen -a to generate it, then restart named.

Test DNS Resolution

Query the forward zone to confirm authoritative answers. The aa flag in the response flags confirms BIND is answering authoritatively:

dig @127.0.0.1 SOA lab.local

The aa flag confirms an authoritative answer from the local server:

;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; ANSWER SECTION:
lab.local.  3600  IN  SOA  ns1.lab.local. admin.lab.local. 2026041501 3600 900 604800 300

Test individual record types:

dig @127.0.0.1 +short www.lab.local

BIND follows the CNAME and returns the resolved A record in one response:

web01.lab.local.
192.168.1.20

The CNAME resolves to the A record in the same response. Test forwarded external resolution:

dig @127.0.0.1 google.com A +short

The forwarder returns a real public address, confirming upstream resolution works:

172.217.170.174

Test reverse DNS with a PTR query:

dig @127.0.0.1 -x 192.168.1.20 +short

The PTR record resolves correctly to the hostname:

web01.lab.local.

Configure DNSSEC

DNSSEC signing protects zones against cache poisoning by attaching cryptographic signatures to every record set. For a lab zone this is optional, but it’s the right habit to build. FreeBSD 15’s BIND 9.18 uses ECDSAP256SHA256 by default, which is ISC’s recommended algorithm for new deployments.

Generate a ZSK (Zone Signing Key) and a KSK (Key Signing Key) for lab.local:

cd /usr/local/etc/namedb/primary

dnssec-keygen -a ECDSAP256SHA256 -b 256 -n ZONE lab.local
dnssec-keygen -a ECDSAP256SHA256 -b 256 -f KSK -n ZONE lab.local

Each command prints the key tag for the generated key pair:

Klab.local.+013+36905
Klab.local.+013+52185

Each keygen call creates a .key and a .private file. Include both public keys at the bottom of the zone file, then sign it:

echo '$INCLUDE Klab.local.+013+36905.key' >> lab.local.db
echo '$INCLUDE Klab.local.+013+52185.key' >> lab.local.db

dnssec-signzone -A -3 $(head -c 6 /dev/random | md5 | cut -c1-16) \
  -N INCREMENT -o lab.local -t lab.local.db

A successful signing run reports the algorithm and signature count:

Verifying the zone using the following algorithms:
- ECDSAP256SHA256
Zone fully signed:
Algorithm: ECDSAP256SHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                            ZSKs: 1 active, 0 stand-by, 0 revoked
lab.local.db.signed
Signatures generated:                       27

Update named.conf to load the signed zone file instead of the plain one:

zone "lab.local" {
    type primary;
    file "/usr/local/etc/namedb/primary/lab.local.db.signed";
    allow-update  { none; };
    allow-transfer { none; };
};

Reload and verify RRSIG records appear in responses:

rndc reload
dig @127.0.0.1 +dnssec lab.local SOA

The answer section now contains two records: the SOA and its RRSIG signature:

;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; ANSWER SECTION:
lab.local.  3600  IN  SOA   ns1.lab.local. admin.lab.local. 2026041502 3600 900 604800 300
lab.local.  3600  IN  RRSIG SOA 13 2 3600 20260515204856 20260415204856 36905 lab.local.
                              q7fGW2RJ8xzYoMg+D4ir0BlJ/Vm8EL2livPzbYvdu3y...

The RRSIG record confirms the SOA is signed with the ZSK (key tag 36905, ECDSAP256SHA256):

dig DNS query showing SOA record and DNSSEC RRSIG signature on FreeBSD 15

For production DNSSEC deployments, schedule zone re-signing before signatures expire. The default signature validity is 30 days. Add a cron job to run dnssec-signzone weekly and rndc reload afterward.

ACL Groups for Client Control

The ACL defined at the top of named.conf controls which clients can query the server. You can define multiple ACLs for different access tiers: allow recursion only to internal clients while permitting authoritative queries from anywhere:

acl "internal" {
    127.0.0.0/8;
    192.168.0.0/16;
    10.0.0.0/8;
};

acl "monitoring" {
    192.168.1.50;    // Zabbix/Prometheus host
};

options {
    allow-query     { any; };          // Authoritative: open
    allow-recursion { internal; };     // Recursion: internal only
    allow-query-cache { internal; };   // Cache access: internal only
};

This pattern lets you run an open authoritative server for public zones while restricting recursive resolution to internal clients. The monitoring host can query the server from the monitoring ACL for health checks. For the FreeBSD jail environments described in the FreeBSD jails with VNET networking guide, set the jail subnet in the internal ACL.

rndc Operations

rndc communicates with named over a local control channel. Three commands you’ll use regularly:

rndc reload

Reloads configuration and zone files without restarting. Named re-reads named.conf and picks up any zone serial changes. Use this after editing zone files.

rndc flush

Flushes the resolver cache. Useful when an upstream record changed and you need clients to see the new value immediately rather than waiting for TTL expiry.

rndc status

The output shows runtime counters and confirms the server is healthy:

version: BIND 9.18.48 (Extended Support Version) <id:>
running on localhost: FreeBSD amd64 15.0-RELEASE
number of zones: 103 (93 automatic)
query logging is ON
recursive clients: 0/900/1000
server is up and running

The “recursive clients” counter is useful for spotting DNS amplification abuse. If it spikes to near the limit under normal conditions, tighten the allow-recursion ACL.

Query Logging

The logging stanza in named.conf directs query logs to /var/log/named/queries.log with rotation at 5 MB (3 versions kept). The log shows client IPs, query names, and record types in real time. Watch it during testing to confirm queries are reaching the right server:

tail -f /var/log/named/queries.log

Each query logs the client address, query name, record type, and the server interface that handled it:

15-Apr-2026 21:49:10.230 queries: info: client @0x3351 127.0.0.1#39037 (ns1.lab.local): query: ns1.lab.local IN A +E(0)K (127.0.0.1)
15-Apr-2026 21:49:10.240 queries: info: client @0x3351 127.0.0.1#17693 (web01.lab.local): query: web01.lab.local IN A +E(0)K (127.0.0.1)
15-Apr-2026 21:49:10.250 queries: info: client @0x3351 127.0.0.1#51197 (mail.lab.local): query: mail.lab.local IN A +E(0)K (127.0.0.1)

The flags after the query name decode as: + (RD bit set, recursion desired), E (EDNS present), K (TSIG/SIG). The parenthesized IP at the end is the listening address that received the query.

To disable query logging temporarily without restarting named (useful during high query rates to reduce disk I/O):

rndc querylog off
rndc querylog on

Firewall Configuration

Open port 53 for DNS clients. FreeBSD uses pf or ipfw depending on your setup. With pf, add to /etc/pf.conf:

pass in on vtnet0 proto { tcp udp } from 192.168.1.0/24 to any port 53

Port 953 (rndc) should stay bound to localhost only, which is the default. Never expose rndc to the network without a TSIG key protecting it.

Troubleshooting

Error: “zone lab.local/IN: loaded serial 0”

The SOA serial in your zone file is set to 0. This is valid but means any secondary server that already has the zone loaded will ignore future transfers until the serial increases. Set it to the current date format: 2026041501. Run named-checkzone and you’ll see it report the serial. If it says 0, edit the SOA.

Error: “rndc: connect failed: 127.0.0.1#953: connection refused”

The rndc.key doesn’t exist or the control channel isn’t listening. Generate the key and restart:

rndc-confgen -a
service named restart

Error: “permission denied on /var/log/named/queries.log”

Named runs as the bind user and can’t create the log file if the directory is owned by root. Fix it with:

mkdir -p /var/log/named
chown bind:bind /var/log/named
rndc reload

DNSSEC validation failure for external zones

With dnssec-validation auto, BIND validates using the built-in trust anchor for the root zone (stored in /usr/local/etc/namedb/bind.keys). If you see validation failures for .com or other TLDs, the trust anchor may be outdated. Run rndc managed-keys status to check, and update the BIND package to get a current bind.keys file.

BIND 9 on FreeBSD vs Linux: Key Differences

The same bind918 package installs identically across FreeBSD, Ubuntu, and Rocky Linux, but paths, service names, and default behaviors differ enough to cause confusion when you cross environments:

ItemFreeBSD 15Ubuntu 24.04Rocky Linux 10
Package namebind918bind9bind
Config directory/usr/local/etc/namedb//etc/bind//etc/named/
Zone directory/usr/local/etc/namedb/primary//var/lib/bind//var/named/
Service namenamed (rc.conf)named (systemd)named (systemd)
Named userbindbindnamed
Default chrootNone (disabled by default)None/var/named/chroot (optional pkg)
Firewall toolpf / ipfwufw / nftablesfirewalld / nftables
SELinux/AppArmorNot presentAppArmor (usually permissive for bind)SELinux enforcing — requires named_write_master_zones boolean
Start serviceservice named startsystemctl start namedsystemctl start named
Enable on bootsysrc named_enable=YESsystemctl enable namedsystemctl enable named

On Rocky Linux 10, the biggest gotcha is SELinux: if named can’t write zone files, check ausearch -m avc -ts recent and run setsebool -P named_write_master_zones 1. FreeBSD skips this entirely. The FreeBSD 15 improvements over 14.x also include updated OpenSSL (used by BIND for DNSSEC), which means the ECDSAP256SHA256 performance is marginally better than on older FreeBSD versions.

For related FreeBSD server setups, see the guides on setting a static IP on FreeBSD, PostgreSQL on FreeBSD, and the Nginx with SSL on FreeBSD 15 guide for a complete web stack alongside DNS.

Related Articles

Cloud Best Cheap VPS hosting Providers 2025 Monitoring How To Install Netdata on RHEL 8 / CentOS 8 AlmaLinux Build Open vSwitch from Source on Rocky / AlmaLinux / RHEL Debian Install Asterisk with FreePBX on Debian 13 / Ubuntu 24.04

Leave a Comment

Press ESC to close