Configuring Unbound as a Recursive DNS Resolver with systemd-resolved
When operating a mail server such as Postfix, DNS lookups play a crucial role in message delivery and spam detection. Postfix often performs frequent DNSBL (DNS-based Blacklist) queries to identify spam sources, and relying on public DNS resolvers for these lookups can quickly become a problem. Many public DNS providers, including Google and Cloudflare, intentionally rate-limit or block DNSBL queries, which can lead to slow mail delivery, lookup failures, or incomplete spam checks.
A more reliable and privacy-friendly approach is to run your own recursive DNS resolver using Unbound. This gives you full control over DNS resolution, validation, and caching, independent of any external service. With Unbound configured to perform full recursive resolution using the DNS root servers, your mail server can handle all DNS lookups locally, improving performance, avoiding rate limits, and ensuring DNSSEC validation for security and integrity.
This guide walks you through setting up Unbound as a local recursive resolver on Debian 12 or 13, fully integrated with Postfix and compatible with systemd-resolved, so your server remains cleanly aligned with Debian’s modern networking stack.
Architecture:
Applications → systemd-resolved (127.0.0.53:53) → Unbound (127.0.0.1:5335) → Root/Authoritative Nameservers
Why Recursive DNS?
Key benefits:
- No rate limiting – Direct queries to authoritative nameservers
- DNS independence – No reliance on third-party DNS services (Google, Cloudflare)
- Privacy – No external logging of your DNS queries
- Performance – Excellent caching for repeated queries
- Essential for mail servers – DNSBL spam filtering requires high query volumes
Prerequisites
- Debian 12/13 or similar systemd-based Linux distribution
- Root or sudo access
- Basic understanding of DNS
Installation
sudo apt update
sudo apt install unbound unbound-anchor curl dnsutils -y
Packages installed:
unbound
– The DNS resolverunbound-anchor
– Tool for updating DNSSEC trust anchorscurl
– Required for downloading root hintsdnsutils
– Providesdig
for testing DNS queries
Configuration
Step 1: Download Root Hints
Root hints enable Unbound to perform recursive resolution starting from the 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.hints
Step 2: Configure Unbound
Create the local configuration file:
sudo vi /etc/unbound/unbound.conf.d/local.conf
Add this configuration:
server:
# Network
interface: 127.0.0.1
interface: ::1
port: 5335
do-ip6: yes
# 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: yes
Important: This configuration uses port 5335 to avoid conflicts with systemd-resolved on port 53.
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.conf
Step 3: 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
Step 4: Configure systemd-resolved
Edit the systemd-resolved configuration:
sudo vi /etc/systemd/resolved.conf
Set these values:
[Resolve]
DNS=127.0.0.1:5335
Domains=~.
Cache=no
DNSStubListener=yes
Step 5: Handle DHCP DNS Override
In containers or systems using DHCP, you must prevent interface-specific DNS servers from overriding your configuration.
Create network configuration:
sudo vi /etc/systemd/network/10-eth0.network
[Match]
Name=eth0
[Network]
DHCP=yes
[DHCP]
UseDNS=false
UseDomains=false
Restart services:
sudo systemctl restart systemd-networkd
sudo systemctl restart systemd-resolved
Verification
Check Configuration
resolvectl status
Expected output:
- Global DNS Servers:
127.0.0.1:5335
- Interface DNS Servers:
127.0.0.1:5335
or empty
Test DNS Resolution
# Basic test
resolvectl query google.com
# Test with dig
dig @127.0.0.1 -p 5335 debian.org
Monitor Unbound Activity
With log-queries: yes
enabled in your configuration, you can see all DNS queries:
# Watch logs in real-time
sudo journalctl -u unbound -f
# Make test queries in another terminal
resolvectl query cloudflare.com
You should see lines like:
info: 127.0.0.1 cloudflare.com. A IN
info: 127.0.0.1 cloudflare.com. AAAA IN
This shows queries coming from 127.0.0.1 (systemd-resolved) being processed by Unbound.
Disable query logging after testing:
For privacy and to reduce log volume in production:
sudo vi /etc/unbound/unbound.conf.d/local.conf
# Change: log-queries: yes to log-queries: no
sudo systemctl restart unbound
Test Caching
# First query (slower - fetches from authoritative nameserver)
time dig @127.0.0.1 -p 5335 wikipedia.org
# Second query (faster - served from cache)
time dig @127.0.0.1 -p 5335 wikipedia.org
The second query should be significantly faster.
Test DNSSEC Validation
Verify that DNSSEC validation is working properly:
# Test a domain with valid DNSSEC - should resolve successfully
dig +dnssec @127.0.0.1 -p 5335 sigok.verteiltesysteme.net
# Test a domain with intentionally broken DNSSEC - should fail (SERVFAIL)
dig +dnssec @127.0.0.1 -p 5335 sigfail.verteiltesysteme.net
# Check a common signed domain
dig +dnssec @127.0.0.1 -p 5335 debian.org
Expected results:
sigok.verteiltesysteme.net
– Should return an answer withad
flag (authenticated data)sigfail.verteiltesysteme.net
– Should returnSERVFAIL
(DNSSEC validation failed)- If both work as expected, DNSSEC validation is functioning correctly
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.sh
Create 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
EOF
Create 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
EOF
Enable 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.service
Performance Tuning
For High-Volume Environments (Mail Servers)
If running a mail server with DNSBL queries or other high-volume DNS use:
server:
# Increase threads
num-threads: 4
# Larger caches
msg-cache-size: 128m
rrset-cache-size: 256m
# Optimize for performance
serve-expired: yes
serve-expired-ttl: 3600
jostle-timeout: 200
Troubleshooting
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 -p 5335 example.com
# Check systemd-resolved
resolvectl status
Interface DNS Override Issue
If resolvectl status
shows interface-specific DNS servers (like 10.0.3.1
):
# Manually set DNS for interface
sudo resolvectl dns eth0 127.0.0.1:5335
sudo resolvectl domain eth0 "~."
# Verify
resolvectl status
Then make permanent using the systemd-networkd configuration in Step 5.
Verify Recursive Resolution
Ensure no forwarding is configured:
# Should return nothing or commented lines
sudo grep -r "forward-zone" /etc/unbound/
# Verify root hints is set
grep "root-hints" /etc/unbound/unbound.conf.d/local.conf
Use with Postfix (Mail Servers)
Postfix automatically uses the system resolver. After configuring Unbound, your DNSBL queries will work without rate limiting.
Verify Postfix DNS configuration:
postconf smtp_host_lookup
Should return:
smtp_host_lookup = dns
This is the default setting and confirms Postfix uses DNS for lookups.
Test DNSBL resolution:
# Test a known-listed IP (127.0.0.2 is standard test)
dig @127.0.0.1 -p 5335 2.0.0.127.zen.spamhaus.org
# Should return 127.0.0.2 or similar listing code
Note: DNSBL queries require reversed IP octets. For IP 203.0.113.50
, query 50.113.0.203.zen.spamhaus.org