Running a Local DNS Resolver with Unbound
July 17, 2026
Every service on a server depends on DNS. By default, servers use whatever resolver the hosting provider assigns — often a shared recursive resolver with no guarantees on uptime, privacy, or DNSSEC validation. Running a local Unbound instance eliminates that dependency. Queries are resolved recursively from the root servers, cached locally, and validated with DNSSEC. The result is faster repeat lookups, no reliance on third-party resolvers, and cryptographic proof that DNS responses haven't been tampered with.
Installing Unbound
On Gentoo
emerge -av net-dns/unbound
Gentoo compiles Unbound with DNSSEC support by default. The dns-root-hints USE flag pulls in root hints automatically.
On Debian
apt install -y unbound dnsutils
The dnsutils package provides dig and nslookup for testing. Debian's Unbound package includes a systemd unit and root hints.
On Alpine
apk add unbound unbound-openrc
Alpine ships a minimal Unbound package. The unbound-openrc package provides the init script.
Configuring Unbound
The default Unbound configuration listens on localhost and acts as a recursive resolver. The configuration file lives at /etc/unbound/unbound.conf. Replace or edit it with a configuration tuned for a server environment.
The Configuration File
Write the following to /etc/unbound/unbound.conf:
server:
# Network
interface: 127.0.0.1
port: 53
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
# Access control
access-control: 127.0.0.0/8 allow
access-control: ::1/128 allow
access-control: 0.0.0.0/0 refuse
access-control: ::/0 refuse
# Performance
num-threads: 2
msg-cache-slabs: 4
rrset-cache-slabs: 4
infra-cache-slabs: 4
key-cache-slabs: 4
msg-cache-size: 64m
rrset-cache-size: 128m
key-cache-size: 32m
neg-cache-size: 16m
target-fetch-policy: "3 2 1 0 0"
# Cache tuning
cache-min-ttl: 300
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
serve-expired: yes
serve-expired-ttl: 86400
# Privacy
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
harden-algo-downgrade: yes
# DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Logging
verbosity: 1
log-queries: no
log-replies: no
log-servfail: yes
Key Configuration Decisions
interface: 127.0.0.1 — Unbound only listens on localhost. This server resolves its own DNS; it doesn't serve queries to the network. If you need to serve DNS to a local network (e.g., LXC containers), add the bridge interface IP here and adjust access-control accordingly.
access-control — explicit deny-by-default. Only localhost can query. This prevents the server from becoming an open resolver, which would be abused for DNS amplification attacks within hours.
cache-min-ttl: 300 — forces a minimum 5-minute cache even for records with very short TTLs. This reduces upstream query volume for domains that set aggressive TTLs (common with CDNs). The tradeoff is slightly stale records during DNS changes, which is acceptable for a server that isn't rotating DNS constantly.
prefetch: yes — when a cached record is about to expire (within 10% of its TTL), Unbound fetches a fresh copy in the background. The next query gets an instant cache hit instead of waiting for a recursive lookup.
serve-expired: yes — if the upstream is unreachable, Unbound serves stale cached records rather than returning SERVFAIL. This keeps services running through brief upstream outages.
auto-trust-anchor-file — this enables DNSSEC validation. Unbound uses RFC 5011 to automatically roll the root trust anchor. The file is created during package installation on most distributions.
Root Trust Anchor
DNSSEC requires a trust anchor — the root zone's public key. Most packages install this automatically. Verify it exists:
ls -la /var/lib/unbound/root.key
If the file is missing, generate it:
unbound-anchor -a /var/lib/unbound/root.key
On Gentoo, the trust anchor may be at /etc/dnssec/root.key depending on the package version. Adjust the auto-trust-anchor-file path accordingly:
ls /etc/dnssec/root.key /var/lib/unbound/root.key 2>/dev/null
Use whichever path exists on your system.
Root Hints
Unbound needs to know the IP addresses of the root DNS servers to start recursive resolution. Most packages include root hints at /etc/unbound/root.hints or fetch them during installation.
If the file is missing or you want to update it:
curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.root
Add a reference in unbound.conf if it's not already included:
server:
root-hints: "/etc/unbound/root.hints"
Root hints change infrequently (every few years), but updating them annually is good practice.
Validating the Configuration
Before starting or restarting Unbound, validate the configuration file:
unbound-checkconf
This parses the full configuration and reports any syntax errors. If valid, it outputs:
unbound-checkconf: no errors in /etc/unbound/unbound.conf
Starting and Enabling the Service
On Gentoo
rc-update add unbound default
rc-service unbound start
On Debian
systemctl enable unbound
systemctl start unbound
On Alpine
rc-update add unbound default
rc-service unbound start
Configuring the System to Use Unbound
After Unbound is running, point the server's own DNS resolution at localhost.
On Gentoo
If the system uses dhcpcd for network configuration, it regenerates /etc/resolv.conf on every DHCP lease renewal. Editing it directly won't persist. Instead, create /etc/resolv.conf.head — dhcpcd prepends its contents to the generated file:
cat > /etc/resolv.conf.head << 'EOF'
nameserver 127.0.0.1
nameserver ::1
EOF
Then trigger a lease renewal to regenerate resolv.conf:
dhcpcd --rebind eth0
Verify the result:
cat /etc/resolv.conf
You should see 127.0.0.1 listed first, followed by whatever nameserver dhcpcd provides as a fallback. The system will try Unbound first and only fall back to the DHCP-provided resolver if Unbound is unreachable.
If the system does not use dhcpcd, edit /etc/resolv.conf directly:
echo "nameserver 127.0.0.1" > /etc/resolv.conf
On Debian
If using systemd-resolved, disable it and point directly at Unbound:
systemctl stop systemd-resolved
systemctl disable systemd-resolved
rm /etc/resolv.conf
echo "nameserver 127.0.0.1" > /etc/resolv.conf
Alternatively, configure systemd-resolved to forward to Unbound by editing /etc/systemd/resolved.conf:
[Resolve]
DNS=127.0.0.1
DNSSEC=no # Unbound handles DNSSEC, not resolved
If using resolvconf, install the head file:
mkdir -p /etc/resolvconf/resolv.conf.d
cat > /etc/resolvconf/resolv.conf.d/head << 'EOF'
nameserver 127.0.0.1
nameserver ::1
EOF
resolvconf -u
On Alpine
echo "nameserver 127.0.0.1" > /etc/resolv.conf
Alpine doesn't run a local resolver by default, so this is straightforward.
Testing the Resolver
Basic Resolution
dig @127.0.0.1 example.com A
You should see a response with status: NOERROR and an answer section containing the IP address.
Cache Performance
Run the same query twice and compare query times:
dig @127.0.0.1 debian.org A | grep "Query time"
dig @127.0.0.1 debian.org A | grep "Query time"
The first query hits the root servers and takes 50-200ms depending on network latency. The second query returns from cache and should show Query time: 0 msec.
DNSSEC Validation
Test that DNSSEC is working by querying a signed domain:
dig @127.0.0.1 cloudflare.com A +dnssec
Look for the ad flag in the response header — this means the response was authenticated via DNSSEC:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
The ad (Authenticated Data) flag confirms that the entire chain of trust from the root zone to cloudflare.com was validated.
To test that DNSSEC rejects bad signatures, query a known-bad domain:
dig @127.0.0.1 dnssec-failed.org A
This should return SERVFAIL because the domain intentionally has invalid DNSSEC signatures. If you get an answer instead of SERVFAIL, DNSSEC validation is not working.
Verify Unbound Is Listening
ss -lnup sport = :53
You should see Unbound bound to 127.0.0.1:53:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
UNCONN 0 0 127.0.0.1:53 0.0.0.0:* users:(("unbound",pid=1234,fd=5))
Troubleshooting
Port 53 Already in Use
If another service is using port 53 (common with systemd-resolved on Debian):
ss -lnup sport = :53
If systemd-resolved is listening, stop it:
systemctl stop systemd-resolved
systemctl disable systemd-resolved
Unbound Fails to Start
Check the log:
# Gentoo/Alpine
cat /var/log/messages | grep unbound
# Debian
journalctl -u unbound
Common causes: missing root trust anchor, invalid unbound.conf syntax, or another process on port 53.
Queries Time Out
Verify the server has outbound UDP/TCP on port 53 to the internet. Unbound needs to reach root servers directly. Test by querying a root nameserver (find the current list in /etc/unbound/root.hints or at https://www.internic.net/domain/named.root):
dig @a.root-servers.net . NS
If this times out, a firewall is blocking outbound DNS. Check iptables or your hosting provider's firewall rules.
Why Mail Servers Need a Local Resolver
If the server runs a mail stack with Rspamd, a local recursive resolver isn't optional — it's required for DNS-based blocklist (RBL) queries to work.
Rspamd checks sender IPs against blocklists like Spamhaus ZEN, SORBS, and URIBL by making DNS queries with reversed IP octets against the blocklist domain. Commercial RBL providers block queries from shared or public resolvers. If the server forwards DNS through a hosting provider's resolver or a public service, Spamhaus returns an error code meaning "query via public resolver not permitted." Rspamd interprets this as an invalid response and disables the blocklist entirely — with no obvious warning beyond a log message.
With a local Unbound instance resolving directly from root servers, RBL queries go straight from the server to the blocklist's authoritative nameservers. No shared resolver in the middle means no rate limiting and no access restrictions.
You can verify RBL queries work after switching to Unbound using the standard Spamhaus test (which queries the reversed loopback address):
# Should return specific blocklist codes like 127.0.0.x — NOT empty or an error
dig @127.0.0.1 +short zen.spamhaus.org
If the query returns no results or an unexpected response, the RBL is not accessible through your resolver.
Summary
After completing these steps:
- DNS queries are resolved recursively from the root servers
- Responses are cached locally for fast repeat lookups
- DNSSEC validates that responses haven't been tampered with
- The server has no dependency on external recursive resolvers
- Stale cache entries are served during upstream outages
- RBL queries for spam filtering work without rate limits or access restrictions
The local resolver is now a foundation for every other service on the server. Mail servers in particular depend on reliable DNS for MX lookups, SPF checks, DKIM verification, and spam blocklist queries.