Running a Local DNS Resolver with Unbound

July 17, 2026

system-administration networking

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.


Other Recent Posts