Bits, Bytes, & Radio Waves

A quiet journey through discovery and understanding.

Building a Dual-Stack BIND 9 DNS Infrastructure on Rocky Linux

Overview

This post will provide a documented build of two BIND 9 DNS servers in a primary/secondary configuration. I will be using a Rocky Linux 10.1 Minimal ISO to do the base install, but will not be documenting that installation.

I wanted to research and learn how to configure a dual stack, IPv4 and IPv6 setup. There definitely are some quirks to IPv6 that I was not aware of prior to this.


RTFM

BIND 9 Administrator Reference Manual


Initial Configuration

There are a few items that should be accomplished before moving forward. The first is to update the system.

sudo dnf update -y

The next thing is to verify that hostnames and IP addresses are configured and expected.

hostname

This should produce a fully qualified domain name, not a short name.

ip address

This should produce address information for IPv4, IPv6 Global Unique, and IPv6 Link-Local.

Now is a good time to record these addresses.


Download the Packages

I am using the repository packages to install bind.

sudo dnf install -y bind bind-utils

BIND File Structure and Terminology

Bind uses a single configuration file called named.conf. On both servers, this configuration file is located at /etc/named.conf. The file consists of:

Comment

There are various comment styles.

/* This is a BIND comment as in C */
// This is a BIND comment as in C++
# This is a BIND comment as in common Unix shells
# and Perl
; This is a comment used in a zone file
Block

Blocks are containers for statements

Statement

Statements define and control specific behaviors. They may have a single value parameter or consist of multiple argument/value pairs.

Primary Server

On the primary server, build out zone directories for containing forward-mapped zone files and reverse-mapped zone files.

sudo mkdir -p /var/named/zones/{forward, reverse}

The directory should like this:

/var/named
├── data
│   └── named.run
├── dynamic
│   ├── managed-keys.bind
│   └── managed-keys.bind.jnl
├── named.ca
├── named.empty
├── named.localhost
├── named.loopback
└── zones
    ├── forward
    └── reverse

Now apply the appropriate permissions:

sudo find /var/named/zones -type d -exec chmod 750 {} \;
sudo find /var/named/zones -type d -exec chown named:named {} \;

Let’s also ensure SELinux contexts are restored.

sudo restorecon -Rv /var/named

Configure the Service

Primary Server – named.conf

Open up the named.conf file in a text editor.

sudo vi /etc/named.conf

Optionally, set line numbers for easier reference.

:set nu

The statements in the blocks do not need to be in any particular order in the file. The only requirement is the level and block at which they are added. They can be in a logical order or alphabetical. I will add in a truncated example here, which I have sorted alphabetically, but the full list can be found in the BIND Administrator Manual, options Block Grammar section.

options {
	allow-query { <address_match_element>; ... };
	allow-recursion { <address_match_element>; ... };
	directory <quoted_string>;
	dnssec-validation ( yes | no | auto );
	dump-file <quoted_string>;
	forward ( first | only );
	forwarders [ port <integer> ] [ tls <string> ] { ( <ipv4_address> | <ipv6_address> ) [ port <integer> ] [ tls <string> ]; ... };
	geoip-directory ( <quoted_string> | none );
	listen-on [ port <integer> ] [ proxy <string> ] [ tls <string> ] [ http <string> ] { <address_match_element>; ... }; // may occur multiple times
	listen-on-v6 [ port <integer> ] [ proxy <string> ] [ tls <string> ] [ http <string> ] { <address_match_element>; ... }; // may occur multiple times
	managed-keys-directory <quoted_string>;
	memstatistics-file <quoted_string>;
	pid-file ( <quoted_string> | none );
	recursing-file <quoted_string>;
	recursion <boolean>;
	secroots-file <quoted_string>;
	session-keyfile ( <quoted_string> | none );
	statistics-file <quoted_string>;
};

Options Block

options Block Grammar


Interfaces Statements

In the options block, ensure the following two statements are configured. The first statement is for IPv4 and the second is for IPv6.

listen-on port 53 { 
    127.0.0.1; 
    172.16.3.100;
};

listen-on-v6 port 53 { 
    ::1;    
    2603:7080:7407:dd36:20c:29ff:fea1:c8b5;
};

Access Control

The next statement can be tightened for security, but I am allowing any.

allow-query     { any; };

Next, I want to ensure this DNS recursively goes out to get an answer. I also specify from which subnets these are permitted.

recursion yes;
allow-recursion {
    127.0.0.1;
    172.16.0.0/16;
    2603:7080:7407:dd36::/64;
};

Forwarding

In the next statement, I want to ensure that if the forwarders are down, the query will fail. In the forwarders statement, I provided a list of where to forward to.

forward only;
forwarders {
    1.1.1.1;
    1.0.0.1;
    2606:4700:4700::1111;
    2606:4700:4700::1001;
};

Boolean Options

I set the dnssec-validation to auto so that it configures itself.

dnssec-validation auto;

Zone Block

zone Block Grammar

At the end of the /etc/named.conf file is where I started to add zone blocks.

This is an example of a zone block.

zone "lab.aaronrombaut.com" IN {
    type primary;
    file "zones/forward/lab.aaronrombaut.com.zone";

    allow-transfer {
        172.16.3.101;
        2603:7080:7407:dd36:20c:29ff:fe87:ff78;
    };

    notify explicit;
    also-notify {
        172.16.3.101;
        2603:7080:7407:dd36:20c:29ff:fe87:ff78;
    };
};

A couple things to note here:

type primary;

This specifies the kind of zone.

file <quoted_string>;

This specifies the zone’s filename.

allow-transfer [ port <integer> ] [ transport <string> ] { <address_match_element>; ... };

This specifies which hosts are allowed to receive zone transfers from the server.

notify ( explicit | master-only | primary-only | <boolean> );
also-notify [ port <integer> ] [ source ( <ipv4_address> | * ) ] [ source-v6 ( <ipv6_address> | * ) ] { ( <server-list> | <ipv4_address> [ port <integer> ] | <ipv6_address> [ port <integer> ] ) [ key <string> ] [ tls <string> ]; ... };

This one is a little tricky, there is some logic behind it. The also-notify option is only meaningful when there is a notify option. Notify has various logic built-in.

notify yes

This is the default setting. DNS NOTIFY(SOA) messages are sent when a zone the server is authoritative for changes.

Messages are sent to the servers listed in the zone’s NS records and to any servers listed in the also-notify option.

notify primary-only

Notifies are only sent for primary zones.

notify explicit

Notifies are sent only to servers explicitly listed in the also-notify option.

notify no

No notifies are sent.


Primary Server – Zone Files

One thing to note — and this may be common knowledge for BIND administrators — is that the format for the serial is:

YYYYMMDDnn

YYYY = Year
MM = Month
DD = Day
nn = Revision number for that day

An example of this is:

2026012801

Forward-Mapped Zone File

This is an example of a forward zone file.

$TTL 1h
;------------------------------------------------------------------------------
; Forward Zone: lab.aaronrombaut.com
; Purpose     : Authoritative records for lab hosts (A/AAAA/CNAME/MX/TXT/etc.)
; Notes       :
;   - Use semicolons for comments.
;   - Trailing dot (.) means "absolute FQDN". Without it, BIND appends the zone.
;   - Parentheses allow a single record (SOA) to span multiple lines.
;   - Keep the serial monotonically increasing (recommended format: YYYYMMDDnn).
;------------------------------------------------------------------------------

@   IN  SOA dnsntp1-v163-100.lab.aaronrombaut.com. admin.lab.aaronrombaut.com. (
        2026012801 ; serial   (YYYYMMDDnn)
        1h         ; refresh  (slave checks for updates)
        15m        ; retry    (if refresh fails)
        1w         ; expire   (slave stops serving after this)
        1h )       ; minimum  (negative cache TTL)

;------------------------------------------------------------------------------
; Authoritative name servers for this zone
;------------------------------------------------------------------------------
    IN  NS  dnsntp1-v163-100.lab.aaronrombaut.com.
    IN  NS  dnsntp2-v163-101.lab.aaronrombaut.com.

;------------------------------------------------------------------------------
; Optional: zone apex records
;------------------------------------------------------------------------------
;@   IN  A     172.16.3.10
;@   IN  AAAA  2603:7080:7407:dd36::10
;@   IN  TXT   "lab zone - authoritative DNS via dnsntp1/dnsntp2"

;------------------------------------------------------------------------------
; Infrastructure (examples)
; Keep hostnames on the left as *relative* names (no trailing dot).
;------------------------------------------------------------------------------
dnsntp1-v163-100    IN  A     172.16.3.100
dnsntp1-v163-100    IN  AAAA  2603:7080:7407:dd36::100

dnsntp2-v163-101    IN  A     172.16.3.101
dnsntp2-v163-101    IN  AAAA  2603:7080:7407:dd36::101

; Root and Intermediate CA (examples)
rootca-v163-102     IN  A     172.16.3.102
rootca-v163-102     IN  AAAA  2603:7080:7407:dd36::102

intca1-v163-103     IN  A     172.16.3.103
intca1-v163-103     IN  AAAA  2603:7080:7407:dd36::103

;------------------------------------------------------------------------------
; Friendly service aliases (stable names you point clients at)
;------------------------------------------------------------------------------
dns                 IN  CNAME dnsntp1-v163-100
ntp                 IN  CNAME dnsntp1-v163-100

;------------------------------------------------------------------------------
; Web/app examples (optional)
;------------------------------------------------------------------------------
;web                 IN  A     172.16.10.50
;web                 IN  AAAA  2603:7080:7407:dd3A::50
;wiki                IN  CNAME web

;------------------------------------------------------------------------------
; End of zone
;------------------------------------------------------------------------------

Reverse-Mapped Zone File – IPv4

This is an example of a reverse zone file for IPv4.

$TTL 1h
;------------------------------------------------------------------------------
; IPv4 Reverse Zone: 3.16.172.in-addr.arpa
; Purpose           : PTR records for 172.16.3.0/24
; Notes             :
;   - Left-hand side is the last octet only (100, 101, etc.)
;------------------------------------------------------------------------------

@   IN  SOA dnsntp1-v163-100.lab.aaronrombaut.com. admin.lab.aaronrombaut.com. (
        2026012801
        1h
        15m
        1w
        1h )

    IN  NS  dnsntp1-v163-100.lab.aaronrombaut.com.
    IN  NS  dnsntp2-v163-101.lab.aaronrombaut.com.

; PTRs
100 IN  PTR dnsntp1-v163-100.lab.aaronrombaut.com.
101 IN  PTR dnsntp2-v163-101.lab.aaronrombaut.com.
102 IN  PTR rootca-v163-102.lab.aaronrombaut.com.
103 IN  PTR intca1-v163-103.lab.aaronrombaut.com.

Reverse-Mapped Zone File – IPv6

This is an example of a reverse zone file for IPv6. Note the records are nibble-reversed.

$TTL 1h
;------------------------------------------------------------------------------
; IPv6 Reverse Zone: 6.3.d.d.7.0.4.7.0.8.0.7.3.0.6.2.ip6.arpa
; Purpose           : PTR records for 2603:7080:7407:dd36::/64
;
; Notes (important):
;   - IPv6 reverse is nibble-based.
;   - For a /64 zone, the owner name contains the *host* nibbles (last 64 bits),
;     reversed, which is 16 nibbles total.
;   - Example host ::0100 (i.e., ...:0000:0000:0000:0100) becomes:
;       0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0
;------------------------------------------------------------------------------

@   IN  SOA dnsntp1-v163-100.lab.aaronrombaut.com. admin.lab.aaronrombaut.com. (
        2026012801
        1h
        15m
        1w
        1h )

    IN  NS  dnsntp1-v163-100.lab.aaronrombaut.com.
    IN  NS  dnsntp2-v163-101.lab.aaronrombaut.com.

; PTRs for ::100, ::101, ::102, ::103 (aligned with IPv4 last octet)
; ::0100 -> 0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0
0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR dnsntp1-v163-100.lab.aaronrombaut.com.

; ::0101 -> 1.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0
1.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR dnsntp2-v163-101.lab.aaronrombaut.com.

2.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR rootca-v163-102.lab.aaronrombaut.com.
3.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR intca1-v163-103.lab.aaronrombaut.com.

Secondary Server

On the secondary server, build out a secondary directory.

sudo mkdir -p /var/named/secondary

The directory should look like this:

/var/named
├── data
│   └── named.run
├── dynamic
│   ├── managed-keys.bind
│   └── managed-keys.bind.jnl
├── named.ca
├── named.empty
├── named.localhost
├── named.loopback
└── secondary

Now apply the appropriate permissions:

sudo find /var/named/secondary -type d -exec chmod 750 {} \;
sudo find /var/named/secondary -type d -exec chown named:named {} \;

Let’s also ensure SELinux contexts are restored.

sudo restorecon -Rv /var/named

Secondary Server – named.conf

Copy the named.conf file from the primary server. Open up the named.conf file in a text editor.

sudo vi /etc/named.conf

Optionally, set line numbers for easier reference.

:set nu

Update the IP addresses in the listen-on and listen-on-v6 statements with the appropriate addresses from this server.

The only next thing to do is change the options in the zone blocks. The type will be secondary and there will be a primaries option containing a list of primary server addresses.

zone "lab.aaronrombaut.com" IN {
    type secondary;
    primaries {
        172.16.3.100;
        2603:7080:7407:dd36:20c:29ff:fea1:c8b5;
    };
    file "secondary/lab.aaronrombaut.com.zone";
};

Validation Commands

The following commands should be used to validate configurations. They are part of the bind-utils package.

The following can be run on both primary and secondary servers. This will check the named.conf file for any errors.

sudo named-checkconf

These next three blocks check the zone files for correctness on the primary server. If named-checkconf and the zone checks succeed, reloading named should be safe.

sudo named-checkzone lab.aaronrombaut.com \
  /var/named/zones/forward/lab.aaronrombaut.com.zone
sudo named-checkzone 3.16.172.in-addr.arpa \
  /var/named/zones/reverse/3.16.172.in-addr.arpa.zone
sudo named-checkzone 6.3.d.d.7.0.4.7.0.8.0.7.3.0.6.2.ip6.arpa \
  /var/named/zones/reverse/6.3.d.d.7.0.4.7.0.8.0.7.3.0.6.2.ip6.arpa.zone

Reload named after changes.

sudo rndc reload

And then check the zonestatus on both the primary and secondary servers:

sudo rndc zonestatus lab.aaronrombaut.com

Primary

On the primary server, it should reflect the type as primary for the zone.

name: lab.aaronrombaut.com
type: primary
files: zones/forward/lab.aaronrombaut.com.zone
serial: 2026092801
nodes: 3
last loaded: Tue, 27 Jan 2026 15:33:40 GMT
secure: no
dynamic: no
reconfigurable via modzone: no

Secondary

On the secondary server, it should reflect the type as secondary for the zone.

name: lab.aaronrombaut.com
type: secondary
files: secondary/lab.aaronrombaut.com.zone
serial: 2026092801
nodes: 3
last loaded: Wed, 28 Jan 2026 15:42:13 GMT
next refresh: Wed, 28 Jan 2026 17:22:28 GMT
expires: Wed, 04 Feb 2026 15:42:13 GMT
secure: no
dynamic: no
reconfigurable via modzone: no

Firewall Configuration

Get a list of the current defined services.

sudo firewall-cmd --list-services

If DNS is not present, add it.

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

Then reload the firewall configuration.

sudo firewall-cmd --reload

Conclusion

This build provides a reliable DNS foundation for a lab environment: the primary server remains the single source of truth for zone content, while the secondary automatically receives updates via zone transfers, improving resiliency and reducing configuration drift. With recursion handled via forwarders, SELinux contexts restored, and firewall rules in place for DNS and NTP, the result is a clean, repeatable deployment that supports both IPv4 and IPv6.

Leave a Reply

Your email address will not be published. Required fields are marked *