DE

PowerDNS Hidden Primary: How DNS Runs in My Homelab

I run my own DNS, without external dependencies. A look at my production architecture: hidden primary in the homelab, two external slaves, and WireGuard as the transfer path.

Share:

Contents

DNS is usually noticed during an incident only when everything else is already red. Mail is hanging. An internal service can't find its name. Or the delegation has been pointing to old data for hours — and nobody noticed because it was still working yesterday.

I do DNS, infrastructure, platforms, and networking professionally — that's my daily work, not a weekend project. That's exactly why I had no desire to leave my zones with some external provider I'd have to contact first or deal with via tickets when serial problems arise. I want to see for myself what the SOA is doing. I want to know whether the NOTIFY went out. And if a PTR record is wrong, I fix it myself — without a ticket and without a sluggish black-box dashboard.

In My Homelab It Looks Like This

A hidden primary on Ubuntu 24.04, two external slaves for the public-facing side, and WireGuard in between. The primary writes, the slaves respond to the outside. NOTIFY and zone transfers run exclusively through the tunnel — I wanted to avoid open AXFR ports on the internet under all circumstances.

Homelab (Hidden Primary) wg0 Slave 1 ──► public NS
LAN + wg0 ─────────► Slave 2 ──► public NS

Hidden here means quite pragmatically: My primary isn't in any public delegation. The internet sees exclusively the two slaves. Accidentally binding it to 0.0.0.0:53 would be its own self-inflicted incident for me — suddenly it would be authoritatively visible, even though it's only supposed to serve as a write source.

By this point, theory and operations diverge for me. On the   host, I strictly separate LAN and wg0. SSH, PowerAdmin, and the usual admin stuff run over LAN. The authoritative DNS and master/slave traffic must exclusively flow through WireGuard. The public endpoints of the slaves I only need for the tunnel handshake, not for AXFR transfers over the open network.

What Happens When a Zone Changes

When I change a record — for me mostly via   PowerAdmin, sometimes directly via the API, very rarely with pdnsutil while debugging — the entry lands in SQLite. The SOA serial increments, the primary sends a NOTIFY through the tunnel, and the slaves pull the data via AXFR or IXFR. Sounds simple.

It usually is — until the moment it isn't. The UI dutifully reported saved, but the serial on the public nameservers stays at the old value. Or the tunnel is up visually, but the NOTIFY never arrives at the slave because the AllowedIPs don't match on one side. Handshake looks good, but DNS is stale anyway.

pdnsutil show-zone example.com
dig @<public-ns-slave1> example.com SOA +short
dig @<public-ns-slave2> example.com SOA +short

In moments like these, I've gotten into the habit of not tinkering with the record itself at all. If the serials diverge, I don't debug the record. I debug replication.

SQLite, Config, WireGuard

For the backend I use gsqlite3 — a simple file at /var/lib/powerdns/pdns.sqlite3. That's exactly why I didn't want to run MariaDB in the homelab: I simply don't have the energy to patch and back up a second service for a minimal write rate that SQLite handles in passing. The catch with this approach is well known: SQLite absolutely hates parallel writers. Letting PowerAdmin and pdnsutil loose on the same zone at the same time is the fastest way to annoy yourself.

# /etc/powerdns/pdns.d/gsqlite3.conf
launch=gsqlite3
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3

Here are the key lines from my pdns.conf — you'll obviously need to replace the placeholders with your actual WireGuard addresses:

primary=yes
local-address=<primary-wg-ip>
allow-axfr-ips=<slave1-wg-ip>,<slave2-wg-ip>
allow-notify-from=<slave1-wg-ip>,<slave2-wg-ip>
also-notify=<slave1-wg-ip>,<slave2-wg-ip>
only-notify=<slave1-wg-ip>,<slave2-wg-ip>
api=yes
webserver=yes
webserver-address=0.0.0.0
webserver-port=8081
webserver-allow-from=127.0.0.1,<admin-net>/<prefix>

Having local-address firmly on the WireGuard address is for me the most important security lever: port 53 only listens there, not on the LAN, not on everything. A bind setting is in doubt always preferable to a firewall rule that you might accidentally overwrite.

allow-axfr-ips and only-notify point to exactly these two slaves — nothing more. I primarily need the API for PowerAdmin; the API key is kept separate, controlled via webserver-allow-from. Stricter still would be binding the webserver-address directly to the WG IP and closing port 8081 via firewall — that's honestly still on my todo list.

WireGuard for me isn't a comfort VPN, but a hard transport channel. Without a tunnel there's no transfer, and without a transfer the slaves become stale. Then the public world answers queries with yesterday's data.

Previously, I had to evict systemd-resolved from port 53 on Ubuntu 24.04 — the usual pain.   PowerDNS itself comes cleanly from the official auth-49 repo via the pdns-server package with pdns-backend-sqlite3. Some standard tinkering, but not the part that gave me headaches during setup.

Everyday Life

I change records in daily life almost exclusively through PowerAdmin. I really only use the CLI with pdnsutil for quick cross-checks: list-all-zones, check-zone, or the occasional forceful increase-serial when I'm not entirely sure whether the UI properly carried the serial forward.

Since then, I've made this my standard procedure: After every change, I immediately check the SOA on the primary and compare it via dig with both slaves. That sounds terribly pedantic. It is. But it saves me exactly those evenings where you sit helplessly in front of the monitor wondering why half the world is still resolving old records.

When things go wrong again, my troubleshooting order is absolutely fixed: First wg show, then journalctl -u pdns, and finally the serial comparison. It's almost always the tunnel, sometimes simply the serial, and only in the rarest cases the record itself. You're most likely to catch SQLite locks when you're clicking wildly in the UI while simultaneously tweaking the zone on the CLI. And whether the primary might be listening unawares and undesired on LAN port 53, I can see with a quick ss -lntup | grep :53 faster than in any well-maintained documentation.

The backup strategy is simple: The SQLite file gets copied away. Either I briefly stop pdns for that, or use the online .backup. Updates come via the PowerDNS repo, followed by a quick restart and the obligatory serial check against the slaves.

A few iron principles I've adopted over the years: No recursor on the same host. No MariaDB without reason. No open AXFR sources on the network. Authoritative and resolver are strictly separate stories for me.

DNS isn't glamorous and never will be. But anyone who doesn't read their own zones ends up just chasing symptoms. That's why I always start debugging with the SOA and the tunnel — and definitely not in the user interface.

Sources: PowerDNS Authoritative Docs PowerDNS Master/Slave PowerDNS Repositories PowerDNS API
Tags: #PowerDNS #Ubuntu 24.04 #SQLite #WireGuard #DNS #Homelab #PowerAdmin #Hidden Primary #Master-Slave

Related Articles