Setting Up BIND9 as an Internal DNS Server on CentOS Stream 9: Zone Files, Forward DNS, SELinux, and firewalld

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

Three Popular DNS Server Options for Internal Networks

After nearly six months running an internal DNS system in a production environment with around 50 servers, I tried three different solutions before settling on BIND9. Here’s what I learned from real-world experience.

The three most popular options for an internal DNS server on CentOS Stream 9:

  • BIND9 (Berkeley Internet Name Domain) — the traditional DNS solution with the most complete feature set
  • dnsmasq — lightweight, easy to configure, ideal for small networks
  • Unbound — focused on recursive DNS, high security, limited authoritative features

Pros and Cons of Each Solution

dnsmasq — Simple but Limited

I started with dnsmasq because the configuration only took 15 minutes. But as the number of hosts grew and I needed to manage multiple zones (lab.company.local, db.company.local…), dnsmasq’s weaknesses started showing. There’s no real zone file support, no AXFR for syncing between primary and secondary DNS, and debugging issues was frustrating due to sparse logs.

Best for: Personal labs, home networks, fewer than 20 hosts, no primary-secondary replication needed.

Unbound — Strong Security but Lacks Authoritative Features

Unbound excels at recursive DNS — that is, forwarding queries out to the internet. But it wasn’t designed to be an authoritative DNS server (the kind that resolves your own internal domain names). Using Unbound to resolve server01.company.local is outside its intended use case, and the configuration ends up being an overly complex workaround.

Best for: Pure recursive resolvers, when DNSSEC validation security is the priority.

BIND9 — More Complex but Worth It

BIND9 is where I ultimately landed. The configuration is more involved, but the payoff is clear: well-structured zone files, support for both forward and reverse lookups, the ability to configure primary-secondary replication, and comprehensive official documentation. On CentOS Stream 9 with SELinux enforcing, BIND9 is also better tested — the SELinux policy for named is already defined in the system.

Best for: Enterprise networks, multiple zones, authoritative DNS requirements, primary-secondary setup for high availability.

Why I Chose BIND9 — Real-World Reasons

My company still has a few servers running CentOS 7, and migrating them to AlmaLinux is a challenge I’ve been working through. During the transition, I needed a DNS server powerful enough to handle both old and new environments in parallel — BIND9, with its ability to configure multiple separate zones, was the natural fit. Furthermore, when the entire infrastructure runs SELinux enforcing (the default on CentOS Stream 9), BIND9 causes almost no issues as long as you put files in the right places.

Deploying BIND9 on CentOS Stream 9

Step 1: Install Packages

sudo dnf install -y bind bind-utils
sudo systemctl enable --now named
sudo systemctl status named

Step 2: Configure named.conf

The main configuration file is at /etc/named.conf. I’m configuring BIND9 as the authoritative DNS for the internal zone company.local and forwarding all other queries to a public DNS:

sudo nano /etc/named.conf
options {
    listen-on port 53 { 127.0.0.1; 192.168.1.10; };  # IP of the DNS server
    listen-on-v6 port 53 { none; };
    directory "/var/named";
    dump-file "/var/named/data/cache_dump.db";
    statistics-file "/var/named/data/named_stats.txt";

    allow-query { localhost; 192.168.1.0/24; };  # Allow only the internal subnet

    # Forward DNS queries to the internet
    forwarders {
        8.8.8.8;
        8.8.4.4;
    };
    forward only;

    recursion yes;
    dnssec-validation yes;
};

logging {
    channel default_debug {
        file "data/named.run";
        severity dynamic;
    };
};

# Internal zone (forward)
zone "company.local" IN {
    type master;
    file "company.local.zone";
    allow-update { none; };
};

# Reverse zone for subnet 192.168.1.0/24
zone "1.168.192.in-addr.arpa" IN {
    type master;
    file "company.local.rev";
    allow-update { none; };
};

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

Step 3: Create the Forward Zone File

sudo nano /var/named/company.local.zone
$TTL 86400
@   IN  SOA     ns1.company.local.  admin.company.local. (
                2024010101  ; Serial (YYYYMMDDNN — increment on every change)
                3600        ; Refresh
                1800        ; Retry
                604800      ; Expire
                86400 )     ; Minimum TTL

; Name servers
@       IN  NS      ns1.company.local.

; A records
ns1         IN  A   192.168.1.10
gateway     IN  A   192.168.1.1
server01    IN  A   192.168.1.20
server02    IN  A   192.168.1.21
db01        IN  A   192.168.1.30
webserver   IN  A   192.168.1.40

; CNAME
www         IN  CNAME   webserver.company.local.

The serial number is a commonly overlooked but critical detail: it must be incremented every time you edit the zone file — BIND9 uses this value to decide whether to reload the zone.

Step 4: Create the Reverse Zone File

sudo nano /var/named/company.local.rev
$TTL 86400
@   IN  SOA     ns1.company.local.  admin.company.local. (
                2024010101
                3600
                1800
                604800
                86400 )

@       IN  NS      ns1.company.local.

; PTR records (reverse lookup)
10      IN  PTR     ns1.company.local.
1       IN  PTR     gateway.company.local.
20      IN  PTR     server01.company.local.
21      IN  PTR     server02.company.local.
30      IN  PTR     db01.company.local.
40      IN  PTR     webserver.company.local.

Step 5: Verify Syntax Before Restarting

# Check named.conf
sudo named-checkconf

# Check forward zone
sudo named-checkzone company.local /var/named/company.local.zone

# Check reverse zone
sudo named-checkzone 1.168.192.in-addr.arpa /var/named/company.local.rev

# If no errors, restart
sudo systemctl restart named

Handling SELinux — The Most Overlooked Part

This is where I spent the most time during my initial setup. SELinux enforcing will block BIND9 if zone files don’t have the correct context. Zone files must be placed in /var/named/ — not in /etc/ or any custom directory you create.

Check the SELinux context of the zone file:

ls -lZ /var/named/company.local.zone

The correct output should include named_zone_t:

-rw-r--r--. 1 root named system_u:object_r:named_zone_t:s0 /var/named/company.local.zone

If the context is wrong (for example, if you copied the file from another location), fix it with:

sudo restorecon -Rv /var/named/
sudo chown root:named /var/named/company.local.zone /var/named/company.local.rev

Check whether SELinux is blocking named:

sudo ausearch -c 'named' -m avc --raw 2>/dev/null | head -20
sudo tail -f /var/log/audit/audit.log | grep named

Configuring firewalld

Open DNS ports (53/udp and 53/tcp) for the internal subnet:

# Method 1: Open dns service for all interfaces
sudo firewall-cmd --permanent --add-service=dns
sudo firewall-cmd --reload

# Method 2: Restrict to internal subnet only (recommended)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="dns" accept'
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-all

Testing the DNS Server

# Test forward lookup
dig @192.168.1.10 server01.company.local

# Test reverse lookup
dig @192.168.1.10 -x 192.168.1.20

# Test forward DNS to the internet
dig @192.168.1.10 google.com

# From a client on the internal network
nslookup server01.company.local 192.168.1.10

Update DNS on the client using NetworkManager:

sudo nmcli con mod "Wired connection 1" ipv4.dns "192.168.1.10"
sudo nmcli con mod "Wired connection 1" ipv4.dns-search "company.local"
sudo nmcli con up "Wired connection 1"

Common Errors and Quick Fixes

  • named won’t start: Check SELinux context first, then journalctl -u named -n 50
  • Query timeout from client: 99% of the time firewalld hasn’t opened the port — firewall-cmd --list-all | grep dns
  • Zone file won’t reload: Serial number wasn’t incremented — update the serial then run sudo rndc reload company.local
  • Permission denied in logs: Zone file doesn’t belong to the named group — chown root:named /var/named/*.zone

Next Steps

This setup has been running stably in production for the past 6 months. The hardest part is really just the first time — especially dealing with SELinux context and firewalld. Once BIND9 is up and running smoothly, it barely needs any maintenance.

If you want to go further, the next step is setting up a secondary DNS server with type slave and AXFR replication — so that when the primary DNS goes down, the internal network can still resolve domain names normally.

Share: