Comprehensive security and hardening guidance for PowerDNS Recursor with configuration examples. The Recursor handles recursive lookups and can be abused if operated as an open resolver.
Critical: Never allow recursion from untrusted networks without proper controls.
# Allow queries only from trusted networks (recommended)
allow-from=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
# For public resolver (use with Lua rate limiting!)
# allow-from=0.0.0.0/0,::/0
# Maximum subnet size for allow-from
allow-from-subnet-size=24
# Bind to specific interfaces only
local-address=10.0.0.1,::1
# Or for internal-only resolver
local-address=127.0.0.1
# Enable DNSSEC validation (recommended for all production resolvers)
dnssec=validate
# Other options:
# dnssec=process - Process DNSSEC but don't require valid results
# dnssec=log-fail - Log validation failures only
# dnssec=off - Disable DNSSEC (not recommended)
# Test DNSSEC validation is working
dig @localhost dnssec-failed.org
# Should return SERVFAIL if validation is working correctly
# Check validation status
dig @localhost example.com +dnssec
# Should show AD (Authenticated Data) flag if validated
# Use built-in trust anchors (default, recommended)
# Custom trust anchor file (optional)
# trustanchor.file=/etc/powerdns/trustanchor.file
# Negative trust anchors for broken domains
# negtrustanchor=broken-dnssec.example.com
# Basic protections against abuse
max-mthreads=2000
max-qperq=50
max-tcp-clients=128
max-tcp-queries-per-connection=100
Create /etc/powerdns/lua/ratelimit.lua:
-- Rate limiting configuration
local client_rates = {
["10.0.0.0/8"] = 1000, -- Internal network: 1000 qps
["192.168.0.0/16"] = 1000, -- Internal network: 1000 qps
["172.16.0.0/12"] = 1000, -- Internal network: 1000 qps
["0.0.0.0/0"] = 50, -- Default: 50 qps
}
-- Simple rate limiting table
local rates = {}
local window = 1 -- 1 second window
function maintenance()
-- Clean old entries
for ip, data in pairs(rates) do
if os.time() - data.timestamp > window then
rates[ip] = nil
end
end
end
function preresolve(dq)
local client_ip = dq.remoteaddr
-- Get rate limit for this client
local limit = client_rates["0.0.0.0/0"] -- Default
for network, rate in pairs(client_rates) do
-- Simple IP matching (use proper CIDR matching in production)
if client_ip:match("^10%.") then
limit = client_rates["10.0.0.0/8"]
elseif client_ip:match("^192%.168%.") then
limit = client_rates["192.168.0.0/16"]
end
end
-- Check rate
if not rates[client_ip] then
rates[client_ip] = {count = 0, timestamp = os.time()}
end
rates[client_ip].count = rates[client_ip].count + 1
if rates[client_ip].count > limit then
pdnslog("Rate limit exceeded for " .. client_ip)
dq:addAnswer(pdns.A, "0.0.0.0")
return true
end
return false
end
Enable Lua scripting in recursor.conf:
lua-config-file=/etc/powerdns/lua/ratelimit.lua
# RPZ file path
rpz-file=/etc/powerdns/rpz/blocklist.txt
# RPZ actions
rpz-action-override=Drop
rpz-ip-override=0.0.0.0
rpz-nxdomain-action=NXDOMAIN
Create /etc/powerdns/rpz/blocklist.txt:
; Malware domains
malware-site.com CNAME .
bad-domain.net CNAME .
; Ad servers
ads.example.com CNAME .
ads-tracker.example.com CNAME .
; Tracking domains
tracker.example.org CNAME .
analytics-tracking.example.net CNAME .
; Phishing domains
phishing-example.com CNAME .
; IP-based blocking
192.0.2.1 CNAME .
203.0.113.50 CNAME .
#!/bin/bash
# /usr/local/bin/update-rpz.sh
RPZ_URL="https://threatfeed.example.com/blocklist.txt"
RPZ_FILE="/etc/powerdns/rpz/blocklist.txt"
curl -fsSL "$RPZ_URL" -o "$RPZ_FILE"
systemctl reload pdns-recursor
# Add to crontab
0 */6 * * * /usr/local/bin/update-rpz.sh
# Enable web server for API and statistics
webserver=yes
# Bind to localhost only (recommended)
webserver-address=127.0.0.1
# Or bind to management network only
# webserver-address=10.0.0.1
webserver-port=8082
webserver-password=your_very_strong_password_here
# Restrict API access to specific networks
webserver-allow-from=127.0.0.1,10.0.0.0/8
# Use API key instead of password (more secure for automation)
api-key=your_generated_api_key_here
# Generate strong API key
# openssl rand -hex 32
# Allow API from management network only
sudo ufw allow from 10.0.0.0/8 to any port 8082 proto tcp
sudo ufw allow from 127.0.0.1 to any port 8082 proto tcp
# Or with iptables
sudo iptables -A INPUT -s 10.0.0.0/8 -p tcp --dport 8082 -j ACCEPT
sudo iptables -A INPUT -s 127.0.0.1 -p tcp --dport 8082 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8082 -j DROP
# Log level (0-9, higher = more verbose)
loglevel=6
# Log to syslog
logging-facility=1
# Log common troubles
log-common-troubles=yes
# Log RPZ actions
log-rpzs=yes
# Log spoofing attempts
log-spoofing=yes
# Or log to file
# log-file=/var/log/powerdns/recursor.log
# Reduce logging for privacy (GDPR compliance)
quiet=yes
# Don't log client IP addresses
# (requires custom Lua scripting)
# Monitor for suspicious activity
journalctl -u pdns-recursor -f | grep -E "(spoofing|RPZ|rate limit)"
# Check for validation failures
journalctl -u pdns-recursor | grep -i "dnssec.*fail"
# Run as dedicated user (default)
setuid=pdns-recursor
setgid=pdns-recursor
# Daemon mode
daemon=yes
# Runtime directory
socket-dir=/var/run/powerdns
# Secure configuration files
sudo chmod 640 /etc/powerdns/recursor.conf
sudo chown pdns-recursor:pdns-recursor /etc/powerdns/recursor.conf
# Secure RPZ files
sudo chmod 640 /etc/powerdns/rpz/*.txt
sudo chown pdns-recursor:pdns-recursor /etc/powerdns/rpz/
# Secure Lua scripts
sudo chmod 640 /etc/powerdns/lua/*.lua
sudo chown pdns-recursor:pdns-recursor /etc/powerdns/lua/
services:
powerdns-recursor:
image: powerdns/pdns-recursor-53:5.3.5
# ... other settings ...
# Security hardening
read_only: true
tmpfs:
- /tmp
- /var/run
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
user: "104:108" # pdns-recursor:pdns-recursor UIDs
security_opt:
- no-new-privileges:true
PowerDNS Recursor doesn’t directly support DoT/DoH. Use dnsdist as frontend:
-- dnsdist configuration
addLocal("0.0.0.0:853", {tls="tls-cert.pem,tls-key.pem"})
addLocal("0.0.0.0:443", {tls="tls-cert.pem,tls-key.pem"})
newServer({address="127.0.0.1:53", pool="recursor"})
# Force TCP for upstream queries (enables TLS via dnsdist)
# (configure in dnsdist, not recursor directly)
allow-from)# Temporarily block suspicious IP
echo "allow-from=0.0.0.0/0,::/0,!192.0.2.1" >> /etc/powerdns/recursor.conf
systemctl reload pdns-recursor
# Enable verbose logging temporarily
pdns_control recursor set loglevel=9
# Check current statistics
curl -H "X-API-Key: your_password" http://localhost:8082/api/v1/servers/localhost/statistics
# Enable detailed logging for audit
loglevel=7
log-common-troubles=yes
log-rpzs=yes
# Forward logs to SIEM
# (configure via syslog)
Questions? Find all contact information on our contact page.