Most DNS privacy guides stop at pointing your system to a privacy-respecting resolver like Cloudflare’s 1.1.1.1 or Quad9. That is an improvement over your ISP’s resolver, but you are still trusting a third party with your complete DNS query history. A recursive resolver eliminates that trust by doing the DNS resolution itself — starting from the root nameservers and traversing the DNS hierarchy directly to the authoritative nameserver for each domain. Unbound is the production-grade tool for this, trusted by ISPs, enterprises, and privacy-focused users alike.
What a Recursive Resolver Does
When you type hackingpc.com in your browser, a DNS lookup occurs:
- Your resolver asks a root nameserver: “Who handles
.com?” - The root nameserver returns the
.comTLD nameservers. - Your resolver asks a
.comnameserver: “Who handleshackingpc.com?” - The TLD nameserver returns Cloudflare (or wherever the domain is hosted).
- Your resolver asks Cloudflare: “What is the IP for
hackingpc.com?” - Cloudflare returns the IP address.
A forwarding resolver (1.1.1.1, 8.8.8.8) performs this process on your behalf — it handles steps 1–6 and returns the result. A recursive resolver performs steps 1–6 itself. No upstream provider sees your complete query history. The authoritative nameservers for each domain see individual queries, but no single entity aggregates all your lookups.
Installing Unbound on Ubuntu / Debian
sudo apt update
sudo apt install unbound
Stop the service temporarily while we configure it:
sudo systemctl stop unbound
If systemd-resolved is running on port 53, it will conflict with Unbound. Disable its DNS stub listener:
sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved
Downloading Root Hints
Unbound needs the root hints file to know where to start the recursive lookup chain — the IP addresses of the 13 root nameserver clusters.
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
Update this file every 6–12 months. The root nameserver IPs rarely change, but keeping the file current is good practice.
Configuring unbound.conf
The main configuration file is /etc/unbound/unbound.conf. Replace its contents (or create /etc/unbound/unbound.conf.d/privacy.conf) with:
server:
# Network
interface: 127.0.0.1
port: 53
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
# Access control
access-control: 127.0.0.0/8 allow
access-control: 0.0.0.0/0 refuse
# Root hints
root-hints: "/var/lib/unbound/root.hints"
# DNSSEC validation
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Privacy settings
hide-identity: yes
hide-version: yes
qname-minimisation: yes
aggressive-nsec: yes
# Performance
prefetch: yes
prefetch-key: yes
num-threads: 2
cache-min-ttl: 3600
cache-max-ttl: 86400
# Logging (minimal for privacy)
verbosity: 1
log-queries: no
log-replies: no
Key settings explained:
qname-minimisation: yes— Instead of sending the full query (hackingpc.com) to root nameservers, sends only the TLD (.com). This reduces information leakage at each step of the recursive process.aggressive-nsec: yes— Uses DNSSEC NSEC records to answer negative queries locally, reducing round trips.hide-identity/hide-version— Prevents Unbound from revealing its version and hostname to queriers.prefetch: yes— Prefetches popular cached records before they expire, reducing latency.
DNSSEC Validation
Unbound validates DNSSEC signatures by default when auto-trust-anchor-file is set. Initialize the trust anchor:
sudo unbound-anchor -a /var/lib/unbound/root.key
sudo chown unbound:unbound /var/lib/unbound/root.key
DNSSEC prevents DNS spoofing by verifying that DNS responses are signed by the authoritative nameserver’s private key. A tampered response fails validation and is rejected.
Starting Unbound
sudo systemctl enable unbound
sudo systemctl start unbound
Check status:
sudo systemctl status unbound
Look for active (running). If it fails, check logs:
sudo journalctl -u unbound -n 50
Testing with dig
Verify Unbound is responding on localhost:
dig @127.0.0.1 google.com
Expected output includes:
;; SERVER: 127.0.0.1#53(127.0.0.1)
Test DNSSEC validation:
dig @127.0.0.1 sigfail.verteiltesysteme.net
This domain is deliberately signed with an invalid DNSSEC signature. Unbound should return SERVFAIL, confirming validation is active.
Test a valid DNSSEC domain:
dig @127.0.0.1 dnssec-tools.org +dnssec
Look for the ad flag (Authenticated Data) in the response flags line:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2
The ad flag confirms the response was DNSSEC-validated.
Pointing System DNS to Unbound
NetworkManager (most Linux desktops)
nmcli connection modify "Your Connection" ipv4.dns "127.0.0.1" ipv4.ignore-auto-dns yes
nmcli connection up "Your Connection"
Direct resolv.conf edit
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
Prevent NetworkManager from overwriting it:
sudo chattr +i /etc/resolv.conf
Remove the immutable flag when you need to make changes: sudo chattr -i /etc/resolv.conf.
Combining Pi-hole with Unbound
Pi-hole blocks ads and trackers at the DNS level. Combined with Unbound, you get ad-blocking plus recursive resolution:
Client → Pi-hole (port 53) → Unbound (port 5335) → Root nameservers
Configure Unbound to listen on port 5335 instead of 53:
server:
interface: 127.0.0.1
port: 5335
Restart Unbound:
sudo systemctl restart unbound
In Pi-hole’s admin interface (http://pi.hole/admin):
- Go to Settings > DNS.
- Uncheck all upstream DNS providers.
- Under Custom 1 (IPv4), enter
127.0.0.1#5335. - Save.
Pi-hole now forwards all non-blocked queries to Unbound, which resolves them recursively. Blocked domains return Pi-hole’s block page; clean domains are resolved recursively without passing through any third-party DNS provider.
Performance Comparison
| Resolver Type | Latency (cold) | Latency (cached) | Privacy |
|---|---|---|---|
| ISP DNS (plain) | 5–20ms | 2–5ms | None |
| Cloudflare 1.1.1.1 (DoH) | 10–30ms | 1–5ms | Trusts Cloudflare |
| Unbound (recursive) | 50–200ms | 0.1–1ms | No upstream trust |
| Pi-hole + Unbound | 50–200ms | 0.1–1ms | No upstream trust + ad blocking |
The cold query latency for Unbound is higher because it performs the full DNS traversal rather than querying a pre-populated resolver cache. However, prefetch: yes and caching mean subsequent queries for popular domains return in under a millisecond from local cache.
For most users, the latency difference is imperceptible in normal browsing. DNS lookups are a small fraction of total page load time, and the cache quickly populates with frequently visited domains.
Maintenance
Update root hints periodically:
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
sudo systemctl restart unbound
Update the DNSSEC trust anchor if it changes (Unbound handles this automatically via the built-in RFC 5011 rollover mechanism when auto-trust-anchor-file is configured).
Check Unbound’s cache statistics:
sudo unbound-control stats_noreset | grep total
Running your own recursive resolver removes the last significant third-party from your DNS traffic. Combined with Pi-hole’s filtering, it is the most complete local DNS privacy solution available without external service dependencies.