PublicDNS.info Live-tested public DNS
Retested every 72 hours.

How to Set Up Unbound as a Recursive DNS Resolver on Linux

Install and configure Unbound as a privacy-focused recursive DNS resolver on Linux. Covers DNSSEC validation, Pi-hole integration, DNS-over-TLS, performance tuning, and monitoring.

Unbound Recursive Resolution Flow

Client (Stub Resolver) 1 Unbound (Recursive Resolver) DNSSEC 2 Root Server ( . ) 3 TLD Server (.com, .net) Auth (Zone) 5 Unbound (caches answer) 6 Client (Gets Answer) Pi-hole Integration (optional) Client Pi-hole (Ad Filtering) Unbound 127.0.0.1:5335 Steps: 1 Query sent 2 Ask root servers 3 Referred to TLD 4 Referred to authoritative 5 Answer returned to Unbound 6 Cached answer to client Query Response Pi-hole

What is Unbound DNS?

Unbound is a validating, recursive, and caching DNS resolver developed by NLnet Labs in the Netherlands. It was released in 2007 as a purpose-built alternative to using BIND for recursive resolution. Where BIND tries to be everything (authoritative server, recursive resolver, zone signer, dynamic update handler), Unbound does one thing: recursive DNS resolution with DNSSEC validation. It does that one thing well.

Unbound is the default resolver in FreeBSD and OPNsense, and it ships in the repositories of every major Linux distribution. It is the most commonly recommended resolver for Pi-hole setups when users want to stop forwarding queries to Google or Cloudflare and instead resolve everything themselves.

The codebase is written in C with privilege separation and chroot support, and is significantly smaller than BIND. It parses a single configuration file, starts fast, and uses minimal memory for a home or small-office deployment.

This guide covers installing Unbound on Linux, configuring it for full recursion, enabling DNSSEC, integrating it with Pi-hole, setting up DNS-over-TLS forwarding, and tuning it for production workloads. Every command is copy-paste ready for Debian/Ubuntu and RHEL/AlmaLinux.

If you just need to point a Linux machine at a different DNS resolver rather than running your own, see our Linux DNS settings guide. If you need authoritative zone hosting, look at our BIND setup guide instead.

How recursive DNS resolution works

When your browser needs to find the IP address for www.example.com, it asks the operating system's stub resolver, which forwards the query to whatever DNS server is configured (typically your router, your ISP, or a public resolver like 1.1.1.1).

A recursive resolver like Unbound takes that query and does the legwork:

  1. It checks its local cache. If the answer is already stored and the TTL has not expired, it returns the cached result immediately.
  2. If there is no cache hit, it queries a root server (one of 13 anycast clusters). The root server does not know the IP for www.example.com, but it knows who handles .com and returns a referral to the TLD servers.
  3. Unbound queries the .com TLD server, which returns a referral to the authoritative nameservers for example.com.
  4. Unbound queries the authoritative server for example.com, which returns the actual A record (e.g., 93.184.216.34).
  5. Unbound caches the answer (and all the intermediate referrals) and returns the result to the client.

The next time any client asks for www.example.com, Unbound answers from cache in under a millisecond. That is the point of running your own resolver: after the first lookup, everything is local and fast.

With DNSSEC enabled, Unbound also verifies cryptographic signatures at each step. If a response has been tampered with or the signatures do not chain back to the root trust anchor, Unbound returns SERVFAIL instead of a poisoned answer.

Unbound architecture overview

Unbound is built around a modular pipeline. Each query passes through a series of modules:

ModuleRole
IteratorHandles the actual recursive resolution logic. It walks the DNS tree from root to authoritative, following referrals and handling CNAMEs.
ValidatorPerforms DNSSEC validation. It checks signatures (RRSIG records), builds the chain of trust from the root, and rejects forged or expired signatures.
CacheStores resolved records in memory. Unbound maintains separate caches for messages (full responses) and RRsets (individual record sets).
RespipOptional module for response IP-based access control (filtering responses by the IP addresses they contain).

Unbound also supports multi-threading. Each thread gets its own set of listening sockets and its own portion of the cache. This allows Unbound to scale across CPU cores without lock contention on a single shared cache.

The binary itself is unbound. Unlike BIND, there is no separate control daemon. The unbound-control utility talks to the running Unbound process over a local socket (or TLS if configured for remote access) to issue commands like cache flushes, statistics dumps, and config reloads.

Why choose Unbound over forwarding resolvers

Most home routers and default OS configurations forward DNS queries to an upstream provider: your ISP, Google (8.8.8.8), Cloudflare (1.1.1.1), or Quad9 (9.9.9.9). This works, but it means that single provider sees every domain you look up.

Privacy

With full recursion, no single server sees the complete picture. The root server sees you asking about .com but not www.example.com. The .com TLD server sees you asking about example.com but not the specific subdomain. The authoritative server sees the full query but does not know who you are (it sees the IP of your resolver, not your client). Your query is split across multiple parties instead of concentrated at one.

Reliability

When Cloudflare or Google has an outage, millions of users lose DNS resolution at the same time. When you run Unbound, your resolver depends on the distributed DNS infrastructure (root servers, TLD servers, authoritative servers) rather than a single provider. If one root server is down, Unbound tries another.

Control

Running your own resolver means you control what gets cached, what gets blocked, and what policies apply. You can set custom TTLs, override specific domains, and inspect your own cache. No third party is injecting NXDOMAIN hijacking, filtering results, or logging your queries for advertising purposes.

When forwarding makes more sense

Forwarding is appropriate when your network has strict egress firewall rules that block outbound DNS to anything except approved resolvers, or when you want the benefit of a large shared cache (public resolvers like Cloudflare serve billions of queries and have nearly everything cached). Unbound supports both modes, so you can switch between them by changing a few lines in the config.

Unbound hardware and OS requirements

Unbound is lightweight. The hardware requirements depend on how many clients you serve and how large you want your cache.

DeploymentCPURAMDisk
Home / single user1 core (any)256 MB total50 MB
Small office (10-50 users)1 core512 MB total100 MB
Large network (500+ users)2-4 cores2-4 GB total500 MB
Raspberry Pi (Pi-hole + Unbound)ARM (any Pi)512 MB minimumSD card

Unbound runs on any Linux distribution that packages it. This guide covers Debian 12, Ubuntu 24.04, RHEL 9, and AlmaLinux 9. It also runs on FreeBSD (where it is the default resolver), OpenBSD, and macOS (via Homebrew). OPNsense and pfSense both ship Unbound as their built-in DNS resolver, so the configuration principles here apply there too, though the web GUI handles the config file for you.

Installing Unbound on Debian/Ubuntu

On Debian 12 (Bookworm) and Ubuntu 24.04 (Noble), Unbound is in the default repositories.

sudo apt update
sudo apt install -y unbound dns-root-data

This installs:

  • unbound — the resolver daemon, unbound-control, unbound-anchor, unbound-checkconf
  • dns-root-data — DNSSEC root trust anchor and root hints, kept up to date by the package manager

After installation, Unbound starts automatically and listens on 127.0.0.1:53. Verify it is running:

sudo systemctl status unbound

You should see active (running). Enable it to start at boot (usually already enabled by the package):

sudo systemctl enable unbound

Check the installed version:

unbound -V

Debian 12 ships Unbound 1.17.x. Ubuntu 24.04 ships 1.19.x or later. Both versions support everything in this guide.

Conflict with systemd-resolved

On Ubuntu, systemd-resolved listens on 127.0.0.53:53 by default. This does not conflict with Unbound on 127.0.0.1:53, but if you want Unbound to be the sole resolver, disable systemd-resolved:

sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf

On Debian 12, systemd-resolved is not installed by default, so this step is usually unnecessary.

Installing Unbound on RHEL/AlmaLinux

On RHEL 9, AlmaLinux 9, and Rocky Linux 9, install from the base repositories:

sudo dnf install -y unbound

This installs the Unbound daemon, unbound-control, unbound-anchor, and unbound-checkconf.

Start and enable the service:

sudo systemctl start unbound
sudo systemctl enable unbound
sudo systemctl status unbound

Initialize the DNSSEC trust anchor (this downloads the root key and verifies it):

sudo unbound-anchor -a /var/lib/unbound/root.key

If this returns exit code 1, it means the key was updated (this is normal on first run). Exit code 0 means it was already up to date.

Check the version:

unbound -V

RHEL 9 ships Unbound 1.16.x. It receives security patches from Red Hat.

You will also want dig and drill for testing. Install them:

sudo dnf install -y bind-utils ldns-utils

Directory structure and key files

Unbound uses a simple directory layout. The key paths differ slightly between Debian and RHEL.

Debian/Ubuntu layout

PathPurpose
/etc/unbound/unbound.confMain configuration file
/etc/unbound/unbound.conf.d/Drop-in config directory (files here are included automatically)
/usr/share/dns/root.hintsRoot hints file (from dns-root-data package)
/usr/share/dns/root.keyDNSSEC root trust anchor (from dns-root-data package)
/var/lib/unbound/Working directory (auto-trust-anchor-file writes here)
/var/log/unbound/Log directory (you may need to create it)

RHEL/AlmaLinux layout

PathPurpose
/etc/unbound/unbound.confMain configuration file
/etc/unbound/conf.d/Drop-in config directory
/etc/unbound/local.d/Local zone overrides directory
/var/lib/unbound/root.keyDNSSEC root trust anchor (managed by unbound-anchor)
/var/lib/unbound/root.hintsRoot hints file (you may need to download this manually)
/var/log/unbound/Log directory (create it if using file logging)

The key difference from BIND: Unbound has a single configuration file (or a small set of includes), no zone files, and no separate control daemon. The entire configuration is in unbound.conf.

Understanding unbound.conf

The unbound.conf file uses a YAML-like syntax with key: value pairs grouped into named sections. Every directive belongs to a section header.

server section

The main section. Controls interfaces, access control, caching, DNSSEC, and logging.

server:
    interface: 0.0.0.0
    port: 53
    access-control: 10.0.0.0/8 allow
    do-ip6: no
    num-threads: 2
    cache-max-ttl: 86400
    verbosity: 1

forward-zone section

Defines upstream forwarders for specific zones (or all zones with name: "."). Used when you want Unbound to forward instead of recurse.

forward-zone:
    name: "."
    forward-addr: 1.1.1.1
    forward-addr: 9.9.9.9

stub-zone section

Points Unbound at specific authoritative servers for a zone, bypassing normal recursion for that zone. Useful for private internal domains.

stub-zone:
    name: "internal.corp"
    stub-addr: 10.0.0.5

remote-control section

Configures the unbound-control interface for runtime management.

remote-control:
    control-enable: yes
    control-interface: 127.0.0.1
    control-port: 8953

include directive

You can split configuration across files:

include: /etc/unbound/unbound.conf.d/*.conf

Debian sets this up by default. You can drop custom configs into the directory and they will be picked up on restart.

Validation

Always check your config before restarting:

unbound-checkconf

No output means no errors. If there is a problem, it prints the line number and error description.

Basic Unbound recursive resolver configuration

This is the core use case: Unbound resolves everything itself by querying root servers, without forwarding to any third party. This is the configuration that gives you the most privacy and control.

Create a new config file (or replace the default):

sudo tee /etc/unbound/unbound.conf > /dev/null <<'EOF'
server:
    # Listen on localhost and your LAN IP
    interface: 127.0.0.1
    interface: 10.0.0.1
    port: 53

    # Allow queries from your LAN
    access-control: 127.0.0.0/8 allow
    access-control: 10.0.0.0/24 allow
    access-control: 0.0.0.0/0 refuse

    # Disable IPv6 if your network does not use it
    do-ip6: no

    # Performance basics
    num-threads: 2
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4

    # Cache sizes
    msg-cache-size: 64m
    rrset-cache-size: 128m

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

    # Root hints (Debian: /usr/share/dns/root.hints, RHEL: download manually)
    root-hints: "/usr/share/dns/root.hints"

    # Privacy: minimize query names sent upstream (RFC 7816)
    qname-minimisation: yes

    # Prefetch records before TTL expires
    prefetch: yes

    # Logging
    verbosity: 1
    logfile: ""
    use-syslog: yes

    # Hardening
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    harden-referral-path: yes
    use-caps-for-id: yes

    # Prevent private address rebinding
    private-address: 10.0.0.0/8
    private-address: 172.16.0.0/12
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10

remote-control:
    control-enable: yes
    control-interface: 127.0.0.1
    control-port: 8953
EOF

Validate and restart:

unbound-checkconf
sudo systemctl restart unbound

Test it:

dig @127.0.0.1 example.com

You should get an A record back with status: NOERROR. The first query will take 50-200 ms (cache miss, full recursion). Repeat it and the time drops to 0 ms (cache hit).

Enabling DNSSEC validation

DNSSEC adds cryptographic verification to DNS responses. Unbound was designed with DNSSEC as a core feature, not a bolt-on.

How it works in Unbound

Unbound's validator module checks every response against the chain of trust rooted at the DNS root zone. The root zone's KSK (Key Signing Key) is the trust anchor. Unbound downloads and maintains this trust anchor automatically using RFC 5011 automated trust anchor updates.

Configuration

DNSSEC validation is enabled by default when you set the trust anchor file:

server:
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

On Debian, the dns-root-data package manages this file. On RHEL, initialize it manually:

sudo unbound-anchor -a /var/lib/unbound/root.key
sudo chown unbound:unbound /var/lib/unbound/root.key

Hardening directives

These settings strengthen DNSSEC enforcement:

server:
    # Require DNSSEC data for trust-anchored zones
    harden-dnssec-stripped: yes

    # Validate glue records (NS referral addresses)
    harden-glue: yes

    # Harden against out-of-zone referrals
    harden-referral-path: yes

    # Aggressive NSEC: use cached NSEC records to synthesize
    # negative answers without querying upstream (RFC 8198)
    aggressive-nsec: yes

Testing DNSSEC

Query a known signed domain:

# Should return NOERROR with AD (Authenticated Data) flag
dig @127.0.0.1 sigok.verteiltesysteme.net A

# Should return SERVFAIL (bad signature)
dig @127.0.0.1 sigfail.verteiltesysteme.net A

If the first returns an answer and the second returns SERVFAIL, DNSSEC is working. If both return answers, your validator is not running.

You can also use drill for more detailed DNSSEC output:

drill -S example.com @127.0.0.1

The -S flag traces the chain of trust from root to the queried zone.

Access control and interface binding

You need to control who can query your resolver and which interfaces it listens on. An open resolver on the internet will be found and abused for DNS amplification attacks within hours.

Interface binding

By default, Unbound listens on 127.0.0.1 only. To serve your LAN, add your server's LAN IP:

server:
    interface: 127.0.0.1
    interface: 10.0.0.1
    interface: 192.168.1.1

To listen on all interfaces (useful for Docker or dynamic IPs, but be careful):

server:
    interface: 0.0.0.0

Access control

The access-control directive accepts a CIDR block and an action:

server:
    # Allow localhost
    access-control: 127.0.0.0/8 allow

    # Allow your LAN
    access-control: 10.0.0.0/24 allow
    access-control: 192.168.1.0/24 allow

    # Refuse everything else
    access-control: 0.0.0.0/0 refuse
    access-control: ::0/0 refuse

Access control actions

ActionBehavior
allowAllow queries (recursive and non-recursive)
denyDrop the query silently (no response sent)
refuseSend a REFUSED response (lets the client know it was blocked)
allow_snoopAllow queries including cache snooping (non-recursive queries that check if something is cached). Only grant this to your monitoring tools.

Use refuse for public networks (it is the polite response) and deny if you want to make the server invisible to scanners.

Configuring logging

Unbound logs via syslog by default. You can switch to file-based logging for easier filtering and rotation.

Syslog (default)

server:
    use-syslog: yes
    verbosity: 1

Logs go to the system journal. View them with:

sudo journalctl -u unbound -f

File-based logging

# Create log directory
sudo mkdir -p /var/log/unbound
sudo chown unbound:unbound /var/log/unbound
server:
    use-syslog: no
    logfile: "/var/log/unbound/unbound.log"
    verbosity: 1
    log-time-ascii: yes
    log-queries: no
    log-replies: no

Verbosity levels

LevelWhat it logs
0Errors only
1Operational information (start, stop, errors)
2Detailed operational info
3Query-level detail
4Algorithm-level detail
5Cache miss detail (very verbose)

For production, use verbosity 1. Enable log-queries: yes temporarily for debugging specific issues, then disable it. On a busy resolver, query logging generates enormous output.

Log rotation

If using file-based logging, set up logrotate. Create /etc/logrotate.d/unbound:

/var/log/unbound/unbound.log {
    weekly
    missingok
    rotate 4
    compress
    delaycompress
    notifempty
    postrotate
        /usr/sbin/unbound-control log_reopen 2>/dev/null || true
    endscript
}

Performance tuning (threads, cache, prefetch)

Out of the box, Unbound works well for a single user or small household. For larger deployments, these settings make a measurable difference.

Threads

Set num-threads to the number of CPU cores available:

server:
    num-threads: 4

Each thread handles its own slice of the cache. To avoid lock contention, set the cache slab count to a power of 2 that is close to (or equal to) the thread count:

server:
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4

Cache sizes

The two main caches are the message cache (complete DNS responses) and the RRset cache (individual record sets). The RRset cache should be roughly twice the message cache:

server:
    msg-cache-size: 128m
    rrset-cache-size: 256m

On a dedicated resolver with 4 GB RAM, you can increase these to 512m and 1g respectively. On a Raspberry Pi, keep them at 16m and 32m.

Prefetching

server:
    # Fetch records before they expire (when 10% of TTL remains)
    prefetch: yes

    # Also prefetch DNSKEY and DS records for DNSSEC
    prefetch-key: yes

Prefetching keeps popular records in cache permanently. When a record is at 10% of its remaining TTL and a client queries it, Unbound refreshes it in the background. The client gets the cached answer immediately, and the next client gets a fresh answer. This eliminates the "first-query penalty" for frequently accessed domains.

Cache TTL controls

server:
    # Minimum TTL: prevents extremely short TTLs from causing excessive queries
    cache-min-ttl: 300

    # Maximum TTL: caps how long records stay cached (1 day)
    cache-max-ttl: 86400

Outgoing query tuning

server:
    # Number of outgoing ports to use (more = better randomization, harder to spoof)
    outgoing-range: 8192

    # Number of queries that can be pending per thread
    num-queries-per-thread: 4096

If you hit "socket count exceeded" errors in the log, increase outgoing-range. You may also need to raise the file descriptor limit:

sudo systemctl edit unbound

Add:

[Service]
LimitNOFILE=65536

Full production config example

server:
    num-threads: 4
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4
    msg-cache-size: 128m
    rrset-cache-size: 256m
    cache-min-ttl: 300
    cache-max-ttl: 86400
    prefetch: yes
    prefetch-key: yes
    outgoing-range: 8192
    num-queries-per-thread: 4096
    so-rcvbuf: 4m
    so-sndbuf: 4m

Integrating Unbound with Pi-hole

Pi-hole handles DNS-based ad blocking, and Unbound handles recursive resolution. Pair them and you stop depending on any third-party DNS provider.

For the Pi-hole installation itself, see our Pi-hole setup guide.

Architecture

Clients on your network point at Pi-hole (port 53). Pi-hole checks its blocklists and either sinkhole the domain or forward the query to Unbound (port 5335 on localhost). Unbound resolves it recursively and returns the answer to Pi-hole, which caches and returns it to the client.

Configure Unbound for Pi-hole

Create a dedicated config file:

sudo tee /etc/unbound/unbound.conf.d/pi-hole.conf > /dev/null <<'EOF'
server:
    verbosity: 0
    interface: 127.0.0.1
    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no

    # Trust Pi-hole on localhost
    access-control: 127.0.0.0/8 allow

    # DNSSEC
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    root-hints: "/usr/share/dns/root.hints"

    # Privacy and hardening
    qname-minimisation: yes
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    aggressive-nsec: yes

    # Performance
    prefetch: yes
    num-threads: 1
    msg-cache-size: 16m
    rrset-cache-size: 32m
    cache-min-ttl: 300
    cache-max-ttl: 86400

    # Prevent rebinding attacks
    private-address: 10.0.0.0/8
    private-address: 172.16.0.0/12
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10
EOF

Validate and restart:

unbound-checkconf
sudo systemctl restart unbound

Test Unbound directly on port 5335:

dig @127.0.0.1 -p 5335 example.com

Point Pi-hole at Unbound

In the Pi-hole web interface:

  1. Go to Settings → DNS
  2. Uncheck all upstream DNS servers (Google, Cloudflare, etc.)
  3. Under Custom 1 (IPv4), enter: 127.0.0.1#5335
  4. Save

Or edit the config file directly:

echo "PIHOLE_DNS_1=127.0.0.1#5335" | sudo tee /etc/pihole/setupVars.conf.d/99-unbound.conf
pihole restartdns

DNSSEC note

Do not enable DNSSEC in Pi-hole's settings when using Unbound. Unbound already validates DNSSEC. If Pi-hole also tries to validate, it can cause double-validation failures. Let Unbound handle all DNSSEC validation.

Verify the full chain

# Query through Pi-hole (port 53)
dig @127.0.0.1 example.com

# Query Unbound directly (port 5335)
dig @127.0.0.1 -p 5335 example.com

# Test a blocked domain through Pi-hole
dig @127.0.0.1 ads.doubleclick.net

The blocked domain should return 0.0.0.0 (Pi-hole sinkhole). The other two should return the real IP.

Using Unbound with DNS-over-TLS (DoT) forwarding

If your network blocks outbound DNS (port 53) or you prefer the speed of a large shared cache, you can configure Unbound to forward queries over TLS to privacy-focused providers. This encrypts the DNS traffic between Unbound and the upstream resolver, preventing your ISP from snooping on queries.

For a comparison of DoT and DoH protocols, see our DoH vs DoT guide.

Configuration

server:
    # All the same server settings as before...
    tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"

forward-zone:
    name: "."
    forward-tls-upstream: yes

    # Cloudflare
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
    forward-addr: 1.0.0.1@853#cloudflare-dns.com

    # Quad9
    forward-addr: 9.9.9.9@853#dns.quad9.net
    forward-addr: 149.112.112.112@853#dns.quad9.net

The #hostname after the port is the TLS authentication name. Unbound verifies the server's TLS certificate matches this hostname. The tls-cert-bundle directive points to the system's CA certificate store so Unbound can validate the TLS chain.

On RHEL/AlmaLinux, the CA bundle path is typically /etc/pki/tls/certs/ca-bundle.crt.

Choosing providers

Good DoT providers with public privacy policies (see also our DNS servers by country directory):

ProviderIPPortTLS Name
Cloudflare1.1.1.1, 1.0.0.1853cloudflare-dns.com
Quad99.9.9.9, 149.112.112.112853dns.quad9.net
Google8.8.8.8, 8.8.4.4853dns.google
Mullvad194.242.2.2853dns.mullvad.net

You can browse more resolvers in our public DNS server directory.

Trade-off: forwarding vs full recursion

DoT forwarding is not the same as full recursion. You get encrypted transport to the upstream provider, but that provider still sees every domain you query. For maximum privacy, run Unbound in full recursive mode (no forward-zone) and accept the unencrypted queries between Unbound and the authoritative servers. The DNS community is working on encrypting this last hop too, but it is not widely deployed yet.

Root hints and trust anchor management

Root hints

Root hints tell Unbound the IP addresses of the 13 DNS root server clusters (a.root-servers.net through m.root-servers.net). This is how Unbound knows where to start a recursive query.

On Debian, the dns-root-data package keeps the hints file updated. On RHEL, download it manually:

sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
sudo chown unbound:unbound /var/lib/unbound/root.hints

Then reference it in your config:

server:
    root-hints: "/var/lib/unbound/root.hints"

Root hints change rarely. A root server IP has not changed since 2017. Updating every 6 months is plenty. Even with outdated hints, Unbound will still work because it learns the correct addresses from the root servers themselves after the first successful query.

Trust anchor

The DNSSEC trust anchor is the public key of the root zone's Key Signing Key (KSK). Unbound uses it to validate the entire DNSSEC chain of trust.

The auto-trust-anchor-file directive tells Unbound to manage the trust anchor automatically using RFC 5011. When the root KSK rolls over (which has happened once, in October 2018), Unbound detects the new key and updates the file without manual intervention.

server:
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

On first install, bootstrap the trust anchor:

sudo unbound-anchor -a /var/lib/unbound/root.key
sudo chown unbound:unbound /var/lib/unbound/root.key

After that, Unbound keeps it current as long as it runs regularly. If the server is offline for an extended period (months), you may need to run unbound-anchor again before starting Unbound.

Firewall configuration

If you are serving DNS to clients beyond localhost, you need to open port 53 on your firewall.

firewalld (RHEL/AlmaLinux/Fedora)

sudo firewall-cmd --permanent --add-service=dns
sudo firewall-cmd --reload

The dns service opens both TCP and UDP port 53.

ufw (Ubuntu/Debian)

sudo ufw allow 53/tcp
sudo ufw allow 53/udp

If you only serve your LAN, restrict it:

sudo ufw allow from 10.0.0.0/24 to any port 53

iptables (manual)

sudo iptables -A INPUT -p tcp --dport 53 -s 10.0.0.0/24 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 53 -s 10.0.0.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 53 -j DROP
sudo iptables -A INPUT -p udp --dport 53 -j DROP

What about port 8953?

Port 8953 is the unbound-control port. Keep this localhost-only. It should never be exposed to the network unless you have a specific remote management need and are using TLS client certificates.

Outbound access

For full recursive mode, Unbound needs outbound access on port 53 (UDP and TCP) to the internet. If your firewall restricts outbound traffic, make sure port 53 is allowed. For DoT forwarding mode, Unbound needs outbound access on port 853 instead.

Testing with dig and drill

dig (from bind-utils or bind9-dnsutils) and drill (from ldnsutils or ldns-utils) are the standard tools for testing DNS resolvers.

Basic resolution test

# Query Unbound on localhost
dig @127.0.0.1 example.com A

# Short output (just the IP)
dig @127.0.0.1 example.com +short

# Query with full output
dig @127.0.0.1 example.com A +noall +answer +stats

DNSSEC verification

# Check for the AD (Authenticated Data) flag
dig @127.0.0.1 example.com A +dnssec

# Known-good DNSSEC test domain
dig @127.0.0.1 sigok.verteiltesysteme.net A +dnssec

# Known-bad DNSSEC test domain (should return SERVFAIL)
dig @127.0.0.1 sigfail.verteiltesysteme.net A

# Detailed DNSSEC trace with drill
drill -S example.com @127.0.0.1

Cache behavior

# First query — cache miss, full recursion
dig @127.0.0.1 example.com | grep "Query time"
# Output: ;; Query time: 87 msec

# Second query — cache hit
dig @127.0.0.1 example.com | grep "Query time"
# Output: ;; Query time: 0 msec

Checking response flags

dig @127.0.0.1 example.com

Look at the flags line:

  • qr — This is a response (not a query)
  • rd — Recursion desired (you asked for it)
  • ra — Recursion available (the server supports it)
  • ad — Authenticated data (DNSSEC validated)

If ra is missing, Unbound is not allowing recursion for your IP. Check access-control.

Testing a specific port (Pi-hole setup)

dig @127.0.0.1 -p 5335 example.com

Reverse DNS lookup

dig @127.0.0.1 -x 8.8.8.8

Should return dns.google.

You can also test your resolver using our DNS dig tool from the browser.

Monitoring and statistics

Unbound provides runtime statistics through unbound-control. You need the remote-control section enabled (see the basic config above).

Setup unbound-control

Generate the control keys (required on first use):

sudo unbound-control-setup

This creates TLS certificates in /etc/unbound/ for authenticated communication between unbound-control and the daemon. Restart Unbound after generating keys:

sudo systemctl restart unbound

Viewing statistics

# Full stats dump
sudo unbound-control stats_noreset

# Key metrics only
sudo unbound-control stats_noreset | grep -E "total.num|cache.count|num.query"

Key metrics to monitor

MetricWhat it means
total.num.queriesTotal queries received since start
total.num.cachehitsQueries answered from cache (higher is better)
total.num.cachemissQueries that required upstream resolution
total.num.recursiverepliesAnswers returned after recursion
msg.cache.countNumber of entries in the message cache
rrset.cache.countNumber of entries in the RRset cache
num.query.type.AA record queries
num.query.type.AAAAAAAA record queries
unwanted.queriesRefused queries (unauthorized clients)

Cache hit ratio

Calculate your cache hit rate:

sudo unbound-control stats_noreset | awk -F= '
    /total.num.cachehits/ {hits=$2}
    /total.num.cachemiss/ {miss=$2}
    END {printf "Cache hit rate: %.1f%%\n", hits/(hits+miss)*100}'

A healthy resolver should show 70-90%+ cache hit rate after running for a few hours.

Flushing the cache

# Flush a specific domain
sudo unbound-control flush example.com

# Flush everything under a domain
sudo unbound-control flush_zone example.com

# Flush the entire cache
sudo unbound-control reload

Prometheus integration

For production monitoring, use the unbound_exporter for Prometheus. It scrapes unbound-control stats and exposes them as Prometheus metrics. This lets you build Grafana dashboards for query rates, cache performance, latency distributions, and error rates.

Troubleshooting common Unbound issues

Unbound will not start

Check the config first:

unbound-checkconf

Then check the journal:

sudo journalctl -u unbound -e --no-pager

Common causes:

  • Port 53 already in use — Another service (systemd-resolved, dnsmasq) is occupying port 53. Stop or disable it.
  • Permission denied on trust anchor — The unbound user cannot read root.key. Fix: sudo chown unbound:unbound /var/lib/unbound/root.key
  • Config syntax error — unbound-checkconf will tell you the exact line and problem.
# Check what is using port 53
sudo ss -tlnp | grep :53

Queries refused

If dig @your-server example.com returns status: REFUSED:

  • Check access-control — is the client's IP or subnet listed with allow?
  • Check interface — is Unbound listening on the IP the client is connecting to?
  • Check the firewall — is port 53 open for the client's source IP?

SERVFAIL responses

SERVFAIL usually means one of:

  • DNSSEC validation failure — The upstream zone has broken DNSSEC. Test with dig +cd (checking disabled). If the query works with +cd, DNSSEC is the problem (upstream, not your server).
  • Cannot reach upstream servers — Your firewall blocks outbound port 53. Test: dig @198.41.0.4 com NS (query a root server directly). If this times out, it is a network/firewall issue.
  • Root hints or trust anchor missing — Verify the files exist and are readable by the unbound user.
# Enable more verbose logging temporarily
sudo unbound-control verbosity 3
# Check logs, then reset
sudo unbound-control verbosity 1

Slow resolution

If the first query to a new domain takes several seconds:

  • IPv6 timeouts — If your server lacks IPv6 connectivity but Unbound tries IPv6 queries, each one times out before falling back to IPv4. Fix: do-ip6: no
  • Rate-limited by upstream — Some authoritative servers rate-limit queries. This is rare for normal usage.
  • Insufficient outgoing sockets — Increase outgoing-range and the file descriptor limit.

Port conflict with Pi-hole

If both Pi-hole (via dnsmasq/FTL) and Unbound try to use port 53:

# Unbound should use a different port for Pi-hole integration
# In unbound.conf:
# port: 5335

Pi-hole's FTL uses port 53. Unbound must listen on a different port (5335 is the convention).

Backup, updates, and maintenance

Backing up Unbound

Unbound has no zone files or databases to back up. The configuration is everything:

sudo tar czf /root/unbound-backup-$(date +%Y%m%d).tar.gz \
    /etc/unbound/ \
    /var/lib/unbound/root.key \
    /var/lib/unbound/root.hints

Run this from cron weekly or before any configuration change. To restore on a new server, install Unbound, extract the backup, and start the service.

Updating Unbound

# Debian/Ubuntu
sudo apt update && sudo apt upgrade unbound

# RHEL/AlmaLinux
sudo dnf update unbound

After updating, check the release notes for any config syntax changes. NLnet Labs publishes release notes at https://nlnetlabs.nl/projects/unbound/about/. Major versions occasionally deprecate old directives.

Restart the service after updating:

sudo systemctl restart unbound

Updating root hints

If not using the dns-root-data package (Debian), update root hints periodically:

sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
sudo chown unbound:unbound /var/lib/unbound/root.hints
sudo systemctl restart unbound

Automate with a cron job (monthly is more than enough):

0 3 1 * * curl -sfo /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache && systemctl restart unbound

Health check script

A simple script to verify Unbound is responding:

#!/bin/bash
if ! dig @127.0.0.1 example.com +short +time=2 +tries=1 > /dev/null 2>&1; then
    echo "Unbound DNS is not responding on $(hostname)" | \
        mail -s "DNS ALERT" admin@example.com
    # Optionally attempt a restart
    # sudo systemctl restart unbound
fi

Run it from cron every 5 minutes:

*/5 * * * * /usr/local/bin/check_unbound.sh

Cache warm-up after restart

After a restart, the cache is empty and all queries incur full recursion latency. For high-traffic resolvers, you can pre-populate the cache by dumping and reloading it:

# Before restart: dump the cache
sudo unbound-control dump_cache > /tmp/unbound_cache.txt

# After restart: reload the cache
sudo unbound-control load_cache < /tmp/unbound_cache.txt

This restores cached records so clients do not experience a performance cliff after maintenance.

Frequently asked questions

What is the difference between a recursive resolver and a forwarding resolver?

A recursive resolver queries the DNS hierarchy directly, starting from root servers, and assembles the answer itself. A forwarding resolver sends every query to an upstream resolver (like 1.1.1.1 or 8.8.8.8) and caches the response. Unbound can operate in either mode. Full recursion gives you more privacy because no single upstream provider sees all your queries. Forwarding is simpler and faster for the first lookup but puts trust in the upstream provider.

Does Unbound support DNS-over-HTTPS (DoH)?

Unbound does not natively support inbound DNS-over-HTTPS as a server. It supports outbound DNS-over-TLS (DoT) for forwarding. If you need DoH for clients, place a DoH proxy like dnscrypt-proxy or nginx in front of Unbound, or use Unbound behind a reverse proxy that terminates HTTPS and forwards plain DNS to Unbound on localhost.

How much RAM does Unbound need?

For a home network or small office, Unbound runs comfortably on 50 to 100 MB of RAM. The main consumer is the cache. By default, the message cache is 4 MB and the RRset cache is 4 MB. For a busy resolver serving hundreds of users, allocating 256 MB to 512 MB for cache improves hit rates and reduces upstream queries.

Can Unbound replace Pi-hole?

No. They serve different purposes. Pi-hole is a DNS sinkhole that blocks ads and trackers using blocklists. Unbound is a recursive resolver that fetches DNS answers. They work best together: Pi-hole filters queries and forwards the rest to Unbound, which resolves them without relying on a third-party DNS provider.

Is Unbound better than BIND for recursive resolution?

For pure recursive resolution, Unbound is generally the better choice. It was designed from the ground up as a validating recursive resolver. It has a smaller attack surface, uses less memory, and requires less configuration than BIND for this role. BIND is better suited when you also need authoritative zone hosting on the same server.

Does Unbound cache DNS responses?

Yes. Caching is one of its core functions. Unbound stores resolved records in memory and serves them for subsequent queries until the TTL expires. It also supports aggressive NSEC caching (RFC 8198), which synthesizes negative answers from cached NSEC records to reduce queries for nonexistent domains.

How do I update root hints?

Download the latest root hints file from IANA: curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache. Then restart Unbound. Root hints rarely change (the last significant update was in 2017 when a root server IP changed), so checking every six months is sufficient.

Can Unbound run on a Raspberry Pi?

Yes. Unbound runs well on any Raspberry Pi model with at least 512 MB of RAM. It is commonly paired with Pi-hole on Raspberry Pi for a combined ad-blocking and privacy-focused DNS setup. The ARM packages are available in the default Raspberry Pi OS (Debian-based) repositories.

Finding reliable DNS servers

If you need upstream resolvers for forwarding mode or just want to compare public DNS providers, browse our DNS directory for live-tested servers filtered by country and reliability.