Kuuko / Build it yourself

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 on 127.0.0.1 port 5300, only to the server itself. !override replaces 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:

  1. DNSSEC works:

    kdig @127.0.0.1 -p 5353 +dnssec <your-domain> SOA

    shows status: NOERROR and the flag ad.

  2. Broken signatures are refused:

    kdig @127.0.0.1 -p 5353 dnssec-failed.org A

    shows status: SERVFAIL. That domain is broken on purpose, for this test.

  3. Encrypted DNS answers at your private path:

    kdig @127.0.0.1 -p 443 +https=/dns-query/<your-private-path> example.com A

    shows status: NOERROR.

  4. Any other path gets nothing:

    kdig @127.0.0.1 -p 443 +https=/dns-query/wrong example.com A

    gives no DNS answer: kdig's HTTP line shows status: 404. The same happens for /dns-query alone, 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.

  5. 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 with sudo ss -tlnup | grep -E "dnsdist|pdns": Recursor only on 127.0.0.1:5300, dnsdist on :443 and 127.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