📟 Retro Mode

Securing Unbound DNS: Caching, Authoritative Zones, and Hardening Strategies

Unbound is a validating, recursive, and caching DNS resolver designed with modularity, high performance, and security at its core. Unlike monolithic BIND setups, Unbound separates recursive resolution from authoritative hosting, making it an ideal front-line resolver for self-hosted home labs and enterprise environments alike.

By hosting your own DNS resolver, you gain granular control over your network traffic, eliminate ISP DNS logging, enforce DNSSEC validation, and significantly speed up lookups through local caching.


1. Installation and Initial Setup

On Debian or Ubuntu-based distributions, install Unbound via the official repositories:

sudo apt update
sudo apt install unbound unbound-host dnsutils

The systemd-resolved Conflict

Modern Linux distributions often use systemd-resolved, which binds to port 53 locally. If you try to start Unbound immediately, it will likely fail. You must disable the systemd-resolved stub listener first:

  1. Edit /etc/systemd/resolved.conf and set DNSStubListener=no.
  2. Restart systemd-resolved: sudo systemctl restart systemd-resolved.
  3. Update your resolv.conf to point to localhost:
    sudo rm /etc/resolv.conf
    sudo ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf
    
  4. Start Unbound: sudo systemctl enable --now unbound.

2. Core Architectural Modes & Configuration Examples

Rather than throwing everything into one file, it is best practice to place the main configuration in /etc/unbound/unbound.conf and modular configurations in /etc/unbound/unbound.conf.d/.

Below is the Base Hardened Configuration (/etc/unbound/unbound.conf), followed by specific use-case examples you can append or include.

A. The Hardened Base Configuration

This configuration locks down access, randomizes queries, and sets up high-performance caching.

server:
    # Verbosity level (0 = only errors, 1 = operational info)
    verbosity: 1

    # Interface binding (Listen on localhost and specific LAN interfaces)
    interface: 127.0.0.1
    interface: 10.0.0.1      # Replace with your internal server IP
    
    # IPv4/IPv6 preferences
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no               # Set to 'yes' if your network is dual-stack
    prefer-ip4: yes

    # Access control: Permit trusted local subnets, refuse public recursion
    access-control: 127.0.0.0/8 allow
    access-control: 10.0.0.0/8 allow     # Adjust to match your VLAN/LAN segments
    access-control: 0.0.0.0/0 refuse

    # Privacy & Fingerprint hardening
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    
    # 0x20 bit randomization to foil spoofing attacks
    use-caps-for-id: yes      

    # Performance & Caching tuning
    num-threads: 2
    msg-cache-size: 64m
    rrset-cache-size: 128m
    infra-cache-numhosts: 10000
    cache-min-ttl: 3600      # Force caching for at least 1 hour to speed up lookups

    # DNSSEC Trust Anchor
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

B. Example 1: DNS-over-TLS (DoT) Forwarder

Instead of querying root servers directly (which happens in plain text), you can configure Unbound to encrypt all outbound DNS queries and forward them to a trusted upstream provider like Quad9 or Cloudflare.

Add this block to the bottom of your configuration:

forward-zone:
    name: "."
    forward-tls-upstream: yes
    
    # Quad9 (Primary and Secondary)
    forward-addr: 9.9.9.9@853#dns.quad9.net
    forward-addr: 149.112.112.112@853#dns.quad9.net
    
    # Cloudflare (Fallback)
    forward-addr: 1.1.1.1@853#cloudflare-dns.com

C. Example 2: Local DNS Records (Home Lab / Split-Horizon)

Unbound can act as an authoritative server for your internal network, allowing you to use custom domain names (like .home.arpa) without exposing them to the internet.

server:
    # Tell Unbound this zone is strictly local
    private-domain: "home.arpa"
    
    # Define the local zone
    local-zone: "home.arpa." static
    
    # Map internal hostnames to IP addresses
    local-data: "router.home.arpa. IN A 10.0.0.1"
    local-data: "proxmox.home.arpa. IN A 10.0.0.20"
    local-data: "nas.home.arpa. IN A 10.0.0.30"
    
    # Reverse DNS lookups (PTR records) for your local IPs
    local-data-ptr: "10.0.0.1 router.home.arpa"
    local-data-ptr: "10.0.0.20 proxmox.home.arpa"

D. Example 3: DNS Sinkhole (Ad and Malware Blocking)

You can use Unbound to block known malicious domains or telemetry servers by routing them to a dead end (like 0.0.0.0), similar to how Pi-hole operates.

server:
    # Block specific domains
    local-zone: "telemetry.microsoft.com" always_nxdomain
    local-zone: "doubleclick.net" always_nxdomain
    
    # Or route them to a blackhole IP
    local-zone: "ads.example.com" redirect
    local-data: "ads.example.com A 0.0.0.0"

3. Key Security Best Practices

  • Enforce Strict Access Control: Never configure Unbound to listen on 0.0.0.0 or a public IP without explicit access-control restrictions. An open recursive resolver can easily be weaponized by attackers in DNS amplification DDoS attacks.
  • Enable DNSSEC Validation: Keep auto-trust-anchor-file active. This ensures cryptographic verification of DNS responses, preventing cache poisoning and man-in-the-middle record tampering. If a domain’s DNSSEC signature is invalid, Unbound will protect your network by dropping the query.
  • Minimize Information Disclosure: Setting hide-identity: yes and hide-version: yes prevents attackers from querying version.server or id.server via TXT records to fingerprint your software stack and look for known vulnerabilities.
  • Rate Limiting: Protect your resolver from being overwhelmed by local rogue devices by implementing rate limiting:
    server:
        ratelimit: 1000       # Max queries per second per zone
        ip-ratelimit: 200     # Max queries per second per IP
    

4. Troubleshooting and Diagnostics

DNS issues can cause network-wide outages. Here is how to diagnose common Unbound problems.

Always Check Syntax First

Before restarting the Unbound service, always check your configuration file for syntax errors:

unbound-checkconf /etc/unbound/unbound.conf

If it returns no output, your configuration is valid.

Everything returns SERVFAIL

The Problem: Clients can connect to Unbound, but every query returns a SERVFAIL status. The Fix:

  1. Time Synchronization: DNSSEC validation relies heavily on accurate time. If your server’s clock is significantly skewed, all DNSSEC checks will fail. Ensure NTP is running: timedatectl status.
  2. Missing Trust Anchor: The root.key file might be missing or corrupt. Run sudo unbound-anchor -a /var/lib/unbound/root.key to fetch a fresh key.
  3. Upstream Connectivity: If you are forwarding queries, ensure port 53 (or 853 for DoT) is not blocked by your ISP or firewall.

Unbound fails to start: Address already in use

The Problem: Another service is listening on port 53. The Fix: Run sudo ss -tulpn | grep :53 to find the culprit. It is almost always systemd-resolved (see Section 1 for the fix) or dnsmasq running on the same machine.

Testing Queries Locally

Use dig to verify Unbound is resolving correctly and caching responses.

  1. Query your local Unbound server:
    dig @127.0.0.1 example.com
    
  2. Check the Query Time: The first query might take 30-100ms. Run the same command a second time. If caching is working, the query time should drop to 0 msec.
  3. Verify DNSSEC:
    dig @127.0.0.1 sigok.verteiltesysteme.net +dnssec
    
    Look for the ad (Authentic Data) flag in the header of the output. If it is present, DNSSEC validation is working correctly.

Permission Errors (AppArmor/SELinux)

If you moved your log files or trust anchor files to a non-standard directory and Unbound fails to start with “Permission denied”, check your mandatory access control logs.

  • Ubuntu/Debian (AppArmor): Check /var/log/syslog. You may need to edit /etc/apparmor.d/usr.sbin.unbound to whitelist your custom directories.
  • RHEL/Fedora (SELinux): Check /var/log/audit/audit.log.