After my split horizon DNS debugging adventure, I realized I had a bigger problem: a single point of failure. My primary DNS server was a Synology NAS running DNS Server (which is really just dnsmasq under the hood). When the NAS rebooted for updates, DNS died. When I misconfigured something, DNS died. Time for a proper secondary.

Why a Secondary DNS Server

Secondary DNS servers (also called “slave” servers in BIND terminology, though that’s being phased out) automatically replicate zones from a primary via zone transfers. Benefits:

  • Redundancy - If the primary goes down, clients can use the secondary
  • Load distribution - Clients can query either server
  • No manual synchronization - NOTIFY messages and SOA checks keep zones in sync
  • Geographic distribution - Place secondaries closer to clients

The key insight: you maintain zones on the primary, and the secondary automatically pulls updates. No more trying to keep two zone files manually synchronized.

The Setup

Primary DNS: Synology NAS at 192.168.2.2 running DNS Server Secondary DNS: FreeBSD 14.1 box at 192.168.2.3 running BIND 9.18

Clients configured with both nameservers: nameserver 192.168.2.2 nameserver 192.168.2.3

Installing BIND on FreeBSD

FreeBSD makes this trivial:

              # Install BIND 9.18 from packages
sudo pkg install bind918

# Or from ports if you want to customize
cd /usr/ports/dns/bind918 && sudo make install clean

            

Enable and start named:

              # Enable at boot
sudo sysrc named_enable="YES"

# Start the service
sudo service named start

# Check status
sudo service named status

            

BIND configuration lives in /usr/local/etc/namedb/ on FreeBSD (not /etc/bind like on Linux).

Configuring the Secondary Server

Edit /usr/local/etc/namedb/named.conf:

              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 all interfaces
    listen-on { any; };
    listen-on-v6 { any; };

    // Allow queries from local networks
    allow-query { 192.168.1.0/24; 192.168.2.0/24; localhost; };

    // Recursion for internal clients
    recursion yes;
    allow-recursion { 192.168.1.0/24; 192.168.2.0/24; localhost; };

    // Rate limiting to prevent abuse
    rate-limit {
        responses-per-second 10;
        window 5;
    };

    // Forward unknown queries to upstream DNS
    forwarders {
        1.1.1.1;
        8.8.8.8;
    };

    // DNSSEC validation
    dnssec-validation auto;

    // Version hiding (security)
    version "not available";
};

// Logging configuration
logging {
    channel default_log {
        file "/var/log/named/named.log" versions 3 size 5m;
        severity info;
        print-time yes;
        print-severity yes;
        print-category yes;
    };

    channel query_log {
        file "/var/log/named/query.log" versions 3 size 10m;
        severity info;
        print-time yes;
    };

    category default { default_log; };
    category queries { query_log; };
};

// ACL for primary server
acl "primary-dns" {
    192.168.2.2;    // Synology NAS
};

// Secondary zone configuration
zone "example.com" {
    type slave;
    file "slave/db.example.com";
    masters { 192.168.2.2; };
    allow-notify { primary-dns; };
    notify no;  // Secondaries don't notify
};

zone "2.168.192.in-addr.arpa" {
    type slave;
    file "slave/db.192.168.2";
    masters { 192.168.2.2; };
    allow-notify { primary-dns; };
    notify no;
};

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

// Localhost zones
zone "localhost" {
    type master;
    file "/usr/local/etc/namedb/master/localhost-forward.db";
};

zone "127.in-addr.arpa" {
    type master;
    file "/usr/local/etc/namedb/master/localhost-reverse.db";
};

            

Key points for slave zones: - type slave; - Tells BIND this is a secondary zone - file "slave/db.example.com"; - Where to store the transferred zone (BIND creates this) - masters { 192.168.2.2; }; - Primary DNS server to transfer from - allow-notify { primary-dns; }; - Only accept NOTIFY messages from the primary - notify no; - Secondaries don’t send NOTIFY (no downstream servers)

Create Directories and Set Permissions

              # Create necessary directories
sudo mkdir -p /usr/local/etc/namedb/working
sudo mkdir -p /usr/local/etc/namedb/slave
sudo mkdir -p /var/log/named
sudo mkdir -p /var/run/named

# BIND runs as user 'bind' - give it ownership
sudo chown -R bind:bind /usr/local/etc/namedb/working
sudo chown -R bind:bind /usr/local/etc/namedb/slave
sudo chown -R bind:bind /var/log/named
sudo chown -R bind:bind /var/run/named

# Restrictive permissions
sudo chmod 755 /usr/local/etc/namedb/slave
sudo chmod 755 /var/log/named

            

The slave/ directory must be writable by the bind user so BIND can create zone files from transfers.

Configuring the Primary (Synology)

Your Synology needs to allow zone transfers to the secondary. Unfortunately, Synology’s DNS Server GUI doesn’t expose zone transfer controls well, but you can work around it.

Option 1: Allow Transfers via Synology GUI

In DNS ServerZonesEdit ZoneZone Transfer: - Enable “Allow zone transfer” - Add 192.168.2.3 to allowed servers

However, Synology’s implementation is limited. For better control, you might need to edit configs directly.

Option 2: Edit dnsmasq Config (Advanced)

Synology’s DNS Server uses dnsmasq, which doesn’t natively support zone transfers in the traditional BIND sense. If you need full zone transfer support, consider:

  1. Run BIND on the Synology instead of DNS Server (requires Docker or compiling)
  2. Make FreeBSD the primary and manage zones there
  3. Use DNS Server’s “Master/Slave” feature if available in your DSM version

For most home setups, making the FreeBSD box the primary is cleaner.

Making FreeBSD the Primary Instead

If your Synology doesn’t properly support zone transfers, flip the setup:

Primary: FreeBSD (192.168.2.3) Secondary: Synology (192.168.2.2) - if it supports slave zones

Or just run BIND as the primary and skip the Synology DNS Server entirely.

Primary Zone Configuration on FreeBSD

Edit /usr/local/etc/namedb/named.conf:

              // ACL for secondary servers
acl "secondary-dns" {
    192.168.2.2;    // Synology or other secondary
};

zone "example.com" {
    type master;
    file "/usr/local/etc/namedb/master/db.example.com";
    allow-transfer { secondary-dns; };
    also-notify { 192.168.2.2; };
    notify yes;
};

zone "2.168.192.in-addr.arpa" {
    type master;
    file "/usr/local/etc/namedb/master/db.192.168.2";
    allow-transfer { secondary-dns; };
    also-notify { 192.168.2.2; };
    notify yes;
};

            

Create the zone file /usr/local/etc/namedb/master/db.example.com:

              $TTL 86400
@       IN      SOA     ns1.example.com. admin.example.com. (
                2025093001      ; Serial (increment on each change)
                3600            ; Refresh (1 hour)
                1800            ; Retry (30 minutes)
                604800          ; Expire (1 week)
                86400 )         ; Minimum TTL (1 day)

; Name servers
        IN      NS      ns1.example.com.
        IN      NS      ns2.example.com.

; A records for name servers
ns1     IN      A       192.168.2.3
ns2     IN      A       192.168.2.2

; Mail server
        IN      MX      10 mail.example.com.

; Host records
mail    IN      A       192.168.2.10
www     IN      A       192.168.2.20
ftp     IN      A       192.168.2.21
homelab IN      A       192.168.2.50

; CNAME records
blog    IN      CNAME   www

            

Set ownership and permissions:

              sudo mkdir -p /usr/local/etc/namedb/master
sudo chown bind:bind /usr/local/etc/namedb/master
sudo chown bind:bind /usr/local/etc/namedb/master/db.example.com
sudo chmod 644 /usr/local/etc/namedb/master/db.example.com

            

Reload BIND:

              # Check configuration syntax first
sudo named-checkconf
sudo named-checkzone example.com /usr/local/etc/namedb/master/db.example.com

# Reload if all good
sudo rndc reload

            

Testing Zone Transfers

Force a zone transfer from the secondary:

              # On the secondary server
sudo rndc retransfer example.com

# Check logs for transfer status
sudo tail -f /var/log/named/named.log

            

You should see:

              30-Sep-2025 14:23:11.123 general: info: zone example.com/IN: Transfer started.
30-Sep-2025 14:23:11.234 xfer-in: info: transfer of 'example.com/IN' from 192.168.2.3#53: connected using 192.168.2.2#51234
30-Sep-2025 14:23:11.345 xfer-in: info: zone example.com/IN: transferred serial 2025093001
30-Sep-2025 14:23:11.456 general: info: zone example.com/IN: sending notifies (serial 2025093001)

            

Check that the zone file was created:

              ls -la /usr/local/etc/namedb/slave/
-rw-r--r--  1 bind  bind  1234 Sep 30 14:23 db.example.com

            

Query both servers to verify consistency:

              # Query primary
dig @192.168.2.3 homelab.example.com +short
192.168.2.50

# Query secondary
dig @192.168.2.2 homelab.example.com +short
192.168.2.50

# Check SOA serials match
dig @192.168.2.3 example.com SOA +short
ns1.example.com. admin.example.com. 2025093001 3600 1800 604800 86400

dig @192.168.2.2 example.com SOA +short
ns1.example.com. admin.example.com. 2025093001 3600 1800 604800 86400

            

Serial numbers should match!

Making Updates and Propagation

When you update a zone on the primary:

  1. Increment the serial number - This is critical!
  2. Reload the zone - sudo rndc reload example.com
  3. NOTIFY is sent automatically to secondaries
  4. Secondaries check SOA - See the new serial, request transfer
  5. Zone transfer happens - AXFR or IXFR (incremental)

Example update workflow:

              # Edit the zone file
sudo vi /usr/local/etc/namedb/master/db.example.com

# Change serial from 2025093001 to 2025093002
# Add new record:
# newhost    IN      A       192.168.2.99

# Check syntax
sudo named-checkzone example.com /usr/local/etc/namedb/master/db.example.com

# Reload
sudo rndc reload example.com

# Watch secondary pull the update
ssh secondary.example.com "sudo tail -f /var/log/named/named.log"

            

The secondary will automatically transfer within seconds (or minutes, depending on refresh interval).

Troubleshooting Zone Transfers

Transfer Not Happening

Check firewall on primary:

              # FreeBSD - allow zone transfers from secondary
sudo pfctl -sr | grep 53

# If using pf, add to /etc/pf.conf:
pass in proto tcp from 192.168.2.2 to any port 53
pass in proto udp from 192.168.2.2 to any port 53

            

Verify primary allows transfers:

              # Check BIND config
grep allow-transfer /usr/local/etc/namedb/named.conf

            

Test TCP connectivity (zone transfers use TCP):

              # From secondary to primary
nc -zv 192.168.2.3 53

            

Serial Number Not Incrementing

If you forget to increment the serial, the secondary won’t transfer. You can force it:

              # On secondary
sudo rndc retransfer example.com

            

Or use a date-based serial format: YYYYMMDDnn (e.g., 2025093001 = Sept 30, 2025, revision 01)

Permission Errors

              # Make sure bind user owns slave directory
sudo chown -R bind:bind /usr/local/etc/namedb/slave
sudo chmod 755 /usr/local/etc/namedb/slave

            

Check logs:

              sudo tail -f /var/log/named/named.log | grep -i error

            

The Problems This Solved

  1. Single point of failure eliminated - NAS reboot? DNS still works via FreeBSD
  2. Configuration errors contained - Bad zone on primary? Secondary keeps serving old good version
  3. No manual sync - Update once on primary, secondary pulls automatically
  4. Faster failover - Clients query both servers, automatic fallback
  5. Better monitoring - Can compare SOA serials to detect sync issues

Before: DNS downtime every time the NAS rebooted (5-10 minutes) After: Zero DNS downtime, seamless failover

Monitoring Zone Sync

Simple script to check serial numbers match:

              #!/bin/sh
# check-dns-sync.sh

PRIMARY="192.168.2.3"
SECONDARY="192.168.2.2"
ZONE="example.com"

PRIMARY_SERIAL=$(dig @$PRIMARY $ZONE SOA +short | awk '{print $3}')
SECONDARY_SERIAL=$(dig @$SECONDARY $ZONE SOA +short | awk '{print $3}')

if [ "$PRIMARY_SERIAL" = "$SECONDARY_SERIAL" ]; then
    echo "OK: Serials match ($PRIMARY_SERIAL)"
    exit 0
else
    echo "CRITICAL: Serial mismatch! Primary: $PRIMARY_SERIAL Secondary: $SECONDARY_SERIAL"
    exit 2
fi

            

Run via cron every 5 minutes:

              */5 * * * * /usr/local/bin/check-dns-sync.sh

            

FreeBSD-Specific Tips

Update BIND safely:

              # Check for updates
sudo pkg update
sudo pkg version -v | grep bind

# Update with automatic restart
sudo pkg upgrade bind918
sudo service named restart

            

Enable query logging temporarily:

              # Enable
sudo rndc querylog on

# Disable when done (generates lots of logs)
sudo rndc querylog off

            

Check BIND memory usage:

              # BIND can be memory-hungry
top -P bind

            

Rotate logs with newsyslog:

Edit /etc/newsyslog.conf:

              /var/log/named/named.log    bind:bind   644  7  *  @T00  JC
/var/log/named/query.log    bind:bind   644  3  *  @T00  JC

            

Going Further

Consider: - DNSSEC - Sign your zones for cryptographic verification - Rate limiting - Protect against DNS amplification attacks - Response Policy Zones (RPZ) - Block malicious domains - Views - Serve different zones to internal vs external clients (split horizon) - Dynamic updates - Let DHCP server update DNS automatically

But for most home labs, a solid primary/secondary setup with zone transfers is plenty.

Conclusion

Running BIND on FreeBSD as a secondary DNS server is straightforward and dramatically improves reliability. Zone transfers handle synchronization automatically, and FreeBSD’s stability makes it an excellent platform for infrastructure services.

The combination of Synology for easy management + FreeBSD/BIND for robust redundancy gives you the best of both worlds. Or better yet, just run BIND as your primary on FreeBSD and skip the Synology DNS entirely - one less moving part to break.