Configure BIND Primary and Secondary DNS on RHEL Linux

Tested on Rocky Linux 8.4 (Green Obsidian)
Package bind 9.11.26, bind-chroot 9.11.26
Applies to RHEL 8, Rocky Linux 8, AlmaLinux 8, CentOS Stream 8, Oracle Linux 8
Privilege sudo or root
Scope BIND primary-to-secondary replication on Rocky Linux 8: allow-transfer, NOTIFY, slave zones, named-chroot, and AXFR checks. You need a working primary with forward and reverse zones first.
Related guides Configure BIND DNS server on Linux
Secure BIND zone transfers with TSIG
firewalld cheat sheet
View logs with journalctl
dnf command

One DNS server is a single point of failure. Add a secondary BIND host and it copies zone files from the primary through AXFR or IXFR, keeps answering when the primary is down, and shares query load.

This guide builds on configure a BIND DNS server on Linux. You already have a working primary, or you create one first, then wire up replication. The examples use Rocky Linux 8 with named-chroot, domain example.com, and subnet 172.29.10.0/24. Swap in your own IPs and zone names where the commands show mine.

IMPORTANT
This guide covers primary/secondary BIND replication with IP-based allow-transfer on a trusted internal network. It is not a substitute for the primary BIND install walkthrough, TSIG-authenticated transfers, DNSSEC signing, or a dnsmasq caching resolver.

Lab environment

Use two Rocky Linux 8.4 hosts on the same subnet. I named mine master and slave:

Role Hostname IP address
Primary DNS master 172.29.10.4/24
Secondary DNS slave 172.29.10.12/24

Forward zone: example.com
Reverse zone: 10.29.172.in-addr.arpa (reverse the first three octets of 172.29.10.0/24)

If the primary already runs BIND from the earlier guide, keep your zone files and add only the transfer settings below.


Configure the primary server for zone transfer

On the primary, open zone transfer to the secondary IP and tell BIND to NOTIFY that host when a zone changes.

Install BIND on the primary

Package installs use the dnf command.

bash
dnf install -y bind bind-chroot

On Rocky Linux 8.4 you should see bind and bind-chroot 9.11.26 install:

output
Rocky Linux 8 - AppStream
Rocky Linux 8 - BaseOS
Rocky Linux 8 - Extras
Total download size: 2.2 M
Installed size: 4.5 M
Downloading Packages:
 bind-chroot-9.11.26-4.el8_4.x86_64.rpm
 bind-9.11.26-4.el8_4.x86_64.rpm
Installed:
  bind-32:9.11.26-4.el8_4.x86_64
  bind-chroot-32:9.11.26-4.el8_4.x86_64
Complete!

Allow transfer and NOTIFY in named.conf

Edit /etc/named.conf on the primary. In my setup the server listens on all interfaces and allows zone transfer only to 172.29.10.12:

text
options {
        listen-on port 53 { 127.0.0.1; any; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        dump-file       "/var/named/data/cache_dump.db";
        statistics-file "/var/named/data/named_stats.txt";
        memstatistics-file "/var/named/data/named_mem_stats.txt";
        recursing-file  "/var/named/data/named.recursing";
        secroots-file   "/var/named/data/named.secroots";
        allow-query     { localhost; any; };
        allow-query-cache { localhost; any; };
        allow-transfer  { 172.29.10.12; };
        notify yes;
        also-notify     { 172.29.10.12; };

        recursion yes;
        ...
};

include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";

allow-transfer names the host that may pull a full zone copy. also-notify pings that host right after you reload, so it does not sit idle until the SOA refresh timer expires.

Declare primary zones in named.rfc1912.zones

Add the forward and reverse zone blocks to /etc/named.rfc1912.zones on the primary:

text
zone "example.com" IN {
        type master;
        file "example.com.zone";
        allow-update { none; };
};

zone "10.29.172.in-addr.arpa" IN {
        type master;
        file "example.com.rzone";
        allow-update { none; };
};

Forward zone file on the primary

Skip this part if example.com already exists on the primary from the BIND install guide. Otherwise create /var/named/example.com.zone along these lines:

bash
cat /var/named/example.com.zone
output
$TTL 1D
@       IN SOA  example.com.     root (
                        5       ; serial
                        1D      ; refresh
                        1H      ; retry
                        1W      ; expire
                        3H )    ; minimum
        IN  NS  localhost
localhost   IN A    127.0.0.1
ns-master       IN A    172.29.10.4
server1         IN A    172.29.10.5
server2         IN A    172.29.10.6
slave           IN A    172.29.10.12

Raise the SOA serial every time you add or change a record. Secondaries only pick up edits when that number increases.

Reverse zone file on the primary

The reverse zone lives in /var/named/example.com.rzone:

bash
cat /var/named/example.com.rzone
output
$TTL 1D
@       IN SOA   example.com.    root (
                5       ; serial
                1D      ; refresh
                1H      ; retry
                1W      ; expire
                3H )    ; minimum
        IN NS    localhost.
4       IN PTR   ns-master.example.com.
5       IN PTR   server1.example.com.
6       IN PTR   server2.example.com.
12      IN PTR   slave.example.com.

Only the last octet of each address appears on the left because the zone origin is already 10.29.172.in-addr.arpa.

Enable the chroot environment on the primary

On Rocky Linux 8 I ran BIND inside /var/named/chroot:

bash
/usr/libexec/setup-named-chroot.sh /var/named/chroot on

Confirm the config files are visible inside the chroot tree:

bash
mount | grep chroot
output
/dev/mapper/rl-root on /var/named/chroot/etc/named.conf type xfs (rw,relatime,...)
/dev/mapper/rl-root on /var/named/chroot/etc/named.rfc1912.zones type xfs (rw,relatime,...)
/dev/mapper/rl-root on /var/named/chroot/var/named type xfs (rw,relatime,...)
tmpfs on /var/named/chroot/run/named type tmpfs (rw,nosuid,nodev,mode=755)

Validate primary zone syntax

Check syntax before you start named:

bash
named-checkconf -t /var/named/chroot /etc/named.conf

Validate each custom zone file:

bash
named-checkzone example.com /var/named/example.com.zone
output
zone example.com/IN: loaded serial 5
OK

Check the reverse zone the same way:

bash
named-checkzone 10.29.172.in-addr.arpa /var/named/example.com.rzone
output
zone 10.29.172.in-addr.arpa/IN: loaded serial 5
OK

OK on both checks means BIND can load the zone files inside chroot.

Open DNS on the primary firewall

Open DNS in firewalld on the primary. The firewalld cheat sheet covers permanent service rules and custom ports.

bash
firewall-cmd --add-service=dns --permanent

--permanent saves the rule across reboots. Reload firewalld to apply it now:

bash
firewall-cmd --reload

Point the primary at itself for resolution

Append the primary address to /etc/resolv.conf on the primary host:

bash
echo "nameserver 172.29.10.4" >> /etc/resolv.conf

On production systems, set DNS through NetworkManager or a resolv.conf drop-in instead of appending by hand.

Start named-chroot on the primary

Stop the plain named service so only one daemon binds port 53:

bash
systemctl stop named

Prevent the non-chroot unit from starting at boot:

bash
systemctl disable named

Enable and start the chroot service:

bash
systemctl enable --now named-chroot

Confirm the daemon is running:

bash
systemctl status named-chroot
output
● named-chroot.service - Berkeley Internet Name Domain (DNS)
   Loaded: loaded (/usr/lib/systemd/system/named-chroot.service; enabled; vendor preset: disabled)
   Active: active (running) since Tue 2021-08-10 07:34:32 EAT; 42s ago
   ...
Aug 10 07:34:32 master named[1956]: zone example.com/IN: loaded serial 5
Aug 10 07:34:32 master named[1956]: all zones loaded
Aug 10 07:34:32 master named[1956]: running
Aug 10 07:34:32 master named[1956]: zone 10.29.172.in-addr.arpa/IN: sending notifies (serial 5)
Aug 10 07:34:32 master named[1956]: zone example.com/IN: sending notifies (serial 5)

The sending notifies lines mean BIND told the secondary that fresh zone data is ready.


Configure the secondary server

The secondary does not hold writable zone files. It pulls copies from the primary and answers queries from those files.

Install BIND on the secondary

Install the same packages on slave:

bash
dnf install -y bind bind-chroot

You get the same bind and bind-chroot 9.11.26 packages as on the primary.

Configure named.conf on the secondary

The secondary listens on its own address (172.29.10.12) and does not define master zones in options:

text
options {
        listen-on port 53 { 127.0.0.1; 172.29.10.12; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        ...
        allow-query     { localhost; any; };

        recursion yes;

        dnssec-enable yes;
        ...
};

include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";

Declare slave zones

Add matching forward and reverse slave stanzas to /etc/named.rfc1912.zones on the secondary:

text
zone "example.com" IN {
        type slave;
        masters { 172.29.10.4; };
        file "slaves/example.com.zone";
};

zone "10.29.172.in-addr.arpa" IN {
        type slave;
        masters { 172.29.10.4; };
        file "slaves/example.com.rzone";
};

masters points at the primary. BIND writes transferred copies under /var/named/slaves/ (inside chroot that path is under /var/named/chroot/var/named/slaves/).

Enable chroot on the secondary

Run the chroot setup script on the slave host:

bash
/usr/libexec/setup-named-chroot.sh /var/named/chroot on

Verify mounts:

bash
mount | grep chroot
output
/dev/mapper/rl-root on /var/named/chroot/etc/named.conf type xfs (rw,relatime,...)
/dev/mapper/rl-root on /var/named/chroot/var/named type xfs (rw,relatime,...)
tmpfs on /var/named/chroot/run/named type tmpfs (rw,nosuid,nodev,mode=755)

Validate and start the secondary

Check main configuration syntax:

bash
named-checkconf -t /var/named/chroot /etc/named.conf

Put both DNS addresses in /etc/resolv.conf on the slave:

text
nameserver 172.29.10.4
nameserver 172.29.10.12

Stop the plain named unit:

bash
systemctl stop named

Prevent the non-chroot unit from starting at boot:

bash
systemctl disable named

Start the chroot service:

bash
systemctl start named-chroot

Once the primary is up and reachable, check that zone files arrived:

bash
ls -l /var/named/slaves/
output
total 8
-rw-r--r-- 1 named named 510 Aug 10 05:31 example.com.rzone
-rw-r--r-- 1 named named 430 Aug 10 05:31 example.com.zone

Files with non-zero size under slaves/ mean AXFR copied both zones from the primary.


Verify zone transfer and replication

After the secondary starts, read its log. On newer hosts you can also follow the service with journalctl:

bash
tail /var/named/data/named.run
output
zone 10.29.172.in-addr.arpa/IN: loaded serial 5
zone example.com/IN: loaded serial 5
all zones loaded
running

Both zones show serial 5, so the secondary loaded the same revision as the primary.

Test NOTIFY after a record change

Edit the forward zone on the primary and bump the SOA serial from 5 to 6. I added labserver pointing at 172.29.10.50:

text
$TTL 1D
@   IN SOA  example.com     root (
                        6   ; serial
                        1D  ; refresh
                        1H  ; retry
                        1W  ; expire
                        3H )    ; minimum
        IN  NS  localhost
localhost   IN A    127.0.0.1
ns-master       IN A    172.29.10.4
server1         IN A    172.29.10.5
server2         IN A    172.29.10.6
slave           IN A    172.29.10.12
labserver       IN A    172.29.10.50

Reload the primary without a full restart:

bash
systemctl reload named-chroot

On the secondary, tail the log again:

bash
tail /var/named/data/named.run
output
client 172.29.10.12#953: received notify for zone 'example.com'
zone example.com/IN: Transfer started.
transfer of 'example.com/IN' from 172.29.10.4#53: connected using 172.29.10.12#44589
zone example.com/IN: transferred serial 6
transfer of 'example.com/IN' from 172.29.10.4#53: Transfer completed: 1 messages, 7 records, 280 bytes, 0.003 secs (127500 bytes/sec)
zone example.com/IN: sending notifies (serial 6)

transferred serial 6 in the log means the update reached the secondary.

Ask the secondary for the new name:

bash
dig labserver.example.com.
output
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28493
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3
;; QUESTION SECTION:
;labserver.example.com.         IN  A
;; ANSWER SECTION:
labserver.example.com.  86400   IN  A   172.29.10.50
;; AUTHORITY SECTION:
example.com.            86400   IN  NS  ns-master.example.com.
example.com.            86400   IN  NS  slave.example.com.
;; SERVER: 172.29.10.12#53(172.29.10.12)

The aa flag and the A record show the secondary is serving the updated zone authoritatively.

Before you expose replication beyond a lab LAN, lock down transfers with TSIG instead of IP addresses alone. See secure BIND zone transfers with TSIG.


Troubleshoot BIND replication

Symptom Likely cause Fix
/var/named/slaves/ empty allow-transfer missing or wrong secondary IP on primary Add the secondary address; reload primary
Secondary never updates after edit SOA serial not incremented Bump serial; systemctl reload named-chroot on primary
REFUSED or NOTAUTH in logs Secondary not in allow-transfer ACL Match ACL to secondary IP or TSIG key
Primary starts, secondary does not Firewall blocks TCP/UDP 53 between hosts firewall-cmd --add-service=dns on both nodes
named-checkzone fails Syntax error in zone file Fix file; re-run check before reload
Two named instances fight for port 53 Both named and named-chroot enabled Disable plain named; use one unit only

References


Summary

You set up BIND primary and secondary servers on Rocky Linux 8 with named-chroot, forward and reverse zones for example.com, and zone transfer to 172.29.10.12. The primary needs allow-transfer and also-notify; the secondary declares type slave zones and stores copies under /var/named/slaves/. When I bumped the SOA serial and reloaded the primary, NOTIFY triggered a transfer, the log showed transferred serial 6, and dig on the secondary returned the new labserver record.

Rocky Linux 8 still ships named-chroot.service; on newer RHEL-family releases you may run plain named without /var/named/chroot and keep the same zone transfer stanzas. If the primary is not in place yet, start with configure a BIND DNS server on Linux. Add TSIG before replication crosses an untrusted network.


Frequently Asked Questions

1. What is the difference between a BIND primary and secondary server?

The primary server holds the writable zone files you edit. The secondary server is read-only and copies zone data from the primary through AXFR or IXFR zone transfers when the SOA serial increases or when NOTIFY tells it to check.

2. Why does the secondary store zone files under /var/named/slaves?

BIND writes transferred zone copies into the path declared in the slave zone file directive. On RHEL-family systems that path is typically slaves/example.com.zone relative to the named working directory.

3. Do I still need named-chroot on current RHEL releases?

Rocky Linux 8 labs in this guide used named-chroot.service. Newer RHEL, Rocky, and AlmaLinux releases often run the plain named service without chroot; match the unit your distribution documents when you adapt the steps.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)