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.
