PowerDNS Recursor with dnsdist
Overview
This guide shows how to deploy and configure PowerDNS Recursor with dnsdist on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. The appliance runs two daemons built from open source projects by PowerDNS.COM BV.
PowerDNS Recursor 5.4 is the recursive resolver. It answers queries on behalf of your clients by walking the DNS hierarchy from the root servers, following delegations, validating DNSSEC signatures and caching the results. dnsdist 2.1 is the DNS load balancer and TLS terminator in front of it. It provides the encrypted transports, the client access control list and live traffic visibility.
Both are installed from the official PowerDNS apt repositories (repo.powerdns.com, branches noble-rec-54 and noble-dnsdist-21), pinned above the distribution packages. Unattended security upgrades keep the appliance patched on your running VM.
How the two daemons fit together
your clients ──▶ plain DNS :53 (udp + tcp) ──┐ your clients ──▶ DNS-over-TLS :853 ─┼──▶ dnsdist ──▶ PowerDNS Recursor your clients ──▶ DNS-over-HTTPS :443/dns-query ─┘ 0.0.0.0 127.0.0.1:5300dnsdist is the only daemon on a routable address. The Recursor is bound to loopback and accepts queries only from 127.0.0.0/8. The resolver itself stays unreachable from the network, even if someone later widens the dnsdist access list by mistake. This is deliberate defence in depth.
The appliance is never an open resolver. An internet-facing resolver that answers anyone is a DNS amplification hazard. Attackers spoof a victim's source address and send small queries, and your instance floods the victim with large responses. This image ships with the dnsdist ACL restricted to loopback and the RFC1918 private ranges only, and it refuses queries from arbitrary internet sources. Opening the appliance to your own client subnets is a single documented change (Step 8). Never add 0.0.0.0/0.
No default credentials. This image never ships a fixed key, password or certificate. On the first boot of every VM, a one-shot service generates a fresh dnsdist console key, dnsdist dashboard password and REST API key. It also generates a fresh PowerDNS Recursor REST API key and webserver password, and a fresh TLS key pair for the encrypted transports. All of it is written to /root/powerdns-recursor-dnsdist-credentials.txt (mode 0600, root only). Both daemons refuse to start until that provisioning has run. A start-time guard also blocks them from running on a published upstream example credential.
What you get:
- PowerDNS Recursor 5.4.5 and dnsdist 2.1.1 from the official PowerDNS packages, each running under systemd as its own unprivileged user (
pdns-recursor.service,dnsdist.service). - Three client transports from one instance: plain DNS on port 53 (UDP and TCP), DNS over TLS on port 853, and DNS over HTTPS on port 443 at
/dns-query. - A dnsdist access control list restricted to loopback and RFC1918 private ranges, so the instance is never an open resolver.
- DNSSEC validation of signed answers, with the
systemd-resolvedstub listener disabled so dnsdist owns port 53 cleanly. - Two authenticated web dashboards, both bound to loopback: dnsdist on
127.0.0.1:8083and the Recursor on127.0.0.1:8082. dig,kdigandcurlpreinstalled, so you can verify all three transports without installing anything.
Prerequisites
- An active Azure subscription, an SSH public key, and a virtual network and subnet in the target region.
- A subscription to the PowerDNS Recursor with dnsdist listing on Azure Marketplace.
- Network Security Group rules that allow TCP 22 (administration) and, from the client networks that will use the resolver, UDP 53 and TCP 53 (plain DNS), TCP 853 (DNS over TLS) and TCP 443 (DNS over HTTPS).
- Recommended virtual machine size:
Standard_B2s(2 vCPU, 4 GB RAM) for a small resolver. UseStandard_D2s_v5or larger for resolvers that serve heavy query volumes.
Deploy from the Azure Portal
Search Marketplace for PowerDNS Recursor with dnsdist, select the SecureAnyCloud publisher, and click Create.
Configure the Network Security Group to allow the transports your clients will use, from your client networks only. Use UDP 53 and TCP 53 for plain DNS, TCP 853 for DNS over TLS and TCP 443 for DNS over HTTPS. Also allow TCP 22 for administration. Both management dashboards stay on loopback and are never exposed.
Deploy from the Azure CLI
RG="dns-prod"; LOCATION="eastus"; VM_NAME="resolver1" IMAGE_URN="sac1647283583153:powerdns-recursor-dnsdist:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:powerdns-recursor-dnsdist:default:latest
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$IMAGE_URN" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku StandardConnect and retrieve the per-instance credentials
Every credential on this appliance is unique to your VM. The first boot generated them. Connect over SSH, then print the credentials file.
sudo cat /root/powerdns-recursor-dnsdist-credentials.txtThe file lists the dnsdist console key, the dnsdist dashboard password and REST API key, the Recursor REST API key and webserver password, the TLS certificate fingerprint, and this instance's addresses. Store them somewhere safe.
Confirm both daemons and the listener topology
Check that both services are running, then confirm which ports are public and which are loopback-only.
sudo systemctl is-active dnsdist.service pdns-recursor.serviceExpected output:
active activeNow confirm the topology. dnsdist should own the three public DNS ports. The Recursor and every management surface must be on 127.0.0.1 only.
echo "PUBLIC (dnsdist front door):"
sudo ss -tulnH | awk '{print $1"\t"$4"\t"$5}' | grep -E '(^|\s)(0\.0\.0\.0|\[::\]):(53|443|853)\b' | sort -u -k2
echo "LOOPBACK ONLY (recursor + management):"
sudo ss -tulnH | awk '{print $1"\t"$4"\t"$5}' | grep -E '127\.0\.0\.1:(5300|8082|8083|5199)' | sort -u -k20.0.0.0:53, 0.0.0.0:853 and 0.0.0.0:443 belong to dnsdist. 127.0.0.1:5300 is the Recursor, 127.0.0.1:8082 is its REST API, 127.0.0.1:8083 is the dnsdist dashboard and 127.0.0.1:5199 is the dnsdist console. None of these is reachable from the network.
Confirm the versions you are running:
pdns_recursor --version 2>&1 | head -1 dnsdist --version 2>&1 | head -1Query the resolver over all three transports
These commands query the appliance on its own routable address, exactly as a client on your VNet would. RESOLVER_IP is resolved from the instance itself, so you can paste each block as-is. From a client machine, set it to this VM's address instead.
Plain DNS on port 53:
RESOLVER_IP="$(hostname -I | awk '{print $1}')" dig +short @"$RESOLVER_IP" A example.comDNS over TLS on port 853. The appliance's certificate is self-signed and unique to your VM. Point kdig at it with +tls-ca, or install your own certificate (Step 9):
RESOLVER_IP="$(hostname -I | awk '{print $1}')" kdig +tls +tls-hostname="$RESOLVER_IP" +tls-ca=/etc/dnsdist/tls/dnsdist.crt \
+short @"$RESOLVER_IP" -p 853 A example.comDNS over HTTPS on port 443 at the /dns-query path. The query below is the RFC 8484 wire-format request for example.com A. The response is binary DNS, so the command reads the answer count from the DNS header instead of printing the response.
RESOLVER_IP="$(hostname -I | awk '{print $1}')" DNS_Q="AAABAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE"
CODE=$(curl -sk -o /tmp/doh.bin -w '%{http_code}' -m 15 \
-H 'accept: application/dns-message' \
"https://${RESOLVER_IP}/dns-query?dns=${DNS_Q}")
echo "HTTP $CODE answer records: $(od -An -tu1 -j6 -N2 /tmp/doh.bin | awk '{print $1*256+$2}')" rm -f /tmp/doh.binA healthy appliance returns HTTP 200 with one or more answer records. An answer count of 0 means the query reached dnsdist but resolution failed. In that case, check outbound UDP 53 from the VM to the internet.
You can also confirm that dnsdist sees the Recursor backend as healthy. Use the per-instance console key:
CONSOLE_KEY=$(sudo grep '^dnsdist.console.key=' /root/powerdns-recursor-dnsdist-credentials.txt | cut -d= -f2-) printf "setKey('%s')\n" "$CONSOLE_KEY" | sudo tee /run/ck.conf >/dev/null
sudo chmod 600 /run/ck.conf
sudo dnsdist -C /run/ck.conf -c 127.0.0.1:5199 -e 'showServers()'
sudo rm -f /run/ck.confThe powerdns-recursor backend at 127.0.0.1:5300 should report state up.
Open the dnsdist dashboard
The dnsdist dashboard shows live queries per second, per-transport latency, backend health and the active access control list. It is bound to loopback, so open an SSH tunnel from your workstation:
ssh -L 8083:127.0.0.1:8083 azureuser@YOUR_VM_ADDRESSThen open http://127.0.0.1:8083/ and sign in with user admin and the dnsdist.webserver.password value from the credentials file.
The header line summarises the appliance at a glance. It shows total questions answered, current QPS, ACL drops, average response time per transport (UDP, TCP, DoT, DoH), the listening ports and the exact ACL in force.
Open the PowerDNS Recursor dashboard
The Recursor has its own status dashboard on 127.0.0.1:8082. It shows resolver-side metrics: query rate, cache hit rate, CPU, and rings of the most-queried domains and the busiest clients. Tunnel to it the same way:
ssh -L 8082:127.0.0.1:8082 azureuser@YOUR_VM_ADDRESSOpen http://127.0.0.1:8082/ and sign in with user admin and the powerdns.recursor.webserver.password value.
The Queries and Remotes rings are the quickest way to see what your clients are resolving. Every query arrives through dnsdist, so the only remote you will see is 127.0.0.1. This is the front door working as designed.
Allow your own clients (the access control list)
Out of the box, the appliance answers loopback and the RFC1918 private ranges only. View the shipped ACL:
grep -A 11 '^setACL' /etc/dnsdist/dnsdist.confIf your clients are already inside 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16, which covers most Azure VNets, there is nothing to change. Confirm your VNet range first:
hostname -ITo add a client range that is not already covered, edit the setACL({...}) list in /etc/dnsdist/dnsdist.conf and add your subnet as a new quoted entry:
setACL({ '127.0.0.0/8', '10.0.0.0/8', ... '203.0.113.0/24', <-- your client subnet })Then apply it:
sudo systemctl reload dnsdistNever add 0.0.0.0/0 or ::/0. That turns this instance into an open resolver, an internet-wide DNS amplification reflector. Attackers use such reflectors to DDoS third parties with your bandwidth and your IP address, and your provider may null-route the instance. The appliance refuses to start if it detects a default route in the ACL. Always restrict the ACL to the client networks you operate.
Restrict access at the Network Security Group as well as in dnsdist. Two independent layers are the right posture for anything that answers DNS.
Replace the TLS certificate (optional)
The DoT and DoH certificate is self-signed and unique to your VM. Clients must either trust /etc/dnsdist/tls/dnsdist.crt or pin its fingerprint:
sudo openssl x509 -in /etc/dnsdist/tls/dnsdist.crt -noout -fingerprint -sha256For clients that expect a publicly trusted certificate, replace the key pair with your own from your CA or an ACME client. Keep the same paths and ownership, then reload:
sudo install -o root -g _dnsdist -m 0644 /path/to/fullchain.pem /etc/dnsdist/tls/dnsdist.crt
sudo install -o root -g _dnsdist -m 0640 /path/to/privkey.pem /etc/dnsdist/tls/dnsdist.key
sudo systemctl reload dnsdistSecurity posture
- No shipped credentials. Every key, password and certificate on this appliance was generated on this VM's first boot. Nothing is shared with any other SecureAnyCloud instance.
- Both daemons fail closed.
dnsdist.serviceandpdns-recursor.serviceeach carry aConditionPathExistsgate on their per-VM secrets. Each also has a start-time guard that refuses to run if a published upstream example credential is in effect, if the console key is too weak, if the ACL contains a default route, or if a management surface is not loopback-bound. You can run that guard yourself at any time:
sudo /usr/local/sbin/sac-dns-secret-guard.sh && echo "secrets in effect are unique to this VM"- Management surfaces are loopback-only. Neither dashboard, neither REST API, nor the dnsdist console is reachable from the network. Reach them over SSH tunnels, as in Steps 6 and 7.
- Secret files are root-owned. The credentials file is
0600 root:root. The dnsdist and Recursor secret drop-ins and the TLS private key are0640, readable only by the daemon that needs them.
sudo ls -l /root/powerdns-recursor-dnsdist-credentials.txt /etc/dnsdist/tls/dnsdist.keyOngoing operations
Reload dnsdist after any configuration change. The reload re-reads the configuration without dropping service.
sudo systemctl reload dnsdistCheck the logs for either daemon.
sudo journalctl -u dnsdist.service --no-pager | tail -20sudo journalctl -u pdns-recursor.service --no-pager | tail -20Unattended security upgrades are enabled, so both packages keep receiving updates from the official PowerDNS repositories on your running VM.
Support
SecureAnyCloud provides 24/7 technical support for this image by email (sales@secureanycloud.com) and live chat, with a one hour average response time for critical issues. We help with client access control, replacing the self-signed certificate, DNSSEC validation, forwarding zones, Response Policy Zones for DNS firewalling, dnsdist rules for rate limiting and filtering, REST API automation, cache and performance tuning, and PowerDNS version upgrades.
All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.
Deploy on Azure
Launch PowerDNS Recursor 5.4 with dnsdist 2.1 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com