Build it yourself · Part 1Checked: 30 October 2026
Chapter 4: The resolver
Why
This is the heart of it: the program that answers your devices' questions. Two parts work together:
- PowerDNS Recursor looks every name up itself, starting from the root of the internet. It doesn't pass your questions on to Google, Cloudflare or your internet provider. It checks DNSSEC signatures, and it checks every step when one name points to another, so a tracker hidden behind a harmless-looking name still gets caught (chapter 6).
- dnsdist sits in front. It speaks encrypted DNS over HTTPS (DoH) with your devices, and it lets in only requests that carry your private path. Recursor never sees your devices' addresses.
Our choice, and the alternatives
Both are free software from PowerDNS, widely used for public DNS services. We compared eight programs before choosing.
- Blocky with Unbound is a good alternative, and simpler to set up.
- Why not AdGuard Home? It's popular, but it keeps query logs for 90 days by default, which is the opposite of what we want here.
What you need
- The server from chapter 3, and an SSH session to it:
ssh dns@<your-server-ip>. - About 45 minutes (our estimate).
All commands below run on the server.
Steps
1. Add PowerDNS's package source and check its key
Ubuntu's own packages are older. PowerDNS publishes current versions for each Ubuntu release at repo.powerdns.com, one source per program and version. We use dnsdist 2.1 and Recursor 5.4.
First, your Ubuntu release name:
. /etc/os-release; echo $VERSION_CODENAME
Then the key, and the two sources, with that name in place of <codename>:
sudo install -d /etc/apt/keyrings
curl -fsS https://repo.powerdns.com/FD380FBB-pub.asc | sudo tee /etc/apt/keyrings/powerdns.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/powerdns.asc] http://repo.powerdns.com/ubuntu <codename>-rec-54 main" | sudo tee /etc/apt/sources.list.d/pdns-rec.list
echo "deb [signed-by=/etc/apt/keyrings/powerdns.asc] http://repo.powerdns.com/ubuntu <codename>-dnsdist-21 main" | sudo tee /etc/apt/sources.list.d/pdns-dnsdist.list
Use these lines, not the ones on PowerDNS's page. Its instructions write both programs into the same file, so the second overwrites the first, and they name the key file differently. Check on the page that your release name and both versions are listed.
Check the key before you trust it:
gpg --show-keys /etc/apt/keyrings/powerdns.asc
The fingerprint must be:
9FAA A557 7E8F CF62 093D 036C 1B0C 6205 FD38 0FBB
(shown without spaces, as 9FAAA5577E8FCF62093D036C1B0C6205FD380FBB). If it's different, stop and delete the file.
Then tell apt to prefer PowerDNS's packages over Ubuntu's older ones:
printf 'Package: pdns-* dnsdist*\nPin: origin repo.powerdns.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/powerdns
2. Install
sudo apt update
sudo apt install pdns-recursor dnsdist knot-dnsutils
knot-dnsutils gives you kdig, a tool for testing encrypted DNS.
Check the versions: dnsdist --version shows 2.1.x, pdns_recursor --version shows 5.4.x. We ran dnsdist 2.1.3 and Recursor 5.4.8 and 5.4.9.
These versions don't last forever. When PowerDNS releases the next version, the previous one gets only critical fixes for a while, then none. Your sources stay on 2.1 and 5.4 until you change them, and nothing will tell you. Chapter 10 has a yearly check for this.
3. Configure Recursor
Recursor should listen only on the server itself, where dnsdist can reach it and nothing else can.
sudo nano /etc/powerdns/recursor.d/dns.yml
Paste:
incoming:
listen: !override
- 127.0.0.1:5300
allow_from: !override
- 127.0.0.0/8
outgoing:
source_address: !override
- 0.0.0.0
- '::'
recursor:
stats_ringbuffer_entries: 0
dnssec:
validation: validate
logging:
quiet: true
What each line does:
listen,allow_from: Recursor answers only on127.0.0.1port 5300, only to the server itself.!overridereplaces the package's default instead of adding to it.source_address: it asks other servers over both IPv4 and IPv6.stats_ringbuffer_entries: 0: Recursor keeps no list of recent questions for its statistics.validation: validate: answers with broken DNSSEC signatures are refused.quiet: true: no log line for each question.
The block lists come in chapter 6.
4. Choose your private path
Only requests that carry this path get an answer. Make it long and random:
openssl rand -hex 16
Keep the result in your password manager. Below it's written <your-private-path>.
5. Configure dnsdist
sudo nano /etc/dnsdist/dnsdist.conf
Paste, with your private path in place of <your-private-path>:
setSecurityPollSuffix("")
setRingBuffersSize(1000, 1)
setRingBuffersOptions({recordQueries = false, recordResponses = false})
setACL({"0.0.0.0/0", "::/0"})
newServer({address = "127.0.0.1:5300", name = "recursor"})
local CERT = "/etc/dnsdist/tls/fullchain.pem"
local KEY = "/etc/dnsdist/tls/privkey.pem"
local PATH = "/dns-query/<your-private-path>"
addDOHLocal("0.0.0.0:443", CERT, KEY, {PATH}, {exactPathMatching = true})
addDOHLocal("[::]:443", CERT, KEY, {PATH}, {exactPathMatching = true})
addLocal("127.0.0.1:5353")
addAction(MaxQPSIPRule(500, 32, 56, 2000, 300, 60, 1), DropAction())
What each line does:
setSecurityPollSuffix(""): dnsdist doesn't ask PowerDNS's servers whether your version has known security problems. Updates come through apt, for as long as your version is supported (chapter 10).setRingBuffersSize,setRingBuffersOptions: dnsdist normally keeps the most recent questions and answers in memory, with your devices' addresses. This turns that off.setACL: anyone may connect. The private path decides who gets an answer.newServer: hand every question to Recursor on the server itself.addDOHLocal: encrypted DNS over HTTPS on port 443, IPv4 and IPv6, answering only at your exact private path. Any other path gets no DNS answer.addLocal("127.0.0.1:5353"): plain DNS on the server itself only, for your tests.MaxQPSIPRule(500, 32, 56, 2000, 300, 60, 1): if one address (or IPv6 network of /56) sends more than 500 questions a second, with bursts up to 2,000, the extra ones get no answer. This stops anyone flooding your server. The counters live in memory, and an address is forgotten about five minutes after its last question.
Why only DNS over HTTPS, not DNS over TLS (port 853)? DNS over TLS has no path, so it can't check your private path, and anyone could use it. Your iPhone and Mac speak DNS over HTTPS, so you don't need it.
6. A temporary certificate
dnsdist won't start without a certificate. Chapter 5 gets a real one; until then, make a temporary one that lasts 30 days:
sudo install -d -m 0755 /etc/dnsdist/tls
sudo openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 \
-subj "/CN=dns.<your-domain>" -addext "subjectAltName=DNS:dns.<your-domain>" \
-keyout /etc/dnsdist/tls/privkey.pem -out /etc/dnsdist/tls/fullchain.pem
sudo chgrp _dnsdist /etc/dnsdist/tls/privkey.pem
sudo chmod 0640 /etc/dnsdist/tls/privkey.pem
The last two lines let dnsdist, which runs as the user _dnsdist, read the key, and nobody else.
7. Check and start
sudo chgrp _dnsdist /etc/dnsdist/dnsdist.conf
sudo chmod 0640 /etc/dnsdist/dnsdist.conf
sudo dnsdist --check-config
It must end with Configuration '/etc/dnsdist/dnsdist.conf' OK!. Then:
sudo systemctl restart pdns-recursor
sudo systemctl restart dnsdist
systemctl is-active pdns-recursor dnsdist
Both must say active.
How you know it worked
All on the server:
DNSSEC works:
kdig @127.0.0.1 -p 5353 +dnssec <your-domain> SOAshows
status: NOERRORand the flagad.Broken signatures are refused:
kdig @127.0.0.1 -p 5353 dnssec-failed.org Ashows
status: SERVFAIL. That domain is broken on purpose, for this test.Encrypted DNS answers at your private path:
kdig @127.0.0.1 -p 443 +https=/dns-query/<your-private-path> example.com Ashows
status: NOERROR.Any other path gets nothing:
kdig @127.0.0.1 -p 443 +https=/dns-query/wrong example.com Agives no DNS answer: kdig's HTTP line shows
status: 404. The same happens for/dns-queryalone, and for anything added after your path. We saw this in a local test with dnsdist 1.8.3; dnsdist 2.1's code treats paths the same way.Nothing is logged about your questions:
sudo journalctl -u pdns-recursor -u dnsdist --since "-5 min" | grep -ciE "example\.com|dnssec-failed"shows
0.
Pitfalls
- Missing
!override. Without it, Recursor adds your setting to the package's default, and may listen on more addresses than you meant. Check withsudo ss -tlnup | grep -E "dnsdist|pdns": Recursor only on127.0.0.1:5300, dnsdist on:443and127.0.0.1:5353. - The rate limit was too tight at first. We started at 50 questions a second. A household behind one router, or many phones behind one mobile carrier address, counts as one address, and one Mac opening many pages at once already lost answers. 500 a second works for us.
- dnsdist keeps recent questions in memory by default, with addresses, for its console. The two
setRingBuffers…lines turn that off. Don't leave them out.
Time and cost
- About 45 minutes (our estimate).
- No extra cost: both programs are free.
Previous: Chapter 3: A small server in the EU · Next: Chapter 5: Certificates
Text: CC BY-SA 4.0 · Commands and configs: CC0