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-controlis critical. By default refuse everyone, then allow only localhost and your private networks.qname-minimisationsends only the needed part of the name to each server (comto the root,example.comto.com…).prefetchrefreshes 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.