Configure BIND DNS Server on RHEL, Rocky Linux, and AlmaLinux

Install and configure BIND on RHEL, Rocky Linux, or AlmaLinux with forward and reverse zones, controlled recursion, NetworkManager clients, and dig testing.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Configure a BIND primary DNS server on RHEL-family Linux with forward and reverse zones, controlled recursion, dig testing, and NetworkManager clients
Tested on Rocky Linux 10.2 (Red Quartz)
Package bind 9.18.28
Applies to RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Privilege sudo or root
Scope Install and configure BIND on RHEL, Rocky Linux, or AlmaLinux , controlled recursion, NetworkManager clients, and dig testing.
Related guides Add Linux To Windows Ad Domain Realm
Boom Boot Linux Lvm Snapshot Rhel 8 Linux
Boot In Single User Mode Rhel Centos 8 Linux
Boot Rescue Mode Iso Rhel Centos 7
Boot With Old Kernel Version Rhel 8 Grubby

This guide walks through a complete internal primary BIND DNS server on Red Hat Enterprise Linux, Rocky Linux, and AlmaLinux. You install BIND with dnf, declare forward and reverse zones for a lab domain, restrict recursion to a trusted subnet, test lookups with dig, point a client at the server through NetworkManager, and reload a zone after a record change.

The commands below were tested on Rocky Linux 10.2. The same package names, named service unit, /etc/named.conf layout, SELinux contexts, and firewalld rules apply to equivalent RHEL and AlmaLinux releases.

IMPORTANT
This article covers an internal primary BIND server with authoritative local zones and optional recursive resolution for trusted clients. It does not cover public DNS hosting at a registrar, DNSSEC signing, secondary zone transfers, split-horizon views, or DHCP-driven dynamic updates.

Understand the BIND DNS server setup

BIND (Berkeley Internet Name Domain), shipped as the named daemon on RHEL-family systems, answers DNS queries on UDP and TCP port 53. In this lab the server does four jobs:

Role What it does in this lab
Authoritative DNS Answers queries for example.test from zone files you control
Forward lookups Maps names such as web.example.test to IPv4 addresses
Reverse lookups Maps 192.168.56.x addresses back to hostnames through PTR records
Recursive resolver (optional) Forwards queries about external domains to upstream resolvers, but only for clients in a trusted ACL

Lab environment

System Hostname IP address
BIND server dns-server.example.test 192.168.56.10
DNS client dns-client.example.test 192.168.56.20
Test host web.example.test 192.168.56.30

Forward zone: example.test
Reverse zone: 56.168.192.in-addr.arpa
Network: 192.168.56.0/24

I use example.test instead of .local because .local is reserved for multicast DNS on many systems and can collide with mDNS responders.

Set the server hostname before you create zone files if you want the SOA and NS records to match the live system:

bash
sudo hostnamectl set-hostname dns-server.example.test

RHEL-family paths at a glance

RHEL, Rocky Linux, AlmaLinux, and Fedora share the same BIND layout on current releases:

Area Path or package
Server package bind
DNS utilities bind-utils (dig, host, nslookup)
Service unit named
Main configuration /etc/named.conf
Zone directory /var/named/
Crypto policy include /etc/crypto-policies/back-ends/bind.config

Install BIND on RHEL, Rocky Linux, and AlmaLinux

Install the BIND server package and client utilities on the DNS server host. Client utilities ship in bind-utils, separate from the bind server package.

Package installs use the dnf command.

bash
sudo dnf install -y bind bind-utils

Sample output:

output
Installed:
  bind-32:9.18.33-15.el10_2.2.x86_64
  bind-utils-32:9.18.33-15.el10_2.2.x86_64
  bind-dnssec-utils-32:9.18.33-15.el10_2.2.x86_64
Complete!

The same command works on RHEL, Rocky Linux, AlmaLinux, and Fedora when the bind package is enabled in your repositories.

Verify the installed version

bash
named -v

Sample output:

output
BIND 9.18.33 (Extended Support Version) <id:>

The exact patch level varies by distribution repository and minor release.

Confirm the named service unit

bash
systemctl list-unit-files named.service

Sample output:

output
UNIT FILE       STATE    PRESET
named.service   disabled disabled

The STATE column can read enabled or disabled depending on whether this host previously ran BIND. The important result here is that named.service exists before you enable it.

Check whether port 53 is already in use

Another resolver (systemd-resolved, dnsmasq, or an old named instance) may already bind port 53. List listeners with the ss command:

bash
sudo ss -lntup | grep ':53 '

If the command prints nothing, port 53 is free on this host.

Back up the default configuration

Distribution packages ship a caching-only template. Copy it before you replace options:

bash
sudo cp -a /etc/named.conf /etc/named.conf.bak.$(date +%Y%m%d)

Configure BIND server options

The example below replaces /etc/named.conf on RHEL, Rocky Linux, and AlmaLinux. Distribution packages ship a caching-only template; this configuration turns the host into a primary authoritative server for your lab domain.

An ACL limits which client addresses may query the server and use recursion. I allow IPv4 loopback, IPv6 loopback (::1), and the lab subnet 192.168.56.0/24 only.

bash
sudo tee /etc/named.conf > /dev/null << 'EOF'
//
// Primary BIND DNS server for example.test
//

acl "trusted-clients" {
    127.0.0.1;
    ::1;
    192.168.56.0/24;
};

options {
    listen-on port 53 { 127.0.0.1; 192.168.56.10; };
    listen-on-v6 port 53 { ::1; };
    directory       "/var/named";

    allow-query { trusted-clients; };

    recursion yes;
    allow-recursion { trusted-clients; };

    forwarders {
        1.1.1.1;
        8.8.8.8;
    };

    dnssec-validation yes;

    managed-keys-directory "/var/named/dynamic";

    include "/etc/crypto-policies/back-ends/bind.config";
};

zone "." IN {
    type hint;
    file "named.ca";
};

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

zone "example.test" IN {
    type primary;
    file "example.test.zone";
    allow-update { none; };
    allow-transfer { none; };
};

zone "56.168.192.in-addr.arpa" IN {
    type primary;
    file "example.test.rev";
    allow-update { none; };
    allow-transfer { none; };
};
EOF

Key options in plain language:

Setting Purpose
listen-on Bind UDP/TCP port 53 on loopback and the server lab address
listen-on-v6 Answer on IPv6 loopback (::1) in this lab
directory Base path for zone file names declared in file
allow-query Which clients may query any zone
recursion / allow-recursion Permit recursive lookups, but only for the trusted ACL
forwarders Upstream resolvers tried first for names outside your zones
dnssec-validation Validates DNSSEC data received during recursive resolution
managed-keys-directory Directory BIND uses for managed DNSSEC trust anchors
bind.config include Applies the RHEL-family system-wide cryptographic policy
include "/etc/named.rfc1912.zones" Loads distribution-provided localhost and reverse-localhost zones
include "/etc/named.root.key" Supplies the root trust anchor used for DNSSEC validation
Zone "." hint Root hints used when BIND performs iterative resolution
allow-transfer (per zone) Controls which hosts may copy the complete zone; none blocks AXFR/IXFR until you add a secondary

Without forward only;, BIND tries the configured forwarders first and falls back to querying authoritative DNS servers itself if the forwarders do not respond. Add forward only; inside the options block when all external resolution must go exclusively through the configured upstream servers.

Modern BIND accepts type primary in zone blocks. Older examples use the legacy keyword master; both mean the same thing for a writable zone.

Do not expose unrestricted recursion to the Internet. The ACL above keeps recursive service on the lab LAN only. When you add a secondary server later, replace allow-transfer { none; }; with the secondary addresses or TSIG-authorized transfer partners.


Create the forward DNS zone

The forward zone file holds address records for names under example.test.

Create /var/named/example.test.zone:

bash
sudo tee /var/named/example.test.zone > /dev/null << 'EOF'
$TTL 86400
@   IN  SOA     dns-server.example.test. admin.example.test. (
                2026072101  ; serial (YYYYMMDDnn)
                3600        ; refresh
                900         ; retry
                604800      ; expire
                86400 )     ; minimum

    IN  NS      dns-server.example.test.

dns-server  IN  A       192.168.56.10
dns-client  IN  A       192.168.56.20
web         IN  A       192.168.56.30
www         IN  CNAME   web.example.test.
@           IN  MX  10  mail.example.test.
mail        IN  A       192.168.56.30
EOF

Zone-file habits worth keeping:

  • FQDNs in the right-hand side of records end with a trailing dot (dns-server.example.test.). Without the dot, BIND appends the zone origin.
  • The SOA serial uses YYYYMMDDnn so you can bump it logically after each edit.
  • Short names on the left (web) are relative to example.test.
  • After every change, increment the serial before you reload the zone.

This standalone lab uses one NS record, which is enough for testing. A redundant deployment should publish additional authoritative servers.

Add the matching zone declaration if you have not already done so in named.conf (shown in the previous section).


Create the reverse DNS zone

Reverse lookups use a special domain derived from the IPv4 network. For 192.168.56.0/24, reverse the first three octets and append in-addr.arpa:

text
192.168.56.0/24  →  56.168.192.in-addr.arpa

Create the reverse zone file:

bash
sudo tee /var/named/example.test.rev > /dev/null << 'EOF'
$TTL 86400
@   IN  SOA     dns-server.example.test. admin.example.test. (
                2026072101
                3600
                900
                604800
                86400 )

    IN  NS      dns-server.example.test.

10  IN  PTR     dns-server.example.test.
20  IN  PTR     dns-client.example.test.
30  IN  PTR     web.example.test.
EOF

Only the last octet of each address appears on the left side because the zone origin already ends with 56.168.192.in-addr.arpa. Classless reverse delegations and IPv6 ip6.arpa zones follow the same idea but use different prefix lengths; this article stays with a full /24 reverse zone.


Validate and start BIND

Validate syntax before you start the service. A clean named-checkconf run produces no output.

bash
sudo named-checkconf
echo "named-checkconf exit code: $?"

Sample output:

output
named-checkconf exit code: 0

Load-test every zone declaration:

bash
sudo named-checkconf -z

Sample output:

output
zone localhost.localdomain/IN: loaded serial 0
zone localhost/IN: loaded serial 0
zone 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa/IN: loaded serial 0
zone 1.0.0.127.in-addr.arpa/IN: loaded serial 0
zone 0.in-addr.arpa/IN: loaded serial 0
zone example.test/IN: loaded serial 2026072101
zone 56.168.192.in-addr.arpa/IN: loaded serial 2026072101

Check each custom zone file individually:

bash
sudo named-checkzone example.test /var/named/example.test.zone

Sample output:

output
zone example.test/IN: loaded serial 2026072101
OK
bash
sudo named-checkzone 56.168.192.in-addr.arpa /var/named/example.test.rev

Sample output:

output
zone 56.168.192.in-addr.arpa/IN: loaded serial 2026072101
OK

Set ownership and permissions

Zone files must be readable by the named user:

bash
sudo chown root:named /etc/named.conf
sudo chmod 640 /etc/named.conf
sudo chown root:named /var/named/example.test.zone /var/named/example.test.rev
sudo chmod 640 /var/named/example.test.zone /var/named/example.test.rev

Restore SELinux contexts

On RHEL, Rocky Linux, and AlmaLinux, restore labels after creating new zone files:

bash
sudo restorecon -Rv /etc/named.conf /var/named/example.test.zone /var/named/example.test.rev

Do not disable SELinux to work around context errors. Permission denials in /var/log/audit/audit.log or journalctl -u named often point at a missing restorecon.

Start and enable the service

Use the systemctl command to enable the service at boot and start it now:

bash
sudo systemctl enable --now named

Confirm the unit is active:

bash
systemctl is-active named

Sample output:

output
active

Confirm listeners on UDP and TCP port 53

bash
sudo ss -lntup | grep named

Sample output, trimmed to the lab listeners:

output
udp   UNCONN 0  0  192.168.56.10:53  0.0.0.0:*  users:(("named",pid=20156,fd=34))
udp   UNCONN 0  0      127.0.0.1:53  0.0.0.0:*  users:(("named",pid=20156,fd=28))
udp   UNCONN 0  0              [::1]:53            [::]:*  users:(("named",pid=20156,fd=37))
tcp   LISTEN 0  10 192.168.56.10:53  0.0.0.0:*  users:(("named",pid=20156,fd=36))
tcp   LISTEN 0  10     127.0.0.1:53  0.0.0.0:*  users:(("named",pid=20156,fd=29))
tcp   LISTEN 0  10             [::1]:53            [::]:*  users:(("named",pid=20156,fd=39))

Both UDP and TCP should listen on the addresses you declared in listen-on and listen-on-v6.

Open the firewall

RHEL, Rocky Linux, and AlmaLinux use firewalld by default. Allow the DNS service permanently in the active zone:

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

When the DNS interface belongs to a non-default firewalld zone, include --zone=<active-zone> in both the add and verification commands.

Confirm the rule is active:

bash
sudo firewall-cmd --list-services

Sample output:

output
cockpit dhcpv6-client dns ssh

Test forward, reverse, and recursive DNS

Query the server directly with dig so you test BIND itself, not the client resolver stack. The dig command cheat sheet covers flags used here.

SOA record (authoritative)

bash
dig @192.168.56.10 example.test SOA +noall +answer +comments

Sample output:

output
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13674
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
example.test.		86400	IN	SOA	dns-server.example.test. admin.example.test. 2026072101 3600 900 604800 86400

The aa flag means this server is authoritative for example.test.

NS record

bash
dig @192.168.56.10 example.test NS +short

Sample output:

output
dns-server.example.test.

A record

bash
dig @192.168.56.10 web.example.test A +short

Sample output:

output
192.168.56.30

CNAME record

bash
dig @192.168.56.10 www.example.test CNAME +short

Sample output:

output
web.example.test.

MX record

bash
dig @192.168.56.10 example.test MX +short

Sample output:

output
10 mail.example.test.

Reverse PTR lookup

bash
dig @192.168.56.10 -x 192.168.56.30 +short

Sample output:

output
web.example.test.

External name through recursion

When recursion is enabled for your subnet, BIND tries the configured forwarders first for external names. Without forward only;, it can fall back to iterative resolution if a forwarder does not answer. Use a verbose dig query so you can read the header flags:

bash
dig @192.168.56.10 www.example.org A +noall +comments +answer

Sample output:

output
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24183
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; ANSWER SECTION:
www.example.org.	300	IN	A	<current-ip-address>

The ra flag shows that recursive service is available. The absence of aa confirms that this server is not authoritative for the external domain. The returned address and TTL can change.

BIND uses the aa flag for authoritative answers, while recursive responses normally show ra when recursion is available. The configured forwarders are tried first, with normal forward-first fallback unless forward only; is set.

rndc status checks

bash
sudo rndc status

Sample output:

output
version: BIND 9.18.33 (Extended Support Version) <id:>
running on localhost: Linux x86_64 6.12.0-211.34.1.el10_2.x86_64
boot time: Mon, 20 Jul 2026 17:27:53 GMT
number of zones: 106 (98 automatic)
server is up and running
bash
sudo rndc zonestatus example.test

Sample output:

output
name: example.test
type: primary
files: example.test.zone
serial: 2026072101
nodes: 6
last loaded: Mon, 20 Jul 2026 17:27:42 GMT

DNS over TCP

DNS clients may retry over TCP when a UDP response is truncated or too large, while full zone transfers use TCP. Verify TCP answers work:

bash
dig @192.168.56.10 +tcp example.test SOA +noall +answer

Sample output:

output
example.test.		86400	IN	SOA	dns-server.example.test. admin.example.test. 2026072101 3600 900 604800 86400

Configure a RHEL-family client to use BIND

On a client such as dns-client.example.test, point the system resolver at the BIND server instead of editing /etc/hosts on every machine. For a small lab, /etc/hosts still works, but a central DNS server scales better when you add nodes.

Set DNS with NetworkManager

Identify the connection name, then set DNS and search domain. The nmcli command manages NetworkManager from the shell on RHEL, Rocky Linux, and AlmaLinux:

bash
nmcli -t -f NAME,DEVICE connection show --active

Sample output:

output
Wired connection 1:enp0s8
bash
sudo nmcli connection modify "Wired connection 1" ipv4.dns "192.168.56.10" ipv4.dns-search "example.test" ipv4.ignore-auto-dns yes
sudo nmcli connection up "Wired connection 1"

ipv4.ignore-auto-dns yes stops DHCP from prepending other resolvers ahead of your BIND server.

Verify from the client

Direct dig queries test BIND:

bash
dig @192.168.56.10 web.example.test +short

getent hosts exercises the client resolver path through /etc/resolv.conf and NSS:

bash
getent hosts web.example.test

Sample output:

output
192.168.56.30   web.example.test

If getent fails but dig @192.168.56.10 works, check that /etc/resolv.conf lists only your BIND address (or lists it first) and that no local caching resolver overrides it.


Add or update DNS records

The normal change workflow is edit, bump serial, validate, reload, query.

Add a host record for app.example.test in the forward zone file:

bash
sudo sed -i 's/2026072101/2026072102/' /var/named/example.test.zone
echo "app         IN  A       192.168.56.31" | sudo tee -a /var/named/example.test.zone

Validate the zone file:

bash
sudo named-checkzone example.test /var/named/example.test.zone

Sample output:

output
zone example.test/IN: loaded serial 2026072102
OK

Reload only that zone:

bash
sudo rndc reload example.test

Sample output:

output
zone reload queued

Confirm the new record is live:

bash
dig @192.168.56.10 app.example.test A +short

Sample output:

output
192.168.56.31

Prefer rndc reload <zone> over systemctl restart named for routine zone-file changes. Use systemctl reload named after named.conf changes.


Troubleshoot BIND DNS failures

Symptom Likely cause Fix
BIND service fails to start Syntax error in named.conf or a zone file Run sudo named-checkconf -z and named-checkzone; read journalctl -u named
connection refused on dig Service stopped or not listening on the queried address systemctl status named; confirm listen-on includes the target IP with ss -lntup
Query times out Firewall, routing, or wrong listener address Open UDP/TCP 53; verify client can reach the server IP; check listen-on
REFUSED response Client outside allow-query or allow-recursion ACL Add the client subnet to trusted-clients and reload
SERVFAIL response Zone syntax error, permissions, bad forwarder, or DNSSEC validation failure Check zone files, ownership, forwarder reachability, and resolver logs
Forward lookup works, reverse fails Missing or wrong reverse zone or PTR Confirm 56.168.192.in-addr.arpa is declared and PTR host octets match
Server answers locally but not from clients Host firewall or ACL firewall-cmd --list-services; verify client source IP is in the ACL
Internal names work, external names fail Recursion disabled or forwarders unreachable Set recursion yes, allow the client in allow-recursion, test forwarder IPs
Zone changes do not appear Serial not incremented or zone not reloaded Bump SOA serial, named-checkzone, then rndc reload example.test
Permission denied in logs Wrong ownership or SELinux context chown root:named and chmod 640 on zone files; run restorecon on /etc/named.conf and zone files

Useful commands:

bash
sudo journalctl -u named -n 30 --no-pager
bash
sudo ss -lntup | grep ':53 '
bash
dig @192.168.56.10 +tcp example.test SOA +retry=0 +time=3

After changing named.conf, run named-checkconf and then sudo systemctl reload named. After a zone-file-only change, validate the file and run sudo rndc reload <zone>.

For secondary servers and zone transfers, see configure a BIND primary and secondary DNS server. For a lightweight caching resolver instead of full BIND authority, see configure a DNS caching server with dnsmasq.


References


Summary

You installed BIND with dnf on a RHEL-family host, defined a trusted-client ACL with controlled recursion, created matching forward and reverse zones for example.test, validated configuration with named-checkconf and named-checkzone, opened firewalld port 53, verified lookups with dig and rndc, pointed a NetworkManager client at the server, and reloaded a zone after a record change. Keep secondary DNS, DNSSEC, caching-only designs, and Ubuntu-family BIND setup in separate guides so this page stays the canonical primary BIND walkthrough for RHEL, Rocky Linux, and AlmaLinux.


Frequently Asked Questions

1. What is the difference between a forward zone and a reverse zone in BIND?

A forward zone maps hostnames to IP addresses (A and CNAME records). A reverse zone maps IP addresses back to names through PTR records under an in-addr.arpa domain derived from the network prefix.

2. Should I enable recursion on an internal authoritative BIND server?

You can enable recursion for trusted local clients so they resolve external names through your server, but restrict it with an ACL. Never run an open recursive resolver on the public Internet.

3. Why must I increment the SOA serial after editing a zone file?

Secondary servers compare SOA serial numbers and transfer a new copy only when the primary serial is higher. Resolver caches are separate and continue using previously cached records until their TTL expires.

4. What is the aa flag in dig output?

The aa (authoritative answer) flag means the responding server is authoritative for the queried zone. For example.test lookups against your primary BIND server, you should see aa set in the flags line.

5. When should I use rndc reload instead of restarting named?

Use rndc reload example.test after validating a change to the existing example.test zone file. It reloads only that authoritative zone and does not flush the recursive cache. After changing named.conf, validate it with named-checkconf and reload the named service. Restart the service only when it is stopped or when a reload cannot apply the change.

6. Does this BIND guide apply to Fedora or Ubuntu?

Fedora uses the same bind package, named service, and /var/named layout as RHEL, so most steps transfer directly. Ubuntu and Debian use bind9, /etc/bind/, and AppArmor instead; follow a separate Ubuntu-family BIND guide for those paths.
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 …