Introduction
Unbound:
- Recursive resolver (queries root servers directly)
- Native DNSSEC validation
- Outbound DNS-over-TLS (DoT) support
- Performant cache, very low memory footprint
- Written in C, audited, security-focused (NLnet Labs)
- Multi-thread, configurable up to 100k qps
Use Unbound when:
- You want a local resolver with no external dependency (privacy)
- You manage enterprise/datacenter LAN
- You want to block ads/malware (with lists)
- You want encrypted DNS to upstream
Prerequisites
- Linux VPS Debian / Ubuntu
- Root access
- Port 53 UDP/TCP free
Step 1: Installation
sudo apt update
sudo apt install -y unbound
unbound -V
sudo systemctl enable --now unbound
Step 2: Minimal config (LAN resolver)
sudo nano /etc/unbound/unbound.conf.d/lan.conf
server:
verbosity: 1
interface: 0.0.0.0
port: 53
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: 172.16.0.0/12 allow
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes
cache-min-ttl: 300
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
num-threads: 2
msg-cache-size: 64m
rrset-cache-size: 128m
Step 3: Download root hints
sudo wget https://www.internic.net/domain/named.root -O /var/lib/unbound/root.hints
server:
root-hints: "/var/lib/unbound/root.hints"
For DNSSEC:
sudo unbound-anchor -a /var/lib/unbound/root.key
server:
auto-trust-anchor-file: "/var/lib/unbound/root.key"
Step 4: Test
sudo unbound-checkconf
sudo systemctl restart unbound
dig @127.0.0.1 google.com
dig @127.0.0.1 example.com
# DNSSEC test
dig @127.0.0.1 +dnssec dnssec-failed.org # should SERVFAIL
dig @127.0.0.1 +dnssec cloudflare.com # should have `ad` flag
Step 5: Outbound DNS-over-TLS (DoT)
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
⚠️ With this config, Unbound is no longer recursive (forwards). Trade-off: less granular privacy, but upstream traffic encrypted.
sudo systemctl restart unbound
Step 6: Hybrid recursive + DoT
Recursive by default, forward sensitive zones:
forward-zone:
name: "google.com"
forward-tls-upstream: yes
forward-addr: 1.1.1.1@853#cloudflare-dns.com
Step 7: Ad / malware blocking
sudo wget https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts -O /tmp/hosts
sudo grep "^0.0.0.0" /tmp/hosts | awk '{print "local-zone: \""$2"\" always_nxdomain"}' > /etc/unbound/unbound.conf.d/blocklist.conf
sudo systemctl restart unbound
Test:
dig @127.0.0.1 doubleclick.net
Should return NXDOMAIN.
Automate refresh:
sudo nano /usr/local/bin/update-blocklist.sh
#!/bin/bash
wget -q https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts -O /tmp/hosts
grep "^0.0.0.0" /tmp/hosts | awk '{print "local-zone: \""$2"\" always_nxdomain"}' > /etc/unbound/unbound.conf.d/blocklist.conf
systemctl reload unbound
sudo chmod +x /usr/local/bin/update-blocklist.sh
echo "0 4 * * 0 root /usr/local/bin/update-blocklist.sh" | sudo tee -a /etc/crontab
Step 8: Local zones (internal resolution)
server:
local-zone: "example.lan." static
local-data: "server1.example.lan. IN A 192.168.50.10"
local-data: "server2.example.lan. IN A 192.168.50.11"
local-data-ptr: "192.168.50.10 server1.example.lan"
local-data-ptr: "192.168.50.11 server2.example.lan"
Step 9: Performance tuning
For high loads:
server:
num-threads: 4
msg-cache-slabs: 8
rrset-cache-slabs: 8
infra-cache-slabs: 8
key-cache-slabs: 8
msg-cache-size: 256m
rrset-cache-size: 512m
so-rcvbuf: 4m
so-sndbuf: 4m
so-reuseport: yes
outgoing-range: 8192
num-queries-per-thread: 4096
Step 10: Monitoring
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-use-cert: no
sudo unbound-control stats
For Prometheus, use unbound_exporter.
Step 11: Outbound DNS over HTTPS (DoH)
Unbound 1.20+ supports outbound DoH via doq. For legacy DoH, use dnscrypt-proxy in front:
client → unbound (53) → dnscrypt-proxy (5353) → DoH upstream
Step 12: Anti-DDoS and rate limiting
server:
ratelimit: 1000 # max qps per source IP
ip-ratelimit: 500
edns-buffer-size: 1232 # avoid UDP amplification
do-not-query-localhost: yes
Troubleshooting
"SERVFAIL" on all domains
sudo unbound-checkconf
sudo unbound -dd
Slow resolution
Cache cold. Check:
sudo unbound-control stats | grep cachehits
Will be faster after a few min.
DNSSEC fails everywhere
sudo unbound-anchor -a /var/lib/unbound/root.key -v
Logs too verbose
server:
verbosity: 0 # 0=silent, 1=normal, 4=detailed
log-queries: no
log-replies: no
log-tag-queryreply: no
High memory consumption
Reduce caches:
server:
msg-cache-size: 32m
rrset-cache-size: 64m
Useful commands
sudo systemctl status unbound
sudo systemctl reload unbound
sudo systemctl restart unbound
sudo unbound-checkconf
sudo unbound-control stats
sudo unbound-control reset_cache
sudo unbound-control flush google.com
sudo journalctl -u unbound -f
sudo unbound -dd
dig @127.0.0.1 google.com
dig @127.0.0.1 +dnssec cloudflare.com
dig @127.0.0.1 +trace example.com
sudo unbound-anchor
Conclusion
Unbound gives you:
- Private, recursive, DNSSEC-validated resolution
- Outbound DoT for encryption
- Ad/malware blocking
- Performance and scalability
Going further:
- Combine with dnscrypt-proxy for DoH
- Use Pi-hole + Unbound (Pi-hole in front, Unbound behind)
- For HA, deploy 2-3 Unbound + Keepalived
Resources
- Official docs: https://unbound.docs.nlnetlabs.nl
- NLnet Labs: https://nlnetlabs.nl
- Best practices: https://nlnetlabs.nl/documentation/unbound/howto-optimise/
- GitHub: https://github.com/NLnetLabs/unbound

















