Unbound Installation and Configuration Guide

STEP 1: update and upgrade system
sudo apt update && sudo apt upgrade -yy
 
Step 1.5: install vm tools for vmware esxi control (Optional)
sudo apt install open-vm-tools
 
Step 2: Disable systemd-resolved’s DNSStubListener
sudo nano /etc/systemd/resolved.conf
#Find the line #DNSStubListener=yes and change it to:
DNSStubListener=no
#Save the file (Ctrl+O, Enter, Ctrl+X)
#Then restart systemd-resolved:
sudo systemctl restart systemd-resolved
 
STEP 3: Install Unbound & outher apps
sudo apt update && sudo apt install -y unbound dnsutils tcpdump bind9-host ufw ca-certificates
#Package notes:
#* unbound – The validating, recursive, and caching DNS resolver
#* dnsutils – Includes diagnostic tools like dig and nslookup
#* tcpdump – Command-line packet analyzer for troubleshooting network/DNS traffic
#* bind9-host – DNS lookup utility (provides the host command)
#* ufw – Uncomplicated Firewall to secure the server ports
#* ca-certificates – Common CA certificates for secure TLS/HTTPS connections
 
STEP 4: Get DNSSEC Root Key
sudo unbound-anchor -a /var/lib/unbound/root.key
#if it fails run
ls -l /var/lib/unbound/root.key
#if it shows your key then its fine
 
STEP 5: Download latest root hints file from IANA:
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
 
STEP 6: Set Permissions on Root Trust Files
sudo chmod 644 /var/lib/unbound/root.hints
sudo chmod 644 /var/lib/unbound/root.key
sudo chown unbound:unbound /var/lib/unbound/root.hints
sudo chown unbound:unbound /var/lib/unbound/root.key
 
Step 7: automate updating root hints file every month:
sudo crontab -e
@monthly curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
 
STEP 8: Backup and Replace Default Config
sudo mv /etc/unbound/unbound.conf /etc/unbound/unbound.conf.bak
sudo nano /etc/unbound/unbound.conf
#Paste the following configuration (adapted for IP 10.10.10.4):
 
server:
    verbosity: 1                              # Log level (0 = quiet, 1 = operational info, up to 5 for debug)
                                              # 0   Quiet   Logs only critical errors
                                              # 1   Operational info (Default for production; logs key events/warnings)
                                              # 2   More detailed (Adds basic query logging and more diagnostics)
                                              # 3   Query-level info (Shows every query received and how it was handled)
                                              # 4+  Debug mode Very verbose (not suitable for production; mostly for dev)
                                              # check logs with “journalctl -u unbound” OR “tail -f /var/log/syslog”
    
    interface: 10.10.10.4                     # Listen on LAN IP (use this for AdGuard to point to)
    interface: 127.0.0.1                      # Also listen on localhost for local queries
 
    port: 53                                  # Standard DNS port
 
    do-ip4: yes                               # Enable IPv4 DNS queries
    do-ip6: no                                # Disables IPV6 DNS queries
    do-udp: yes                               # Enable DNS over UDP (default for most queries)
    do-tcp: yes                               # Enable DNS over TCP (for larger queries and fallback)
    
    # Access control: allow only local devices to query Unbound
    access-control: 127.0.0.0/8 allow         # Allow localhost
    access-control: 192.168.1.0/24 allow      # Allow your LAN subnet
    access-control: 10.10.0.0/16 allow        # Allow your LAN subnet
    access-control: 0.0.0.0/0 refuse          # Deny all others by default
 
    # Privacy and security hardening
    hide-identity: yes                        # Hide server identity in DNS responses
    hide-version: yes                         # Hide Unbound version in DNS responses
    harden-glue: yes                          # Reject unverified glue records (to prevent poisoning)
    harden-dnssec-stripped: yes               # Reject responses that appear to have DNSSEC removed
    harden-referral-path: yes                 # Verify DNS referral chains for integrity
    use-caps-for-id: yes                      # Use 0x20-bit encoding to harden against DNS spoofing
    qname-minimisation: yes                   # Send minimal query info to upstream servers (privacy)
    aggressive-nsec: yes                      # DNSSEC hardening
    do-not-query-localhost: yes               # Avoid querying local resolvers
    val-permissive-mode: no                   # Enforce strict DNSSEC validation
 
    # DNS rebinding protection (block responses that point back to a local ip address)
    private-address: 192.168.0.0/16           # Block local subnet
    #private-address: 10.0.0.0/8               # Block local subnet
    private-address: 172.16.0.0/12            # Block local subnet
    private-address: 127.0.0.0/8              # Block loopback
    private-address: ::1                      # Block IPv6 loopback
    private-address: fc00::/7                 # Block IPv6 ULA
    private-address: fe80::/10                # Block IPv6 link-local
 
    # Allow private IPs in homelab responses (allows the .homelab extention to be resolved to private ip address)
    domain-insecure: “homelab”
 
    # Rate limiting (anti-DNS abuse)
    ratelimit: 100                            # Max queries per second from clients
    ratelimit-slabs: 4                        # Memory slab count (power of 2)
    ip-ratelimit: 50                          # Per-IP rate limit (queries/sec)
 
    # Caching and performance
    prefetch: yes                             # Preemptively refresh cache when TTL is nearing expiry
    prefetch-key: yes                         # Prefetch DNSSEC keys along with records
    msg-cache-size: 256m                      # Message cache (stores responses)
    rrset-cache-size: 512m                    # RRset cache (stores DNS record sets)
                                              # Unbound recommends keeping rrset-cache-size about 2× larger than msg-cache-size.
                                              # server with 2 GB of RAM msg-cache-size: 64m/rrset-cache-size: 128m
                                              # server with 4–8 GB of RAM msg-cache-size: 256m/rrset-cache-size: 512m
    cache-min-ttl: 86400                      # Minimum time (in seconds) to keep a cached entry (1 day)
    cache-max-ttl: 604800                     # Maximum time (in seconds) to keep a cached entry (7 day)
 
    # Stale DNS serving in case upstream becomes unavailable
    serve-expired: yes                        # Allow serving expired (stale) DNS entries
    serve-expired-ttl: 604800                 # Serve stale entries for up to 7 day
    serve-expired-client-timeout: 1800        # Keep trying to refresh stale data for 30 minutes in background
 
    # Performance tuning
    num-threads: 2                            # Number of threads (adjust based on CPU cores)
so-reuseport: yes                         # Enable socket reuse port for better multi-thread packet distribution
    so-rcvbuf: 4m                             # Socket receive buffer size
    so-sndbuf: 4m                             # Socket send buffer size
    edns-buffer-size: 1232                    # EDNS buffer size for DNS messages (mitigates fragmentation)
    rrset-roundrobin: yes                     # Randomize record order in responses (load balancing benefit)
 
    # Disable unnecessary services
    statistics-interval: 0                    # Disable periodic stats logging
    extended-statistics: no                   # Disable detailed statistics collection
 
    # TLS certificate bundle (for validating DoT upstream)
    # Path to system CA bundle
    tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
 
    # DNSSEC trust anchor configuration
    # Root trust anchor for validating DNSSEC signatures
    auto-trust-anchor-file: “/var/lib/unbound/root.key”
 
    # root hints tell unbound where to find ip address for root server
    root-hints: “/var/lib/unbound/root.hints”
 
remote-control:                           # Disable remote control interface
    control-enable: no
 
# Forward .homelab queries to BIND
forward-zone:
    name: “homelab”                           # Matches all *.homelab queries
    forward-addr: 10.10.10.6                  # IP address of your BIND server
 
# The below is the forwarding section, if you wish unbound to send dns queries straight to root server; comment all of the below out
 
# Forwarding section: send all queries to Quad9 via DNS-over-TLS
forward-zone:
    name: “.”                                 # Forward all queries (root zone)
    forward-tls-upstream: yes                 # Use DNS-over-TLS to encrypt queries to forward-addr
    forward-addr: 9.9.9.9@853                 # Quad9 DoT primary server
    forward-addr: 149.112.112.112@853         # Quad9 DoT secondary server
    forward-addr: 2620:fe::fe@853             # Quad9 IPv6 DoT primary (optional)
    forward-addr: 2620:fe::9@853              # Quad9 IPv6 DoT secondary (optional)
 
# Forwarding section: send all queries to Quad9 via DNS-over-HTTPS
#forward-zone:
#    name: “.”
#    forward-ssl-upstream: yes                 # Use TLS for DoH
#    forward-host: “dns.quad9.net”             # Quad9 DoH server hostname
#    forward-addr: 9.9.9.9                     # Quad9 IPv4 address (used for connecting)
#    forward-addr: 149.112.112.112             # Quad9 secondary IPv4
#    forward-addr: 2620:fe::fe                 # Quad9 IPv6 primary
#    forward-addr: 2620:fe::9                  # Quad9 IPv6 secondary
 
# Alternatively, with Unbound version 1.17.0 and later, you can specify DoH URLs directly:
# unbound -V (to verify unbound version)
#forward-zone:
#    name: “.”
#    forward-doh-url: “https://dns.quad9.net/dns-query”   # Primary DoH URL
#    forward-doh-url: “https://dns9.quad9.net/dns-query”   # secondary DoH URL
 
# Forwarding section: send all queries to DNS.SB via DNS-over-TLS
#forward-zone:
#    name: “.”
#    forward-tls-upstream: yes
#    forward-addr: 185.222.222.222@853#dot.sb       # DNS.SB primary IPv4 DoT
#    forward-addr: 45.11.45.11@853#dot.sb           # DNS.SB secondary IPv4 DoT
#    forward-addr: 2a09::@853#dot.sb                # DNS.SB IPv6 DoT
#    forward-addr: 2a11::@853#dot.sb                # DNS.SB IPv6 DoT
 
# Forwarding section: send all queries to Mullvad via DNS-over-TLS
#forward-zone:
#    name: “.”
#    forward-tls-upstream: yes
#    forward-addr: 194.242.2.4@853#base.dns.mullvad.net     # Mullvad Base IPv4 DoT (Blocks Ads & Trackers)
#    forward-addr: 2a07:e340::4@853#base.dns.mullvad.net  # Mullvad Base IPv6 DoT (Blocks Ads & Trackers)
 
# Forwarding section: send all queries to UncensoredDNS via DNS-over-TLS
#forward-zone:
#    name: “.”
#    forward-tls-upstream: yes
#    forward-addr: 91.239.100.100@853       # UncensoredDNS primary IPv4 DoT
#    forward-addr: 89.233.43.71@853         # UncensoredDNS secondary IPv4 DoT
#    forward-addr: 2a01:3a0:53:53::1@853    # UncensoredDNS IPv6 DoT
#    forward-addr: 2a01:3a0:53:53::2@853    # UncensoredDNS IPv6 DoT
 
 
#Save with Ctrl+O, then Enter, then Ctrl+X to exit.
 
STEP 9: Run a quick config test:
sudo unbound-checkconf
 
STEP 10: Enable and Start Unbound
sudo systemctl enable unbound
sudo systemctl restart unbound
 
Step 11: UFW Rules
# Set default policies (deny all incoming, allow all outgoing)
# IMPORTANT: Do this FIRST to ensure security by default
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (so you don’t lock yourself out)
# This rule is crucial and should be added BEFORE enabling UFW
sudo ufw allow ssh
# Allow DNS (Unbound) from your local network (LAN)
# This allows devices on your 192.168.1.x network to query Unbound
sudo ufw allow from 192.168.1.0/24 to any port 53 proto udp
sudo ufw allow from 192.168.1.0/24 to any port 53 proto tcp
sudo ufw allow from 10.10.0.0/16 to any port 53 proto udp
sudo ufw allow from 10.10.0.0/16 to any port 53 proto tcp
# Allow localhost access for Unbound (for the server itself and AdGuard Home if on the same machine)
sudo ufw allow from 127.0.0.1 to any port 53 proto udp
sudo ufw allow from 127.0.0.1 to any port 53 proto tcp
# Enable UFW (if not already enabled)
# This should be the last step after all allow rules are in place
sudo ufw enable
# Reload UFW (only needed if UFW was already enabled and you modified rules)
# If you just enabled it with the above command, this isn’t strictly necessary immediately after.
# But it’s good to know for future rule changes.
sudo ufw reload
#To check:
sudo systemctl status unbound
 
STEP 12: Test Unbound
#Try resolving a domain using Unbound:
dig @10.10.10.4 example.com
#You should get a valid response.
dig @10.10.10.4 example.com +stats
#test caching and performance
dig @10.10.10.4 test1.homelab
#test bind .homelab resolution
 
#To test DNSSEC validation:
dig +dnssec @10.10.10.4 dnssec-failed.org
#If DNSSEC is working, you should get no answer (because that domain is intentionally broken).