"Home DNS appliance, part two: Unbound, DNSSEC and real hardening"


The first part of this series got an Orange Pi Zero 3 running Pi-hole and pointed your router's DHCP clients at it. That gets you network-wide ad blocking. It does not get you a hardened, self-verifying DNS stack. This part does: we replace the upstream forwarders with a local recursive resolver, turn on DNSSEC validation, lock down the box itself, and check the result is actually doing what we think it is. Everything here runs fine on the 1 GB Zero 3 — recursion is cheap at household query rates — though we'll note where the hardware ceiling matters.

Why stop forwarding, and why Unbound

A stock Pi-hole install forwards queries to Google, Cloudflare, Quad9 or your ISP. Two consequences. First, your resolver sees every hostname every device on your network touches; forwarding to a public provider means a third party builds that profile instead of you. Second, a forwarded answer arrives with no way for you to verify it — you're trusting whatever the upstream returns.

Unbound, from NLnet Labs, is a full recursive resolver. It starts at the root servers, walks the delegation chain, and (with DNSSEC enabled) validates a cryptographic signature at each step. No single third party sees your query stream, and forged answers get rejected rather than served.

The trade-off, which we'll concede up front: first resolution of a domain is slower than forwarding — tens to a few hundred milliseconds versus a cached upstream response — because Unbound must contact the root, the TLD and the authoritative nameserver in sequence. Unbound's aggressive DNSSEC caching (RFC 8198, enabled by default in recent versions) mitigates much of this for signed zones, and the cache holds answers for their TTL, so warm performance is close to parity. On a 1 GB Zero 3 you should cap the cache at 128 MB (msg-cache-size and rrset-cache-size); the default 4 MB sizing in Pi-hole's bundled Unbound config is fine but conservative.

The Pi-hole + Unbound configuration

The officially documented pairing lives at docs.pi-hole.net/guides/unbound/ and is worth following rather than improvising. In summary: install unbound, configure it to listen only on the loopback interface on port 5335 (so it sits behind Pi-hole rather than duplicating port 53), and set Pi-hole's upstream to a single # entry — 127.0.0.1#5335 — which tells Pi-hole to speak plain DNS over loopback to Unbound.

Key lines in /etc/unbound/unbound.conf.d/pi-hole.conf:

server:
    verbosity: 1
    interface: 127.0.0.1
    port: 5335
    do-ip6: no
    do-udp: yes
    do-tcp: yes
    access-control: 127.0.0.1/32 allow
    hide-identity: yes
    hide-version: yes
    qname-minimisation: yes
    prefetch: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    harden-below-nxdomain: yes
    aggressive-nsec: yes
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

What each of these buys you:

DoH and DoT: the honest position

A recurring question: why not DNS-over-TLS or DNS-over-HTTPS? Our position is that inside your own network, on a validating recursive resolver, plain DNS on loopback is the correct choice. Encryption of DNS between your laptop and your own resolver protects nothing — that traffic never leaves the LAN. DoT/DoH matter on the egress hop; with Unbound, the egress hop is recursive UDP/TCP 53 to authoritative nameservers worldwide, which by design cannot be funnelled through one provider anyway.

The residual risk is on-path observers (your ISP, or anyone between you and the authoritative servers) seeing which root/TLD/authoritative servers you contact. qname minimisation blunts hostname-level exposure but doesn't encrypt it. If that risk matters to you, Unbound supports TLS-forwarded upstreams — the unbound.conf(5) man page documents forward-tls-upstream: and tls-cert-bundle — but note the frank limitation: TLS forwarding means you've re-selected a visible upstream provider, reintroducing the trust you just removed. We run recursion unencrypted and say so. Pick your poison deliberately.

Also be clear about what DNSSEC does not do: it authenticates answers, it does not encrypt them. Anyone sniffing the wire still sees names. Some vendors sell DNSSEC as privacy tech; it isn't.

Firewall and DHCP gotchas

Two failure modes cause most support emails.

DHCP overlap. If Pi-hole's DHCP server is enabled and the router also runs DHCP, you get intermittent, maddening "sometimes no internet" reports. Pick exactly one. Our appliance kit ships with Pi-hole DHCP disabled and the router as sole DHCP server; if you flip Pi-hole's on, disable the router's first, and check radvd/IPv6 router advertisements too — DHCPv6 and RA are separate failure points from DHCPv4.

Firewall. On the Zero 3 (nftables), the minimum set:

table inet filter {
    chain input {
        type default priority 0; policy drop;
        iifname "lo" accept
        ct state established,related accept
        iifname "eth0" tcp dport { 53, 80 } accept
        iifname "eth0" udp dport { 53, 67, 443 } accept
    }
}

That allows DNS (53) and the Pi-hole web UI (80) from the LAN, plus DHCP (67) only if Pi-hole serves DHCP, plus 443 for Pi-hole's optional local HTTPS. Everything else inbound drops. Port 22 (SSH) is deliberately not open to the LAN default here — see below for why you may re-enable it, restricted.

Note what's absent: no inbound 5335. Unbound listens on loopback only, so an nftables rule for it is dead weight — and if you later bind it to eth0 by accident, the missing rule fails closed instead of silently exposing recursion to the LAN (which would make the box an open-resolver-shaped target if NAT were ever misconfigured).

Hardening the box itself

DNS for a whole network is a high-value target on a low-power board. Minimum work:

SSH. Install openssh-server, then in /etc/ssh/sshd_config: PermitRootLogin no, PasswordAuthentication no, PubkeyAuthentication yes, MaxAuthTries 3, and bind to the LAN interface if the board has only one. Generate a key on your workstation, verify login works before closing the password path — lockouts on a headless board are a pain. If you must have password access, keep it but add AllowUsers naming a single non-root account.

Unattended upgrades. Debian's UnattendedUpgrades wiki page documents the setup: apt install unattended-upgrades, enable in /etc/apt/apt.conf.d/20auto-upgrades (Update-Package-Lists "1"; Unattended-Upgrade "1"), and it handles the security-origins repo by default. Two caveats we test for at build time: confirm unbound and pihole survive an upgrade cycle (Pi-hole's installer rewrites some config files — pin or re-run pihole -r after major updates), and reboot the appliance after kernel upgrades so you're not quietly running a patched kernel while the loaded one is old. A monthly check-reboot-required cron entry is the cheap fix.

Reduce the surface. An Orange Pi Zero 3 needs no Samba, no Docker, no web tools beyond Pi-hole's UI. systemctl list-unit-files --state=enabled and turn off anything you can't name a use for. ss -tlnp should show: port 53 (pihole-FTL), port 80 (pihole-FTL), port 5335 (unbound, loopback only), port 22 (sshd, LAN only). Anything else is a question you should answer before shipping the box to your desk.

Backups. Pi-hole's teleporter export (pihole -a -t) takes seconds and captures your blocklists, regex filters and DHCP config. Schedule it weekly to another machine. An appliance that can't be rebuilt from backup isn't hardened; it's a single point of failure with extra steps.

Verifying: DNSSEC and leaks

Check validation is real, not assumed:

dig @<pi-hole-ip> www.dnssec-failed.org

The status should be SERVFAIL — the zone deliberately carries broken signatures. If you get an answer, validation isn't happening (usually: the trust anchor file missing, or Pi-hole still pointed at a public forwarder).

For leaks: resolve a unique subdomain while watching, or use one of the standard browser-based leak-test pages; with local recursion your exit traffic hits the authoritative servers directly, so "leak" tests that look for third-party resolvers will show only your own IP — which is the expected, correct result, not a misconfiguration. The thing to actually confirm is that no public resolver IP (8.8.8.8, 1.1.1.1, 9.9.9.9) appears in the path.

Also worth a minute: pihole -g after changing upstreams, then check the query log shows Unbound answer times in the single-digit milliseconds for cached entries.

Where the built appliance earns its keep

You can do all of this on any SBC in an evening, and if you want to, you should — an Orange Pi Zero 3 runs the stack comfortably, and a second-hand OpenWrt travel router works if your goal is DNS hygiene on the road rather than a permanent household appliance. What we sell at StealthOz.tech is the accumulated version of that evening, done repeatedly: the nftables ruleset above applied and tested, SSH pre-hardened with a build-time key drop, unattended upgrades verified to survive a Pi-hole major-version jump, DNSSEC validation confirmed with the SERVFAIL test before the unit ships, and DHCP set up so the overlap failure can't happen. Everything described in this article is public knowledge and reproducible; the kit exists because "reproducible in an evening" and "already reproducibly done" are different products for different people. We're explicit about that distinction — if you'd rather build, the guide above is the honest, complete version of the build.

Lawful-use note, since we deal in security-adjacent gear: running your own recursive resolver is ordinary network administration. It changes what your own devices ask your own resolver; it is not, and shouldn't be marketed as, a tool for evading lawful process or someone else's network policy.


← All posts