Unbound is a fast, secure, validating, recursive DNS resolver. Instead of sending every lookup to Google or Cloudflare, it resolves names itself by querying the root, TLD and authoritative servers directly, and caches the answers. On a VPS it can serve your servers, your VPN clients or a Pi-hole, with DNSSEC validation and no third-party resolver seeing all your queries.

Recursive vs forwarding resolvers

  • Recursive (Unbound’s default): starts at the root servers and follows delegations to the authoritative answer. No single upstream sees all your queries.
  • Forwarding: passes queries to an upstream resolver (Cloudflare, Quad9), optionally over TLS. It’s simpler and benefits from big shared caches, but that provider sees everything.

Unbound does both.

Install

sudo apt install unbound            # Debian/Ubuntu
sudo dnf install unbound            # RHEL family

On Ubuntu, systemd-resolved already listens on 127.0.0.53:53. Unbound can listen on other addresses, or you can make resolved use Unbound as its upstream.

Basic configuration

/etc/unbound/unbound.conf.d/server.conf:

server:
    interface: 127.0.0.1
    interface: 10.8.0.1            # e.g. your WireGuard interface, to serve VPN clients
    port: 53

    access-control: 0.0.0.0/0 refuse
    access-control: 127.0.0.0/8 allow
    access-control: 10.8.0.0/24 allow

    # privacy and hardening
    hide-identity: yes
    hide-version: yes
    qname-minimisation: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no

    # performance
    prefetch: yes
    cache-min-ttl: 60
    serve-expired: yes
    num-threads: 2
    msg-cache-size: 64m
    rrset-cache-size: 128m

    # DNSSEC trust anchor (managed automatically on Debian/Ubuntu)
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
  • access-control is critical. By default refuse everyone, then allow only localhost and your private networks.
  • qname-minimisation sends only the needed part of the name to each server (com to the root, example.com to .com…).
  • prefetch refreshes popular entries before they expire, so cache hits stay fast.

Check and start:

sudo unbound-checkconf
sudo systemctl enable --now unbound
dig @127.0.0.1 example.com
dig @127.0.0.1 example.com +dnssec     # look for the "ad" flag (authenticated data)

Test DNSSEC validation with a deliberately broken domain:

dig @127.0.0.1 dnssec-failed.org       # should return SERVFAIL

Never run an open resolver

A resolver that answers anyone on the internet gets found and abused for DNS amplification attacks within days. Your IP ends up in abuse reports, and your bandwidth is consumed. Besides access-control, firewall port 53:

sudo ufw allow in on wg0 to any port 53     # VPN clients only
sudo ufw deny 53

Verify from outside, with dig @your-public-ip example.com from another network. It must time out or be refused. See DDoS attack types.

Forward upstream over TLS (optional)

If you prefer a privacy-focused upstream to full recursion, for example because some networks interfere with direct queries, forward with DNS over TLS:

forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 9.9.9.9@853#dns.quad9.net
    forward-addr: 1.1.1.1@853#cloudflare-dns.com

Make sure tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt is set in server:. See DNS over HTTPS and TLS.

Use it on the server itself

With systemd-resolved, make it forward to Unbound (if Unbound listens on 127.0.0.1:53, disable resolved’s stub listener first, or run Unbound on another port):

# /etc/systemd/resolved.conf
[Resolve]
DNS=127.0.0.1
DNSStubListener=no

Then restart resolved and point /etc/resolv.conf at 127.0.0.1. See change DNS servers.

Pi-hole + Unbound

A popular combination: Pi-hole filters ads and trackers, and Unbound resolves recursively. Run Unbound on 127.0.0.1#5335, and set Pi-hole’s custom upstream to 127.0.0.1#5335. See Pi-hole on a VPS.

Local overrides

Answer internal names yourself:

server:
    local-zone: "internal.example." static
    local-data: "db.internal.example. IN A 10.0.0.12"
    local-data: "app.internal.example. IN A 10.0.0.21"

Monitoring

sudo unbound-control stats_noreset | grep -E 'total.num.queries|cachehits|cachemiss'
sudo unbound-control dump_cache | head
sudo unbound-control flush example.com

Enable unbound-control with sudo unbound-control-setup if needed. Cache hit rates above 70–80% are typical for busy resolvers.

Quick reference

Task Command / setting
Check config unbound-checkconf
Allow clients access-control: 10.8.0.0/24 allow
Test dig @127.0.0.1 example.com +dnssec
DNSSEC fail test dig @127.0.0.1 dnssec-failed.org → SERVFAIL
Forward over TLS forward-tls-upstream: yes
Flush a name unbound-control flush NAME

Related: PowerDNS and AdGuard Home.

  • DNS
  • Self-Hosting
  • Privacy