Unbound is a validating, recursive and caching DNS resolver developed by NLnet Labs. Instead of sending every lookup to your provider's or a public resolver, Unbound walks the DNS tree from the root servers itself, validates DNSSEC signatures and caches the answers. In this tutorial you will install Unbound on Ubuntu 24.04, serve the server itself and a private network, verify DNSSEC, add local hostnames and, optionally, forward queries over DNS-over-TLS.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - Optionally, a private network (such as a CubePath private network) with the clients that will use the resolver. This guide uses
10.10.0.0/24as the private range andyour_private_ipas the resolver's address on it; replace both with your own values.
WarningNever allow the whole Internet to query your resolver. An open resolver is abused for DNS amplification attacks within hours. This guide only answers the server itself and your private network.
Step 1 - Installing Unbound
Install Unbound together with bind9-dnsutils, which provides the dig command used for testing:
sudo apt update
sudo apt install unbound bind9-dnsutils
The package starts Unbound right away, listening on 127.0.0.1 only. Check the version and the service state:
unbound -V | head -n 1
systemctl status unbound --no-pager
Version 1.19.2
● unbound.service - Unbound DNS server
Loaded: loaded (/usr/lib/systemd/system/unbound.service; enabled; preset: enabled)
Active: active (running) since ...
Ubuntu's package already ships two useful snippets in /etc/unbound/unbound.conf.d/: root-auto-trust-anchor-file.conf, which enables DNSSEC validation with an automatically updated root key in /var/lib/unbound/root.key, and remote-control.conf, which enables the unbound-control management tool over a local Unix socket. You will keep both and put your own settings in a separate file.
Unbound also has the root server addresses compiled in, so you do not need to download a root.hints file.
Step 2 - Keeping systemd-resolved out of the way
Ubuntu 24.04 runs systemd-resolved, which listens on 127.0.0.53 and 127.0.0.54 port 53. Because Unbound will bind to 127.0.0.1 and your private IP explicitly, the two do not conflict and you do not need to disable systemd-resolved. Confirm what is listening on port 53:
sudo ss -lnup 'sport = :53'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",...))
UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",...))
UNCONN 0 0 127.0.0.1:53 0.0.0.0:* users:(("unbound",...))
Step 3 - Configuring Unbound as a private resolver
Create a configuration file for your resolver:
sudo nano /etc/unbound/unbound.conf.d/resolver.conf
Add the following content, replacing your_private_ip and 10.10.0.0/24:
server:
# Listen on loopback and on the private network only
interface: 127.0.0.1
interface: your_private_ip
port: 53
do-ip6: no
# Who may query. Anything not listed is refused.
access-control: 127.0.0.0/8 allow
access-control: 10.10.0.0/24 allow
# Performance: roughly one thread per CPU core
num-threads: 2
so-reuseport: yes
msg-cache-size: 64m
rrset-cache-size: 128m
# Keep popular records warm and answer from cache if upstream is slow
prefetch: yes
prefetch-key: yes
serve-expired: yes
serve-expired-ttl: 86400
aggressive-nsec: yes
# Privacy and hardening
hide-identity: yes
hide-version: yes
qname-minimisation: yes
harden-glue: yes
harden-dnssec-stripped: yes
edns-buffer-size: 1232
verbosity: 1
A few notes on these settings:
access-controlis what keeps the resolver private. By default Unbound refuses every address that is not listed, except localhost.rrset-cache-sizeshould be about twicemsg-cache-size. Increase both if the resolver serves many clients and has spare RAM.serve-expiredlets Unbound answer with a recently expired record while it refreshes it in the background, which hides slow or failing authoritative servers from your clients.- If you only want a resolver for the server itself, remove the
interface: your_private_ipand10.10.0.0/24lines.
Check the syntax before applying it:
sudo unbound-checkconf
unbound-checkconf: no errors in /etc/unbound/unbound.conf
Restart Unbound to load the configuration:
sudo systemctl restart unbound
Confirm that it now listens on both addresses:
sudo ss -lnup 'sport = :53' | grep unbound
Step 4 - Testing resolution and DNSSEC
Send a query to Unbound on the loopback address:
dig @127.0.0.1 cubepath.com
The first lookup takes longer because Unbound resolves it from the root servers. Run the same command again and the Query time drops to 0 or 1 ms, which means the answer came from the cache:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41022
...
;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
Now verify DNSSEC validation. A correctly signed domain returns the ad (authenticated data) flag:
dig @127.0.0.1 isc.org +dnssec | grep flags:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
A domain with deliberately broken signatures must fail with SERVFAIL:
dig @127.0.0.1 dnssec-failed.org | grep status:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 5310
If dnssec-failed.org resolves normally, validation is not active: check that /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf exists and that /var/lib/unbound/root.key is present.
Step 5 - Opening the firewall for the private network
If you use UFW, allow DNS only from your private range. Never open port 53 to everyone:
sudo ufw allow from 10.10.0.0/24 to any port 53 proto udp
sudo ufw allow from 10.10.0.0/24 to any port 53 proto tcp
sudo ufw status
From a client on the private network, test the resolver:
dig @your_private_ip cubepath.com +short
To make a client use it permanently, set your_private_ip as its nameserver. On an Ubuntu client with netplan, add it under nameservers.addresses for the private interface and run sudo netplan apply.
Step 6 - Using Unbound for the server itself
The server still resolves through systemd-resolved, which uses the DNS servers received from DHCP or netplan. To send its own lookups to Unbound, create a drop-in for systemd-resolved:
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo nano /etc/systemd/resolved.conf.d/unbound.conf
[Resolve]
DNS=127.0.0.1
Domains=~.
Domains=~. makes this server the preferred one for all domains. Restart the service and check the result:
sudo systemctl restart systemd-resolved
resolvectl status | head -n 8
Global
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Current DNS Server: 127.0.0.1
DNS Servers: 127.0.0.1
DNS Domains: ~.
Step 7 - Adding local hostnames
Unbound can answer for internal names that do not exist in public DNS. Create a separate file for them:
sudo nano /etc/unbound/unbound.conf.d/local-zone.conf
server:
local-zone: "internal.example.com." static
local-data: "web01.internal.example.com. IN A 10.10.0.10"
local-data: "db01.internal.example.com. IN A 10.10.0.20"
local-data-ptr: "10.10.0.10 web01.internal.example.com"
local-data-ptr: "10.10.0.20 db01.internal.example.com"
# Block a domain for every client
local-zone: "blocked.example.net." always_nxdomain
The static type answers only with the data you define and returns NXDOMAIN for any other name inside internal.example.com, so internal names never leak to the Internet. Check and reload:
sudo unbound-checkconf && sudo systemctl restart unbound
dig @127.0.0.1 web01.internal.example.com +short
dig @127.0.0.1 -x 10.10.0.10 +short
10.10.0.10
web01.internal.example.com.
Step 8 - Forwarding over DNS-over-TLS (optional)
By default Unbound performs full recursion, which is the most private option because no single upstream sees all your queries. If you prefer to send everything to a trusted public resolver, encrypt that traffic with DNS-over-TLS so your provider cannot read or modify it.
sudo nano /etc/unbound/unbound.conf.d/forward-tls.conf
server:
tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 149.112.112.112@853#dns.quad9.net
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 1.0.0.1@853#cloudflare-dns.com
tls-cert-bundle is required: without it Unbound cannot verify the upstream certificates. The name after # must match the certificate the resolver presents. Apply and test:
sudo unbound-checkconf && sudo systemctl restart unbound
dig @127.0.0.1 example.com +short
To confirm that queries leave the server encrypted, watch port 853 in a second terminal while you run a new dig for a name that is not cached yet:
sudo tcpdump -ni any port 853 -c 10
DNSSEC validation stays active when forwarding, so the dnssec-failed.org test from Step 4 must still return SERVFAIL.
Step 9 - Checking cache statistics
unbound-control shows how the resolver is performing:
sudo unbound-control stats_noreset | grep -E '^total\.num\.(queries|cachehits|cachemiss)='
total.num.queries=1284
total.num.cachehits=1051
total.num.cachemiss=233
Useful commands when you change records upstream and do not want to wait for the TTL:
sudo unbound-control flush www.example.com
sudo unbound-control flush_zone example.com
Troubleshooting
Unbound does not start after a change. Run sudo unbound-checkconf to get the exact file and line, then read the service log with sudo journalctl -u unbound -n 50 --no-pager.
error: can't bind socket: Cannot assign requested address. The address in interface: does not exist on the server yet, usually because the private network interface is not configured. Check it with ip -brief addr.
Clients on the private network get REFUSED. Their source address is not covered by an access-control line. Add the correct range and restart Unbound.
Clients time out. Check the firewall rules from Step 5 and confirm that Unbound listens on your_private_ip with sudo ss -lnup 'sport = :53'.
unbound-control fails to connect. Make sure /etc/unbound/unbound.conf.d/remote-control.conf is still present and that you run the command with sudo.
Conclusion
You now have a private, validating and caching DNS resolver on Ubuntu 24.04 that serves the server and your private network, answers for internal hostnames and can optionally forward over DNS-over-TLS. As next steps, you can put an ad-blocking layer such as Pi-hole or AdGuard Home in front of Unbound, publish encrypted DNS-over-TLS and DNS-over-HTTPS endpoints for remote clients, or build a split-horizon setup with BIND views when internal and external clients need different answers for the same domain.
