How to Set Up Unbound as a Recursive DNS Resolver
Install Unbound, configure root hints and DNSSEC validation, and point your LAN at a private recursive DNS resolver — no upstream forwarder required.
Before you start
- ▸A Linux machine with a static IP address on your LAN
- ▸Root or sudo access
- ▸Outbound UDP and TCP access to port 53 on the internet (for recursion to root servers)
- ▸Basic familiarity with editing config files and using systemctl
Unbound is a validating, recursive, caching DNS resolver developed by NLnet Labs. Running your own recursive resolver gives you full DNS privacy (no upstream forwarder sees your queries), DNSSEC validation at the edge of your network, and a local cache that speeds up repeated lookups. This guide walks through installing Unbound, wiring in a fresh root hints file, enabling DNSSEC, and pointing clients at the new resolver.
Install Unbound
Debian / Ubuntu
sudo apt update
sudo apt install unbound -y
Fedora / RHEL 9 / Rocky Linux 9
sudo dnf install unbound -y
Arch Linux
sudo pacman -S unbound
On RHEL-family systems, systemd-resolved and NetworkManager may already occupy port 53. Check before starting Unbound:
sudo ss -tulpn | grep ':53'
If systemd-resolved appears, disable its stub listener by editing /etc/systemd/resolved.conf, setting DNSStubListener=no, then restarting it. Alternatively, bind Unbound to a specific interface IP rather than 0.0.0.0 and configure NetworkManager to point there.
Fetch Current Root Hints
Unbound ships with a compiled-in root hints list, but it goes stale. Pull a fresh copy from IANA and store it where Unbound expects it:
sudo curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.root
Add a systemd timer or monthly cron job to keep this file current. The root server addresses rarely change, but it is good practice to refresh it a few times per year.
Configure Unbound
Drop your main configuration into /etc/unbound/unbound.conf.d/local.conf (Debian/Ubuntu and Fedora both include a conf.d drop-in directory). On Arch, edit /etc/unbound/unbound.conf directly or create a drop-in using the include: directive.
sudo nano /etc/unbound/unbound.conf.d/local.conf
A solid starting configuration for a home or small-office resolver:
server:
# Listen on all interfaces; restrict with interface: 192.168.1.1 if preferred
interface: 0.0.0.0
port: 53
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
# Allow queries only from trusted sources
access-control: 127.0.0.0/8 allow
access-control: 192.168.0.0/16 allow
access-control: 10.0.0.0/8 allow
access-control: 0.0.0.0/0 refuse
# Root hints file
root-hints: "/etc/unbound/root.hints"
# Logging (set to 0 in production to reduce I/O)
verbosity: 1
# Harden against common attacks
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
use-caps-for-id: yes
# Privacy
hide-identity: yes
hide-version: yes
qname-minimisation: yes
# Performance
prefetch: yes
rrset-roundrobin: yes
num-threads: 2
msg-cache-slabs: 4
rrset-cache-slabs: 4
infra-cache-slabs: 4
key-cache-slabs: 4
rrset-cache-size: 256m
msg-cache-size: 128m
so-rcvbuf: 1m
# DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
Adjust num-threads to match your CPU core count. The cache sizes above are appropriate for a machine with 1 GB or more of RAM; scale down on constrained hardware.
Initialize DNSSEC Trust Anchor
Unbound ships a helper tool, unbound-anchor, that fetches and verifies the root DNSSEC trust anchor using the built-in ICANN certificate. Run it once before the first service start:
sudo unbound-anchor -a /var/lib/unbound/root.key
Exit code 0 means the anchor was already valid; exit code 1 means it was fetched or updated — both outcomes are fine. The file at /var/lib/unbound/root.key is managed automatically by Unbound at runtime via RFC 5011 key rollover, so you do not need to run this command again.
On Debian/Ubuntu the package creates this file automatically during install; double-check it exists before continuing:
ls -lh /var/lib/unbound/root.key
Validate the Configuration
Always check the config file syntax before restarting the daemon:
sudo unbound-checkconf /etc/unbound/unbound.conf.d/local.conf
Expected output: unbound-checkconf: no errors in /etc/unbound/unbound.conf.d/local.conf. Fix any reported errors before proceeding.
Enable and Start Unbound
sudo systemctl enable --now unbound
Confirm it is running and listening:
sudo systemctl status unbound
sudo ss -tulpn | grep unbound
Open the Firewall
If this resolver will serve machines other than localhost, allow DNS through the firewall.
firewalld (Fedora / RHEL / Rocky)
sudo firewall-cmd --add-service=dns --permanent
sudo firewall-cmd --reload
ufw (Ubuntu / Debian)
sudo ufw allow 53/tcp
sudo ufw allow 53/udp
nftables (Arch or manual setup)
sudo nft add rule inet filter input udp dport 53 accept
sudo nft add rule inet filter input tcp dport 53 accept
Persist nftables rules by saving them to /etc/nftables.conf and ensuring nftables.service is enabled.
Point Clients at Your Resolver
Single machine (resolv.conf)
# Replace 192.168.1.1 with your resolver's IP
echo 'nameserver 192.168.1.1' | sudo tee /etc/resolv.conf
On systems using systemd-resolved or NetworkManager, /etc/resolv.conf is managed automatically. Set the DNS server in the NetworkManager connection instead:
nmcli con mod "Wired connection 1" ipv4.dns "192.168.1.1"
nmcli con up "Wired connection 1"
Entire LAN via DHCP server
In your DHCP server config (dnsmasq, ISC DHCP, or your router's admin UI), set the DNS option to the IP address of the machine running Unbound. Clients will pick it up on their next lease renewal.
Verify Resolution and DNSSEC
Test basic resolution and confirm DNSSEC validation is working:
# Basic lookup
dig @127.0.0.1 example.com A
# DNSSEC — look for 'ad' flag in the flags line
dig @127.0.0.1 sigok.verteiltesysteme.net A +dnssec
# This deliberately broken domain must return SERVFAIL if DNSSEC is active
dig @127.0.0.1 sigfail.verteiltesysteme.net A
A successful DNSSEC setup returns the ad (Authenticated Data) flag on valid signed responses, and SERVFAIL on deliberately broken ones. If sigfail resolves successfully, DNSSEC validation is not active — recheck your trust anchor path and auto-trust-anchor-file setting.
Check the local cache statistics:
sudo unbound-control stats_noreset | grep total
Troubleshooting
- Port 53 already in use: Run
ss -tulpn | grep ':53'to identify the process. Common culprits aresystemd-resolved(disable stub listener as above) anddnsmasqstarted by NetworkManager. Stop or reconfigure the conflicting service. - SERVFAIL on all queries: The resolver cannot reach root servers. Confirm outbound UDP/TCP 53 is allowed on the host firewall and that the machine has a working default route. Temporarily set
verbosity: 3and checkjournalctl -u unbound -ffor timeout messages. - root.key permission errors: Unbound drops privileges to the
unbounduser after startup. Ensure/var/lib/unbound/root.keyis owned by that user:sudo chown unbound:unbound /var/lib/unbound/root.key. - Slow first queries: Expected. Unbound must recurse from the root for every uncached name. After the cache warms up, repeat queries resolve from memory in under a millisecond. Enable
prefetch: yes(already in the config above) to keep popular records warm. - Clients not using the new resolver: On machines with
systemd-resolved, checkresolvectl statusto see the active DNS server per interface. NetworkManager may override/etc/resolv.conf; configure DNS throughnmclias shown above.
Frequently asked questions
- Do I need to forward queries to another DNS server, or does Unbound resolve everything itself?
- Unbound is a full recursive resolver — it starts at the root servers and follows referrals all the way to the authoritative nameserver for each domain. No upstream forwarder is needed or configured in this guide. If you want to forward to a public resolver instead, you can add a forward-zone block, but you lose the privacy benefit of full recursion.
- Will Unbound conflict with systemd-resolved?
- Yes, both try to bind port 53 by default. The simplest fix is to disable systemd-resolved's stub listener by setting DNSStubListener=no in /etc/systemd/resolved.conf and restarting it. Alternatively, bind Unbound to a specific non-loopback IP and leave resolved on 127.0.0.53.
- How do I add local DNS entries so Unbound resolves my internal hostnames?
- Add local-data and local-data-ptr directives inside the server block in your config file, for example: local-data: "nas.home. IN A 192.168.1.50" and local-data-ptr: "192.168.1.50 nas.home". Reload Unbound with systemctl reload unbound after changes.
- How often should I update the root hints file?
- Two to four times a year is sufficient. The root server addresses are very stable, but refreshing keeps you current if any change. A simple systemd timer or monthly cron job running curl to overwrite /etc/unbound/root.hints followed by systemctl reload unbound is all you need.
- Can I run Unbound on the same machine as a web server or other services on port 53?
- Only one process can own port 53 on a given IP address at a time. If you need to share the machine, bind Unbound to specific interface IPs rather than 0.0.0.0 using the interface: directive, leaving other IPs free for other services.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.