Setting Up Unbound Recursive DNS Resolver on Debian 13
A practical guide for system administrators running production servers. This guide focuses on the clean, reliable approach: running Unbound directly without systemd-resolved.
Why Run Your Own Recursive DNS Resolver?
When operating servers, especially mail servers, DNS reliability and performance are critical. Public DNS providers (Google, Cloudflare, Quad9) have a significant limitation: they intentionally rate-limit or block DNSBL (DNS-based Blacklist) queries used for spam detection. This causes:
- Slow mail delivery
- Failed spam checks
- Incomplete DNSBL lookups
- Delivery delays and timeouts
Running your own recursive DNS resolver with Unbound solves this completely:
- No rate limiting – Direct queries to authoritative nameservers
- DNS independence – No reliance on third-party services
- Privacy – No external logging of your DNS queries
- Performance – Excellent caching for repeated queries
- Essential for mail servers – Unlimited DNSBL queries
- DNSSEC validation – Built-in security and integrity checking
Prerequisites
- Debian 12/13 (or similar systemd-based distribution)
- Root or sudo access
- Basic understanding of DNS
- Server environment (physical, VM, or container)
Installation
sudo apt update
sudo apt install unbound unbound-anchor curl dnsutils -yPackages installed:
unbound– The DNS resolverunbound-anchor– DNSSEC trust anchor managementcurl– For downloading root hintsdnsutils– Providesdigfor testing
Configuration
Step 1: Download Root Hints
Root hints enable Unbound to perform recursive resolution starting from DNS root servers:
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
sudo chown unbound:unbound /var/lib/unbound/root.hints
sudo chmod 644 /var/lib/unbound/root.hintsStep 2: Configure Unbound
Create the local configuration file:
sudo vi /etc/unbound/unbound.conf.d/local.confAdd this configuration:
server:
# Module configuration - disable subnetcache (not needed for local resolver)
module-config: "validator iterator"
# Network - listen on standard port
interface: 127.0.0.1
interface: ::1
port: 53
do-ip6: no
# Access control
access-control: 127.0.0.0/8 allow
access-control: ::1 allow
do-not-query-localhost: no
# Recursive resolution
root-hints: "/var/lib/unbound/root.hints"
# Performance
msg-cache-size: 50m
rrset-cache-size: 100m
cache-min-ttl: 300
cache-max-ttl: 86400
cache-max-negative-ttl: 3600
prefetch: yes
prefetch-key: yes
# Privacy and security
hide-identity: yes
hide-version: yes
qname-minimisation: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes
val-clean-additional: yes
# DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Network optimization
edns-buffer-size: 1232
# Logging (set to 0 after testing)
verbosity: 1
log-queries: yesImportant configuration notes:
- module-config: Specifies which Unbound modules to use. We’re using
validator(for DNSSEC validation) anditerator(for recursive resolution). The default Debian configuration includessubnetcachewhich isn’t needed for a local resolver and causes a warning withprefetchenabled. - Port 53: Standard DNS port, since systemd-resolved won’t be running
- do-ip6: no: IPv6 is disabled for server use. Spamhaus and some DNSBL providers falsely flag IPv6 queries as “open resolver” activity, potentially blocking your server from their services. Only enable IPv6 if you’ve verified all your DNSBL providers support it properly.
Remove duplicate configuration if it exists:
# Check for existing auto-trust-anchor-file configuration
ls /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf
# If it exists, remove it to avoid duplication
sudo rm /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.confStep 3: Disable systemd-resolved
systemd-resolved must be completely disabled. It creates DNS resolution files owned by systemd-resolve:systemd-resolve, which causes critical problems for chrooted services like Postfix (see dedicated section below).
# Stop and disable systemd-resolved
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
# Mask to prevent it from ever starting again
sudo systemctl mask systemd-resolved
# Verify it's properly disabled
systemctl status systemd-resolved
# Should show: Loaded: masked (Reason: Unit systemd-resolved.service is masked.)Why mask instead of just disable?
The mask command creates a symlink to /dev/null, which prevents the service from being started by any means—manually, as a dependency, or automatically. This is stronger than disable and ensures systemd-resolved stays permanently off.
Step 4: Disable unbound-resolvconf Service
During installation, the Debian unbound package automatically enables unbound-resolvconf.service. This service can interfere with our configuration, so disable it:
# Disable the service
sudo systemctl disable --now unbound-resolvconf.service
# Verify it's disabled
systemctl is-enabled unbound-resolvconf.service
# Should show: disabledWhy disable this? Even though the service includes a conditional check and won’t activate without the resolvconf package, disabling it keeps your configuration clean and prevents potential future conflicts.
Step 5: Create Static resolv.conf
Remove the systemd-resolved symlink and create a static /etc/resolv.conf:
# Remove the systemd-resolved symlink
sudo rm -f /etc/resolv.conf
# Create proper resolv.conf pointing to Unbound
sudo tee /etc/resolv.conf > /dev/null << 'EOF'
nameserver 127.0.0.1
nameserver ::1
EOF
# Set proper permissions
sudo chmod 644 /etc/resolv.confMake it immutable (bare metal/VMs):
On physical servers and VMs, use the immutable flag to prevent any process from modifying /etc/resolv.conf:
# Make the file immutable
sudo chattr +i /etc/resolv.conf
# Verify the immutable flag
lsattr /etc/resolv.conf
# Should show: ----i---------e------- /etc/resolv.confTo edit later (if needed):
# Remove immutable flag
sudo chattr -i /etc/resolv.conf
# Make changes
sudo vi /etc/resolv.conf
# Restore immutable flag
sudo chattr +i /etc/resolv.confFor Incus/LXC containers:
The chattr +i command requires the CAP_LINUX_IMMUTABLE capability, which unprivileged containers don’t have. You’ll get “Operation not permitted” errors.
Container solution – use systemd-networkd configuration instead:
sudo vi /etc/systemd/network/10-eth0.networkAdd:
[Match]
Name=eth0
[Network]
DHCP=yes
[DHCP]
UseDNS=false
UseDomains=falseThen enable and restart systemd-networkd:
sudo systemctl enable systemd-networkd
sudo systemctl restart systemd-networkdWhat this does: Tells systemd-networkd to ignore DNS servers provided by DHCP, preventing /etc/resolv.conf from being overwritten even without the immutable flag.
Step 6: Configure Unbound Daemon Options
Create the Unbound defaults file to prevent environment variable warnings:
sudo vi /etc/default/unboundAdd:
# Additional daemon options
DAEMON_OPTS=""This prevents systemd warnings about unset environment variables. Leave it empty for standard operation, or add options later if needed (e.g., DAEMON_OPTS="-d -v" for debugging).
Step 7: Validate and Start Unbound
# Validate configuration
sudo unbound-checkconf
# Start Unbound
sudo systemctl restart unbound
sudo systemctl enable unbound
# Check status
sudo systemctl status unbound
# Should show: active (running)Expected startup messages:
When Unbound starts, you’ll see informational notices in the logs:
notice: init module 0: validator
notice: init module 1: iteratorThese are normal and confirm that:
- validator module is loaded (handles DNSSEC validation)
- iterator module is loaded (performs recursive DNS resolution)
The modules load in this order: validator checks DNSSEC signatures first, then iterator does the actual DNS resolution.
Verification
Basic DNS Resolution Tests
# Test direct query to Unbound
dig @127.0.0.1 debian.org
# Test system resolution
ping -c 2 google.com
# Verify resolv.conf
cat /etc/resolv.conf
# Should show: nameserver 127.0.0.1
# Check ownership
ls -la /etc/resolv.conf
# Should show: -rw-r--r-- 1 root root
# For bare metal/VMs, verify immutable flag
lsattr /etc/resolv.conf
# Should show: ----i---------e-------Test DNS Caching
# First query (slower - fetches from authoritative nameserver)
time dig @127.0.0.1 wikipedia.org
# Second query (faster - served from cache)
time dig @127.0.0.1 wikipedia.orgThe second query should be significantly faster (typically <10ms vs 50-200ms).
Test DNSSEC Validation
# Test valid DNSSEC - should resolve successfully
dig +dnssec @127.0.0.1 sigok.verteiltesysteme.net
# Test broken DNSSEC - should fail (SERVFAIL)
dig +dnssec @127.0.0.1 sigfail.verteiltesysteme.net
# Check a common signed domain
dig +dnssec @127.0.0.1 debian.orgExpected results:
sigok.verteiltesysteme.net– Returns answer withadflag (authenticated data)sigfail.verteiltesysteme.net– ReturnsSERVFAIL(validation failed)- If both work correctly, DNSSEC validation is functioning
Monitor Unbound Activity
With log-queries: yes enabled, you can watch DNS queries in real-time:
# Watch logs
sudo journalctl -u unbound -f
# In another terminal, make queries
dig cloudflare.comYou should see log entries like:
info: 127.0.0.1 cloudflare.com. A INDisable query logging after testing:
For production use, disable query logging to protect privacy and reduce log volume:
sudo vi /etc/unbound/unbound.conf.d/local.conf
# Change: log-queries: yes to log-queries: no
sudo systemctl restart unboundPostfix and the systemd-resolved Incompatibility
If you’re running Postfix (or any mail server), this section is critical.
Why systemd-resolved Breaks Postfix
Postfix runs in a chroot environment (/var/spool/postfix/) by default on Debian for security. This chroot needs its own copy of /etc/resolv.conf at /var/spool/postfix/etc/resolv.conf, and this file must be owned by root:root for Postfix to function.
The Problem:
Postfix includes a resolvconf hook at /etc/resolvconf/update-libc.d/postfix that automatically copies /etc/resolv.conf to the chroot:
cat /etc/resolvconf/update-libc.d/postfix
# Shows: [ lo != "$IFACE" ] && cp -pLu /etc/resolv.conf /var/spool/postfix/etc/resolv.conf 2>/dev/null || :When systemd-resolved is active:
/etc/resolv.confis a symlink to/run/systemd/resolve/stub-resolv.conf- That file is owned by
systemd-resolve:systemd-resolve - The hook uses
cp -pLwhich preserves ownership when copying - Result:
/var/spool/postfix/etc/resolv.confbecomes owned bysystemd-resolve:systemd-resolve - Postfix fails with ownership error
The Insidious Intermittent Trap:
The hook uses the -u flag (“update only if source is newer”), which creates an intermittent failure mode:
- Most of the time: Source file isn’t newer, so
cp -udoesn’t copy anything - Postfix keeps working with the old
root:rootfile - After system events: Reboot, network change, systemd-resolved restart
- Source becomes newer, hook copies it with wrong ownership
- Postfix suddenly breaks!
This intermittent behavior makes the problem extremely hard to diagnose. Your mail server may work perfectly for weeks, then fail unpredictably after a routine system event.
Demonstration:
# With systemd-resolved active (DON'T do this on production!)
ls -la /etc/resolv.conf
# Shows: lrwxrwxrwx ... /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
ls -la /run/systemd/resolve/stub-resolv.conf
# Shows: -rw-r--r-- 1 systemd-resolve systemd-resolve ...
# Trigger the trap (simulates when source is newer)
sudo cp -pL /etc/resolv.conf /var/spool/postfix/etc/resolv.conf
# Check ownership - BROKEN!
ls -la /var/spool/postfix/etc/resolv.conf
# Shows: -rw-r--r-- 1 systemd-resolve systemd-resolve ...
# Postfix detects the problem
postfix check
# Shows: postfix/postfix-script: warning: not owned by root: /var/spool/postfix/etc/resolv.confVerify Postfix Chroot Configuration
After disabling systemd-resolved and configuring Unbound, verify the chroot resolv.conf has correct ownership:
# Check if the file exists with correct ownership
ls -la /var/spool/postfix/etc/resolv.conf
# Should show: -rw-r--r-- 1 root rootIf the file doesn’t exist yet, Postfix will copy it on next restart:
sudo systemctl restart postfix
ls -la /var/spool/postfix/etc/resolv.conf
# Should now show: -rw-r--r-- 1 root rootVerify Postfix is happy:
postfix check
# Should return with no warningsTest DNSBL Resolution
With Unbound configured, test that Postfix can query DNSBL services:
# Test a known-listed IP (127.0.0.2 is standard DNSBL test)
dig @127.0.0.1 2.0.0.127.zen.spamhaus.org
# Should return 127.0.0.2 or similar listing codeNote: DNSBL queries require reversed IP octets. For IP 203.0.113.50, query 50.113.0.203.zen.spamhaus.org
Verify Postfix DNS configuration:
postconf smtp_host_lookup
# Should show: smtp_host_lookup = dnsWhy this matters: With Unbound, your mail server can now perform unlimited DNSBL queries without rate limiting from public DNS providers. This is essential for effective spam filtering.
Maintenance
Automatic Updates with systemd Timer
Keep root hints and DNSSEC trust anchors updated automatically.
Create update script:
sudo tee /usr/local/bin/update-unbound-data.sh > /dev/null <<'EOF'
#!/bin/bash
set -e
echo "$(date): Updating Unbound data..."
# Update DNSSEC root trust anchor
/usr/sbin/unbound-anchor -a /var/lib/unbound/root.key
# Update root hints
curl -s -o /tmp/root.hints.tmp https://www.internic.net/domain/named.root
if [ -s /tmp/root.hints.tmp ]; then
mv /tmp/root.hints.tmp /var/lib/unbound/root.hints
chown unbound:unbound /var/lib/unbound/root.hints
chmod 644 /var/lib/unbound/root.hints
else
rm -f /tmp/root.hints.tmp
exit 1
fi
# Reload Unbound
systemctl reload unbound
echo "$(date): Update completed"
EOF
sudo chmod +x /usr/local/bin/update-unbound-data.shCreate systemd service:
sudo tee /etc/systemd/system/unbound-update.service > /dev/null <<'EOF'
[Unit]
Description=Update Unbound root hints and trust anchor
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/update-unbound-data.sh
EOFCreate systemd timer:
sudo tee /etc/systemd/system/unbound-update.timer > /dev/null <<'EOF'
[Unit]
Description=Weekly Unbound data update
[Timer]
OnCalendar=weekly
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
EOFEnable the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now unbound-update.timer
# Check timer status
sudo systemctl list-timers unbound-update.timer
# Test manually
sudo systemctl start unbound-update.servicePerformance Tuning for High-Volume Servers
For mail servers or other high-volume DNS use, add these performance optimizations to /etc/unbound/unbound.conf.d/local.conf:
server:
# Increase threads (set to number of CPU cores)
num-threads: 4
# Larger caches for high volume
msg-cache-size: 128m
rrset-cache-size: 256m
# Serve expired entries while fetching fresh data
serve-expired: yes
serve-expired-ttl: 3600
# Faster timeout for unresponsive servers
jostle-timeout: 200
# Increase socket buffer (prevents dropped packets under load)
so-rcvbuf: 1mAfter making changes:
sudo unbound-checkconf
sudo systemctl restart unboundNote about so-rcvbuf: You may see a warning about the buffer size not being granted. To fix:
# Check current limit
sudo sysctl net.core.rmem_max
# Set higher limit
sudo sysctl -w net.core.rmem_max=1048576
# Make permanent
echo "net.core.rmem_max=1048576" | sudo tee -a /etc/sysctl.conf
sudo sysctl -pTroubleshooting
DNS Not Resolving
# Check Unbound status
sudo systemctl status unbound
# Check for errors
sudo journalctl -u unbound -n 50
# Test direct query
dig @127.0.0.1 example.com
# Check resolv.conf
cat /etc/resolv.conf
# Should show: nameserver 127.0.0.1systemd-resolved Keeps Starting
If systemd-resolved keeps restarting despite being disabled:
# Verify the mask
systemctl status systemd-resolved
# Should show: Loaded: masked
# If not masked, mask it
sudo systemctl mask systemd-resolved
# Check for socket activation
systemctl list-sockets | grep resolved
sudo systemctl mask systemd-resolved.socketPostfix Can’t Resolve DNS
# Check chroot resolv.conf ownership
ls -la /var/spool/postfix/etc/resolv.conf
# Must show: -rw-r--r-- 1 root root
# If wrong ownership, fix it
sudo cp /etc/resolv.conf /var/spool/postfix/etc/resolv.conf
sudo chown root:root /var/spool/postfix/etc/resolv.conf
sudo chmod 644 /var/spool/postfix/etc/resolv.conf
# Restart Postfix
sudo systemctl restart postfix
# Verify
postfix checkVerify Recursive Resolution
Ensure Unbound is doing recursive resolution (not forwarding):
# Should return nothing or only commented lines
sudo grep -r "forward-zone" /etc/unbound/
# Verify root hints is configured
grep "root-hints" /etc/unbound/unbound.conf.d/local.conf
# Test with trace to see full resolution path
dig +trace @127.0.0.1 debian.orgresolv.conf Gets Overwritten (Containers)
If /etc/resolv.conf keeps getting overwritten in containers:
# Verify systemd-networkd configuration exists
cat /etc/systemd/network/10-eth0.network
# Should show: UseDNS=false
# If missing, create it (see Step 5)
# Then restart
sudo systemctl restart systemd-networkd
# Check if DHCP client is overriding
ps aux | grep dhc
# If dhclient is running, it may need configurationIPv6 Considerations for Servers
This configuration disables IPv6 DNS queries (do-ip6: no) by default. Here’s why:
Spamhaus DNSBL IPv6 Issue:
- Spamhaus and some DNSBL providers have issues with IPv6 DNS queries
- They may falsely flag your resolver as an “open resolver”
- This can block your mail server from using their DNSBL services
- Since DNSBL queries are essential for spam filtering, IPv6 is disabled
Only enable IPv6 if:
- You’ve verified all your DNSBL providers support IPv6 queries properly
- You’ve tested and confirmed no false “open resolver” detections
- You have a specific requirement for IPv6 DNS resolution
Important: This is about DNS queries Unbound makes, not about your mail server sending/receiving mail over IPv6 (which is configured separately in Postfix).
To enable IPv6 later:
sudo vi /etc/unbound/unbound.conf.d/local.conf
# Change: do-ip6: no to do-ip6: yes
sudo unbound-checkconf
sudo systemctl restart unboundSummary
You now have a fully functional recursive DNS resolver configured properly for server environments:
What You’ve Accomplished:
- Direct recursive DNS resolution from root servers
- No rate limiting from public DNS providers
- Privacy-focused (no external query logging)
- DNSSEC validation for security
- Excellent caching performance
- Unlimited DNSBL queries for mail servers
- Automatic updates via systemd timers
- Postfix-safe configuration (correct ownership)
Why This Configuration:
This guide uses a clean, direct approach: Unbound handles all DNS resolution without systemd-resolved adding an unnecessary layer. For servers—especially mail servers—this is the only safe configuration:
- Simpler: One DNS resolver, not two
- Reliable: No intermittent ownership issues
- Safe for Postfix: Maintains correct file ownership in chroot
- Better performance: No additional hop through systemd-resolved
- Easier to troubleshoot: Clear, direct resolution path
Your server can now perform DNS resolution independently, with full control over caching, validation, and query volume. For mail servers, this means unlimited DNSBL lookups without rate limiting or ownership problems.
