DNS HOWTO: resolvers, Unbound and BIND 9 on Linux today
DNS on Linux has three separate jobs: the resolver on every machine that asks questions, an optional caching resolver that answers for your whole network, and an authoritative server that publishes your own zone. This page sets up each one with current tools: systemd-resolved, Unbound and BIND 9.20, and shows how to debug with dig and drill.
The classic DNS HOWTO from 2001 (BIND 8 era) is kept by the Linux Documentation Project at tldp.org. Everything below is written for today’s releases.
The resolver on every machine
Programs do not talk DNS themselves. They call the C library (getaddrinfo()), which reads the hosts: line in /etc/nsswitch.conf to decide where to look: /etc/hosts, DNS, mDNS and so on. Use getent to test the same path your programs use:
grep '^hosts' /etc/nsswitch.conf
getent hosts www.example.com
ls -l /etc/resolv.conf
systemd-resolved
On Ubuntu and Fedora, systemd-resolved is the local resolver. It runs a stub listener on 127.0.0.53 (full features, including LLMNR and mDNS) and on 127.0.0.54 (plain “proxy” mode). In its recommended mode, /etc/resolv.conf is a symlink to /run/systemd/resolve/stub-resolv.conf, which lists only nameserver 127.0.0.53 plus your search domains. The real upstream servers are kept per network interface; you see them with resolvectl:
resolvectl status
resolvectl query www.example.com
resolvectl statistics
sudo resolvectl flush-caches
For glibc programs to use resolved directly (not only through the stub), the nss-resolve module must be in nsswitch. The systemd manual recommends this line:
hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
On Debian 13, resolved is a separate package (systemd-resolved, plus libnss-resolve for the NSS module) and is not installed by default; ifupdown or NetworkManager writes a plain /etc/resolv.conf instead. RHEL, Alma and Rocky also normally let NetworkManager write that file. In that case, cat /etc/resolv.conf shows the real servers.
Do not hand-edit a symlink. If /etc/resolv.conf points into /run/systemd/resolve/, your edits are lost. Set servers through NetworkManager (nmcli connection modify … ipv4.dns), systemd-networkd, or a resolved drop-in file instead.
To send every query from a machine to your own resolver, create /etc/systemd/resolved.conf.d/lan-dns.conf:
[Resolve]
DNS=192.168.1.1
Domains=~.
Domains=~. is a “route-only” domain for the root, which means “use this server for all names”. Apply it with sudo systemctl restart systemd-resolved.
A caching resolver with Unbound
A caching resolver answers for many machines, remembers answers until their TTL runs out, and checks DNSSEC signatures. Unbound (from NLnet Labs) does this job well and has nothing to do with publishing zones. By default it listens only on localhost and refuses queries from anywhere except 127.0.0.0/8, so you must open it to your LAN on purpose.
# Debian / Ubuntu
sudo apt install unbound
# Fedora / RHEL / Alma / Rocky
sudo dnf install unbound
On Debian and Ubuntu, put local settings in a new file such as /etc/unbound/unbound.conf.d/lan.conf; the packaged files in that directory already enable remote control and the DNSSEC root trust anchor. On Fedora and RHEL, add the same lines to the server: section of /etc/unbound/unbound.conf. This example serves the 192.168.1.0/24 network from a router at 192.168.1.1:
server:
interface: 127.0.0.1
interface: 192.168.1.1
access-control: 127.0.0.0/8 allow
access-control: 192.168.1.0/24 allow
hide-identity: yes
hide-version: yes
prefetch: yes
private-domain: "lan"
local-zone: "lan." static
local-data: "nas.lan. IN A 192.168.1.20"
local-data-ptr: "192.168.1.20 nas.lan"
The last four lines give local machines names under .lan without running a separate zone. prefetch: yes refreshes popular names shortly before they expire, at the cost of about 10% more upstream traffic. Check and start it:
sudo unbound-checkconf
sudo systemctl enable --now unbound
sudo systemctl restart unbound # after later changes
dig @192.168.1.1 www.isc.org +dnssec
unbound-checkconf prints no errors in /etc/unbound/unbound.conf when the file parses. In the dig output, the ad flag in the header line means Unbound validated the DNSSEC signatures. Open port 53 to the LAN only: with firewalld, add the service to the zone that holds the LAN interface, not the Internet-facing one (for example sudo firewall-cmd --permanent --zone=internal --add-service=dns then sudo firewall-cmd --reload); with ufw, sudo ufw allow from 192.168.1.0/24 to any port 53.
Hand it out by DHCP. Give clients this resolver’s address in your DHCP options (see the DHCP mini-HOWTO) so you do not have to configure each machine.
An authoritative zone with BIND 9
BIND 9 is ISC’s DNS server. As of November 2026, the 9.20 branch is the current stable and extended-support (ESV) version, and the older 9.18 branch reached end of life in mid-2026. Debian 13 ships 9.20. For a server that only publishes your zones, turn recursion off: an authoritative server should not also be a public resolver.
| Debian / Ubuntu | Fedora / RHEL family | |
|---|---|---|
| Install | sudo apt install bind9 bind9-utils bind9-dnsutils | sudo dnf install bind bind-utils |
| Main config | /etc/bind/named.conf (includes named.conf.options and named.conf.local) | /etc/named.conf |
| Zone files | /var/lib/bind/ (writable by the bind user) | /var/named/ |
| Service | named | named |
On Debian, replace the options block in /etc/bind/named.conf.options with:
options {
directory "/var/cache/bind";
recursion no;
allow-transfer { none; };
listen-on { any; };
listen-on-v6 { any; };
};
Then declare the zone in /etc/bind/named.conf.local. Here 198.51.100.53 is a secondary server that may copy the zone:
zone "example.com" {
type primary;
file "/var/lib/bind/db.example.com";
allow-transfer { 198.51.100.53; };
also-notify { 198.51.100.53; };
};
On Fedora and RHEL, put the zone block in /etc/named.conf with file "example.com.zone"; (relative to /var/named), and change the shipped listen-on, listen-on-v6 and allow-query lines, which limit the server to localhost.
The zone file
$TTL 1h
@ IN SOA ns1.example.com. hostmaster.example.com. (
2026110401 ; serial (YYYYMMDDnn)
12h ; refresh
15m ; retry
4w ; expire
1h ) ; negative-caching TTL
IN NS ns1.example.com.
IN NS ns2.example.net.
IN MX 10 mail.example.com.
IN A 192.0.2.10
ns1 IN A 192.0.2.2
mail IN A 192.0.2.4
www IN A 192.0.2.10
www IN AAAA 2001:db8::10
docs IN CNAME www
@ means the zone name itself. Names without a trailing dot get the zone name added, so www becomes www.example.com.. The second field of the SOA, hostmaster.example.com., is the contact address with the first dot standing for @. A zone needs at least two name servers, ideally on different networks.
Check, load and test
sudo named-checkconf -z
sudo named-checkzone example.com /var/lib/bind/db.example.com
sudo systemctl enable --now named
sudo rndc reload example.com # after each zone edit
dig @192.0.2.2 www.example.com A +norecurse
A good zone check prints:
zone example.com/IN: loaded serial 2026110401
OK
In the dig answer, the aa (authoritative answer) flag shows the reply came from your zone, not from a cache. named-checkconf -z also test-loads every primary zone named in the config.
Chroot is no longer the main defense
The old Chroot-BIND HOWTO jailed named in its own directory tree. Today the packages run named as an unprivileged user under systemd, and on Fedora and RHEL SELinux confines it. Red Hat states that BIND under SELinux in enforcing mode is more secure than a change-root setup; a bind-chroot package with a named-chroot service still exists there if a policy demands it.
DNSSEC in two lines
As a resolver, BIND validates DNSSEC by default with the built-in root trust anchor, and Unbound does so whenever a trust anchor file is configured, as Debian’s package does out of the box. To sign your own zone, BIND’s Key and Signing Policy does the whole job. Add one line to the zone block:
dnssec-policy default;
BIND then creates the keys, signs the zone, keeps a signed copy next to the zone file (with a .signed extension) and rolls signatures over by itself. It needs write access to the zone directory, which is why Debian zones belong in /var/lib/bind/. The last step is manual: give the DS record for your key to your domain registrar, as described in the BIND manual’s DNSSEC chapter.
Debugging with dig and drill
dig comes from bind9-dnsutils (Debian) or bind-utils (Fedora/RHEL). drill, from ldnsutils or ldns-utils, does the same with a smaller code base.
dig www.example.com # ask the servers in /etc/resolv.conf
dig @192.168.1.1 www.example.com +short
dig example.com MX
dig -x 192.0.2.10 # reverse (PTR) lookup
dig www.example.com +trace # follow the delegation from the root
drill -T www.example.com # the same trace with drill
drill -D www.example.com # ask for DNSSEC records
resolvectl query www.example.com # what systemd-resolved returns
Read the header first: status: NOERROR with an empty answer means the name exists but has no record of that type; NXDOMAIN means the name does not exist; SERVFAIL from a validating resolver often means a DNSSEC failure or an unreachable authoritative server. Logs are in journalctl -u named or journalctl -u unbound.
Mistakes to avoid
- Forgetting to raise the SOA serial after an edit. Secondaries only copy the zone when the serial goes up.
- Leaving off the trailing dot on a full name:
mail.example.cominside the zone becomesmail.example.com.example.com.. - Pointing an MX or NS record at a CNAME. They must point to a name with its own A or AAAA record.
- Running an open resolver. Allow recursion only for your own networks, and keep authoritative servers at
recursion no;. - Port 53 already in use. Check with
sudo ss -lunp 'sport = :53'; bind Unbound or BIND to specific addresses rather than fighting other listeners. - Editing
/etc/resolv.confwhen it is managed by systemd-resolved or NetworkManager.
Sources
- BIND 9 versions and support status, ISC
- BIND 9 Administrator Reference Manual: configurations and zone files, ISC
- BIND 9 Administrator Reference Manual: DNSSEC, ISC
- BIND 9 configuration reference (inline-signing), ISC
- named-checkconf(1), Debian 13 manual pages
- named-checkzone(1), Debian 13 manual pages
- bind9 package file list, Debian 13
- Setting up and configuring a BIND DNS server, RHEL 10 documentation
- Unbound configuration guide, NLnet Labs
- unbound.conf(5), NLnet Labs
- unbound package file list, Debian 13
- systemd-resolved.service(8), man7.org
- resolved.conf(5), man7.org
- resolvectl(1), man7.org
- nss-resolve(8), man7.org
- dig(1), Debian 13 manual pages
- drill(1), Debian 13 manual pages