| 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.
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:
sudo hostnamectl set-hostname dns-server.example.testRHEL-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.
sudo dnf install -y bind bind-utilsSample 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
named -vSample 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
systemctl list-unit-files named.serviceSample output:
UNIT FILE STATE PRESET
named.service disabled disabledThe 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:
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:
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.
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; };
};
EOFKey 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:
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
EOFZone-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
YYYYMMDDnnso you can bump it logically after each edit. - Short names on the left (
web) are relative toexample.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:
192.168.56.0/24 → 56.168.192.in-addr.arpaCreate the reverse zone file:
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.
EOFOnly 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.
sudo named-checkconf
echo "named-checkconf exit code: $?"Sample output:
named-checkconf exit code: 0Load-test every zone declaration:
sudo named-checkconf -zSample 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 2026072101Check each custom zone file individually:
sudo named-checkzone example.test /var/named/example.test.zoneSample output:
zone example.test/IN: loaded serial 2026072101
OKsudo named-checkzone 56.168.192.in-addr.arpa /var/named/example.test.revSample output:
zone 56.168.192.in-addr.arpa/IN: loaded serial 2026072101
OKSet ownership and permissions
Zone files must be readable by the named user:
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.revRestore SELinux contexts
On RHEL, Rocky Linux, and AlmaLinux, restore labels after creating new zone files:
sudo restorecon -Rv /etc/named.conf /var/named/example.test.zone /var/named/example.test.revDo 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:
sudo systemctl enable --now namedConfirm the unit is active:
systemctl is-active namedSample output:
activeConfirm listeners on UDP and TCP port 53
sudo ss -lntup | grep namedSample output, trimmed to the lab listeners:
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:
sudo firewall-cmd --permanent --add-service=dns
sudo firewall-cmd --reloadWhen 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:
sudo firewall-cmd --list-servicesSample output:
cockpit dhcpv6-client dns sshTest 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)
dig @192.168.56.10 example.test SOA +noall +answer +commentsSample 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 86400The aa flag means this server is authoritative for example.test.
NS record
dig @192.168.56.10 example.test NS +shortSample output:
dns-server.example.test.A record
dig @192.168.56.10 web.example.test A +shortSample output:
192.168.56.30CNAME record
dig @192.168.56.10 www.example.test CNAME +shortSample output:
web.example.test.MX record
dig @192.168.56.10 example.test MX +shortSample output:
10 mail.example.test.Reverse PTR lookup
dig @192.168.56.10 -x 192.168.56.30 +shortSample 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:
dig @192.168.56.10 www.example.org A +noall +comments +answerSample 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
sudo rndc statusSample 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 runningsudo rndc zonestatus example.testSample output:
name: example.test
type: primary
files: example.test.zone
serial: 2026072101
nodes: 6
last loaded: Mon, 20 Jul 2026 17:27:42 GMTDNS 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:
dig @192.168.56.10 +tcp example.test SOA +noall +answerSample output:
example.test. 86400 IN SOA dns-server.example.test. admin.example.test. 2026072101 3600 900 604800 86400Configure 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:
nmcli -t -f NAME,DEVICE connection show --activeSample output:
Wired connection 1:enp0s8sudo 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:
dig @192.168.56.10 web.example.test +shortgetent hosts exercises the client resolver path through /etc/resolv.conf and NSS:
getent hosts web.example.testSample output:
192.168.56.30 web.example.testIf 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:
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.zoneValidate the zone file:
sudo named-checkzone example.test /var/named/example.test.zoneSample output:
zone example.test/IN: loaded serial 2026072102
OKReload only that zone:
sudo rndc reload example.testSample output:
zone reload queuedConfirm the new record is live:
dig @192.168.56.10 app.example.test A +shortSample output:
192.168.56.31Prefer 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:
sudo journalctl -u named -n 30 --no-pagersudo ss -lntup | grep ':53 'dig @192.168.56.10 +tcp example.test SOA +retry=0 +time=3After 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
- ISC BIND 9 Administrator Reference Manual
- ISC BIND 9 manual pages
- RHEL 10: Configuring a BIND DNS server
- RHEL 10: Configuring zones on a BIND DNS server
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.

