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 Server → Zones → Edit Zone → Zone 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:
- Run BIND on the Synology instead of DNS Server (requires Docker or compiling)
- Make FreeBSD the primary and manage zones there
- 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:
- Increment the serial number - This is critical!
- Reload the zone -
sudo rndc reload example.com
- NOTIFY is sent automatically to secondaries
- Secondaries check SOA - See the new serial, request transfer
- 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
- Single point of failure eliminated - NAS reboot? DNS still works via FreeBSD
- Configuration errors contained - Bad zone on primary? Secondary keeps serving old good version
- No manual sync - Update once on primary, secondary pulls automatically
- Faster failover - Clients query both servers, automatic fallback
- 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.