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-resolved stub 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.