| |

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 resolver
  • unbound-anchor – Tool for updating DNSSEC trust anchors
  • curl – Required for downloading root hints
  • dnsutils – Provides dig 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 with ad flag (authenticated data)
  • sigfail.verteiltesysteme.net – Should return SERVFAIL (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

Similar Posts