Unbound DNS Resolver Installation and Configuration
Unbound is a high-performance, security-focused recursive DNS resolver that supports DNSSEC validation, DNS-over-TLS forwarding, and fine-grained access control for privacy-conscious deployments. This guide covers installing Unbound on Linux, configuring caching and DNSSEC, setting up DNS-over-TLS, and defining local zones.
Prerequisites
- Ubuntu 22.04/Debian 12 or CentOS/Rocky 9
- Root or sudo access
- Port 53 (TCP/UDP) available — disable
systemd-resolvedstub listener if needed - Basic understanding of DNS resolution
Install Unbound
# Ubuntu/Debian
sudo apt update && sudo apt install -y unbound
# CentOS/Rocky
sudo dnf install -y unbound
# Check version
unbound -V
# Stop the service before configuring
sudo systemctl stop unbound
Disable systemd-resolved stub listener if it occupies port 53:
# Check if something is using port 53
sudo ss -tlnup | grep :53
# Disable systemd-resolved stub
sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved
Basic Configuration
Create the main Unbound configuration:
sudo tee /etc/unbound/unbound.conf << 'EOF'
server:
# Interface to listen on (0.0.0.0 for all interfaces)
interface: 0.0.0.0
port: 53
# Access control - restrict to localhost and local network
access-control: 127.0.0.0/8 allow
access-control: ::1/128 allow
access-control: 10.0.0.0/8 allow
access-control: 172.16.0.0/12 allow
access-control: 192.168.0.0/16 allow
access-control: 0.0.0.0/0 refuse
# Harden against various attacks
harden-glue: yes
harden-dnssec-stripped: yes
harden-large-queries: yes
harden-short-bufsize: yes
harden-algo-downgrade: yes
# Privacy
hide-identity: yes
hide-version: yes
qname-minimisation: yes
# Prefetch popular entries before expiry
prefetch: yes
# EDNS buffer size
edns-buffer-size: 1232
# Number of threads (match CPU cores)
num-threads: 2
# Cache sizes per thread
msg-cache-size: 64m
rrset-cache-size: 128m
# Logging
verbosity: 1
log-queries: no
# Use root hints for recursive resolution
root-hints: "/etc/unbound/root.hints"
# DNSSEC (auto-trust-anchor-file handles key management)
auto-trust-anchor-file: "/var/lib/unbound/root.key"
EOF
# Download root hints file
sudo curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache
sudo systemctl enable --now unbound
# Test resolution
dig @127.0.0.1 google.com
DNSSEC Validation
Unbound validates DNSSEC signatures by default when the trust anchor is configured:
# Initialize the DNSSEC trust anchor (managed automatically)
# Ubuntu/Debian - this is done automatically on install
sudo unbound-anchor -a /var/lib/unbound/root.key
# CentOS/Rocky
sudo -u unbound unbound-anchor -a /var/lib/unbound/root.key
# Verify DNSSEC validation is working
# Should return AD (Authenticated Data) flag for signed domains
dig @127.0.0.1 google.com +dnssec | grep -E "flags:|status:"
# Test with a deliberately bad DNSSEC domain (should return SERVFAIL)
dig @127.0.0.1 dnssec-failed.org | grep status
# Expected: SERVFAIL
# Test with a properly signed domain
dig @127.0.0.1 cloudflare.com +dnssec | grep -E "flags:|AD"
# Expected: flags should include 'ad'
Configure DNSSEC negative trust anchors for broken domains (temporary fix):
# Add to /etc/unbound/unbound.conf server block:
# Skip DNSSEC for a specific broken domain
domain-insecure: "broken-dnssec.example.com"
Caching Optimization
Tune the cache for better performance:
# Add to the server block in unbound.conf
# Cache size (increase for busier resolvers)
msg-cache-size: 128m
rrset-cache-size: 256m
neg-cache-size: 16m
# Serve expired records for up to 1 hour while fetching fresh ones
serve-expired: yes
serve-expired-ttl: 3600
serve-expired-reply-ttl: 0
# Aggressively use DNSSEC for cache filling
aggressive-nsec: yes
# Prefetch expiring records
prefetch: yes
prefetch-key: yes
# Minimum and maximum TTL overrides
cache-min-ttl: 60
cache-max-ttl: 86400
cache-max-negative-ttl: 300
Check cache statistics:
# Enable statistics collection
sudo tee -a /etc/unbound/unbound.conf << 'EOF'
statistics-interval: 0
extended-statistics: yes
EOF
sudo systemctl restart unbound
# View live statistics
sudo unbound-control stats_noreset | grep -E "total|cache|hit|miss"
# Dump the cache
sudo unbound-control dump_cache | head -30
# Flush a specific cached record
sudo unbound-control flush google.com
sudo unbound-control flush_type google.com A
# Flush the entire cache
sudo unbound-control flush_zone .
Access Control
Restrict who can query Unbound:
# In the server block:
# Deny all by default, then allow specific sources
access-control: 0.0.0.0/0 refuse
access-control: ::0/0 refuse
# Allow localhost
access-control: 127.0.0.0/8 allow
access-control: ::1 allow
# Allow internal networks
access-control: 10.10.0.0/16 allow
access-control: 192.168.1.0/24 allow
# Refuse with a proper DNS response (vs. silent drop)
access-control: 203.0.113.0/24 refuse
Enable the remote control interface (for unbound-control):
# Generate control keys
sudo unbound-control-setup
# Add to unbound.conf
sudo tee -a /etc/unbound/unbound.conf << 'EOF'
remote-control:
control-enable: yes
control-interface: 127.0.0.1
control-port: 8953
server-key-file: "/etc/unbound/unbound_server.key"
server-cert-file: "/etc/unbound/unbound_server.pem"
control-key-file: "/etc/unbound/unbound_control.key"
control-cert-file: "/etc/unbound/unbound_control.pem"
EOF
sudo systemctl restart unbound
sudo unbound-control status
DNS-over-TLS Forwarding
Forward all queries over TLS to an encrypted upstream resolver for privacy:
sudo tee -a /etc/unbound/unbound.conf << 'EOF'
forward-zone:
name: "."
# Cloudflare DNS-over-TLS
forward-tls-upstream: yes
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 1.0.0.1@853#cloudflare-dns.com
# Quad9 DNS-over-TLS
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 149.112.112.112@853#dns.quad9.net
EOF
sudo systemctl restart unbound
# Test that queries use TLS
dig @127.0.0.1 example.com
# Verify by watching traffic: sudo tcpdump -i any port 853 -n
Note: When using forward-zone, Unbound acts as a forwarder rather than a full recursive resolver. Remove the root-hints and auto-trust-anchor-file lines from the basic config if forwarding all queries, or keep them for hybrid operation.
Local Zone Configuration
Define custom local DNS overrides for split-horizon or internal services:
sudo tee -a /etc/unbound/unbound.conf << 'EOF'
# Private internal zone
local-zone: "internal.example.com." static
# Add A records for internal hosts
local-data: "web01.internal.example.com. A 10.0.0.10"
local-data: "web02.internal.example.com. A 10.0.0.11"
local-data: "db01.internal.example.com. A 10.0.0.20"
local-data: "db01.internal.example.com. A 10.0.0.21"
# PTR records for reverse lookups
local-data-ptr: "10.0.0.10 web01.internal.example.com"
local-data-ptr: "10.0.0.20 db01.internal.example.com"
# Block a domain (return NXDOMAIN)
local-zone: "malware.example.com." always_nxdomain
# Return a specific IP for ad blocking (nullroute)
local-zone: "ads.tracker.com." redirect
local-data: "ads.tracker.com. A 0.0.0.0"
EOF
sudo systemctl restart unbound
# Test local data
dig @127.0.0.1 web01.internal.example.com
dig @127.0.0.1 -x 10.0.0.10 # reverse lookup
Troubleshooting
Unbound fails to start:
# Check configuration syntax
sudo unbound-checkconf /etc/unbound/unbound.conf
# Check detailed logs
journalctl -u unbound -n 50 --no-pager
# Check if port 53 is in use
sudo ss -tlnup | grep :53
DNSSEC validation failures for legitimate domains:
# Test with verbose DNSSEC output
dig @127.0.0.1 example.com +dnssec +multi
# Check if the domain's DNSSEC is actually broken
delv @8.8.8.8 example.com
# Temporarily bypass DNSSEC for a domain
# Add to server block: domain-insecure: "example.com"
High CPU or memory usage:
sudo unbound-control stats_noreset | grep total
# Reduce cache sizes or increase threads
# Check for query floods: sudo unbound-control dump_requestlist
Slow DNS resolution:
# Test resolution time
dig @127.0.0.1 google.com | grep "Query time"
# Check if prefetch is working
sudo unbound-control stats_noreset | grep prefetch
Conclusion
Unbound provides a secure, privacy-respecting recursive DNS resolver with DNSSEC validation, query minimization, and DNS-over-TLS forwarding built in. Its fine-grained access control and local zone configuration make it suitable both for personal servers and enterprise deployments where internal hostnames need custom resolution. For environments requiring privacy above all, pairing Unbound with DNS-over-TLS forwarding to Cloudflare or Quad9 encrypts all resolver traffic against eavesdropping.


