Pi-hole + Unbound: DNS Without a Middleman
Blocking ads is good. But even with Pi-hole, external DNS providers see every domain name your network resolves. This guide shows how Unbound as a recursive resolver eliminates that final data transfer — and why it matters beyond the homelab.
The DNS Privacy Problem
Every time a device on your network visits a website, it first asks a DNS server: "What IP address does this domain have?" These queries reveal more than they might initially appear to.
Even without knowing the actual page content, DNS logs create a detailed profile: which services are used, at what times, how frequently. This affects individuals — and businesses even more so.
Running your DNS traffic through external resolvers like 8.8.8.8 (Google), 9.9.9.9 (Quad9), or 1.1.1.1 (Cloudflare) gives those third parties visibility into every domain your organisation resolves — including internal services, cloud providers, partner APIs, and business research. A Raspberry Pi 4 costs around 70 euros and handles DNS for small networks of up to roughly 50 devices without effort. The hardware investment is modest; the privacy gain is substantial.
The commonly recommended solution — Pi-hole — solves part of the problem, not all of it.
Pi-hole Alone: Good, But Incomplete
Pi-hole is a DNS filter: it receives all DNS queries from the network and blocks known advertising and tracking domains before a connection is established. That is effective and worthwhile.
Pi-hole does not resolve DNS queries itself. It forwards non-blocked queries to a configured upstream resolver — for example Quad9 or Cloudflare. That external resolver then sees all queries Pi-hole passes through.
Google DNS (8.8.8.8) is the most widely used public resolver — and one
of the most privacy-unfriendly. If an external resolver is used at all: Quad9
(9.9.9.9) with DNSSEC validation or Cloudflare (1.1.1.1)
are significantly better options. Better still: Unbound.
The fundamental problem remains: a third party in the loop sees your DNS traffic. Unbound solves that.
Unbound: The Elegant Solution
Unbound is a validating, recursive DNS resolver. Rather than forwarding a query to an external provider, Unbound queries the authoritative nameservers directly — starting from the DNS root servers.
The process: Unbound first queries a root server (a.root-servers.net etc.),
which responds with the responsible TLD nameserver (e.g. for .com). That
server refers to the authoritative nameserver for the domain. Unbound resolves the
query entirely without ever sending a complete query to a single external provider.
The result is cached.
No external DNS provider therefore sees the complete DNS traffic of your network.
Architecture and Data Path
The data path after this setup:
Pi-hole is the only DNS listener on the network (port 53). Its upstream points
exclusively to 127.0.0.1#5533 — Unbound on the same device. Unbound
itself is only reachable via localhost and the local network, not publicly.
Installation
Install Pi-hole
If Pi-hole is not yet installed, the official installer script provides the established path:
Terminalcurl -sSL https://install.pi-hole.net | bash
During installation, select any value for the upstream DNS configuration — this will be replaced by Unbound in the next step. Pi-hole can also be reconfigured afterwards through the admin interface.
Install Unbound
Unbound is included in the standard repositories of all common Debian-based systems:
Terminalsudo apt update
sudo apt install unbound
Check the status after installation:
Terminalsudo systemctl status unbound
Unbound starts with a minimal default configuration after installation. This will be replaced with a hardened configuration in the next step.
Update Root Hints
Unbound needs a list of the DNS root servers. This file should be updated occasionally:
Terminalsudo wget -O /etc/unbound/named.root \
https://www.internic.net/domain/named.root
The file contains the current IP addresses of the 13 root server groups.
Configuring Unbound
The following configuration is based on a production homelab setup. All security-relevant options are enabled and commented. Create or replace the configuration file:
/etc/unbound/unbound.conf.d/pi-hole.confserver:
# ──────────────────────────────────────────────
# Network: listen on localhost only.
# Pi-hole forwards internally; external devices
# never speak to Unbound directly.
# ──────────────────────────────────────────────
interface: 127.0.0.1
port: 5533
do-ip4: yes
do-ip6: no
prefer-ip4: yes
# Access control:
# Refuse everything by default, then selectively allow.
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.0/8 allow
access-control: 192.168.1.0/24 allow # Adjust to your LAN subnet
# ──────────────────────────────────────────────
# Hide identity
# Do not reveal which software or version runs here.
# ──────────────────────────────────────────────
hide-identity: yes
hide-version: yes
# ──────────────────────────────────────────────
# QNAME minimisation (RFC 7816)
# Unbound sends only the minimal necessary part of the
# domain name to each nameserver — no server sees the
# full query except the authoritative nameserver.
# ──────────────────────────────────────────────
qname-minimisation: yes
qname-minimisation-strict: yes
# ──────────────────────────────────────────────
# DNSSEC hardening
# Responses are cryptographically validated.
# Manipulated or forged answers are detected and
# discarded.
# ──────────────────────────────────────────────
harden-glue: yes
harden-dnssec-stripped: yes
harden-below-nxdomain: yes
harden-referral-path: yes
harden-algo-downgrade: yes
harden-large-queries: yes
harden-short-bufsize: yes
# DNSSEC trust anchor (automatically updated)
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# ──────────────────────────────────────────────
# Performance
# prefetch: nearly-expired cache entries are renewed
# before they expire.
# serve-expired: during refresh, the expired entry is
# served rather than making the client wait.
# cache-min-ttl: prevents entries with very short TTLs
# from unnecessarily polluting the cache.
# ──────────────────────────────────────────────
prefetch: yes
serve-expired: yes
cache-min-ttl: 4400
edns-buffer-size: 1232
# ──────────────────────────────────────────────
# Additional security options
# use-caps-for-id: 0x20 encoding as protection
# against cache poisoning attacks.
# aggressive-nsec: DNSSEC NSEC records are used
# to confirm negative answers without querying the
# authoritative server again.
# ──────────────────────────────────────────────
use-caps-for-id: yes
aggressive-nsec: yes
unwanted-reply-threshold: 10000
# ──────────────────────────────────────────────
# Root hints and local zones
# ──────────────────────────────────────────────
root-hints: "named.root"
do-not-query-localhost: no
insecure-lan-zones: yes
local-zone: "168.192.in-addr.arpa." nodefault
# ──────────────────────────────────────────────
# System paths
# ──────────────────────────────────────────────
tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
username: "unbound"
directory: "/etc/unbound"
Test the configuration and restart the service:
Terminalsudo unbound-checkconf
sudo systemctl restart unbound
sudo systemctl enable unbound
Set the value at access-control to your own LAN subnet, e.g.
10.0.0.0/24 or 192.168.0.0/24. Everything else
will be refused by Unbound.
Pointing Pi-hole to Unbound
For Pi-hole to use Unbound as its upstream, the custom DNS server must be configured in Pi-hole.
Pi-hole v6 (pihole.toml)
In Pi-hole v6, configuration is managed through /etc/pihole/pihole.toml.
Adjust the upstream DNS section:
[dns]
# Unbound on the same device, port 5533
upstreams = [ "127.0.0.1#5533" ]
Pi-hole v5 (Admin Interface)
In Pi-hole v5 via the admin interface: Settings → DNS → Upstream DNS Servers.
Remove all existing checkmarks, enter 127.0.0.1#5533 under
"Custom 1 (IPv4)" and save.
Make sure no other upstream DNS servers are configured (neither Quad9 nor Cloudflare). Pi-hole should use exclusively Unbound — otherwise queries will continue to go partially to external resolvers.
Restart Pi-hole for the change to take effect:
Terminalsudo systemctl restart pihole-FTL
Network Hardening: UFW and TCP Wrappers
Pi-hole and Unbound should only be reachable from your own network. The following rules show a typical configuration that allows only LAN access.
UFW Firewall
UFW (Uncomplicated Firewall) on Debian/Ubuntu sets iptables rules without complex syntax. Goal: deny all incoming connections, then selectively allow the LAN.
Terminal# Default rules
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH from LAN only (adjust subnet)
sudo ufw allow from 192.168.1.0/24 to any port 22
# DNS: Pi-hole on port 53
sudo ufw allow from 192.168.1.0/24 to any port 53
# Unbound directly (optional, for debugging)
sudo ufw allow from 192.168.1.0/24 to any port 5533
# Pi-hole admin interface (port 80/443)
sudo ufw allow from 192.168.1.0/24 to any port 80,443 proto tcp
# Enable UFW
sudo ufw enable
sudo ufw status verbose
Port 5533 does not strictly need to be open via UFW, since Pi-hole only reaches
Unbound via localhost (127.0.0.1). The rule is optional and can be
useful for occasional debugging from other devices. In a fully hardened setup
it can be omitted.
TCP Wrappers (hosts.allow / hosts.deny)
TCP Wrappers is an additional access control layer that operates independently of the firewall. The combination of both increases security depth.
/etc/hosts.allow# All services: access only from LAN
ALL: 192.168.1.0/24
/etc/hosts.deny
# PARANOID: reject hosts whose hostname does not match
# their resolved IP address (reverse DNS check).
# Protects against spoofing attempts.
ALL: PARANOID
ALL: PARANOID rejects connections where the hostname does not match
the IP address — protection against DNS spoofing. ALL: ALL would be
stricter (blocks everyone not in hosts.allow), but
PARANOID is the more practical compromise for most homelab
scenarios.
Verification
After configuration, verify that everything works as expected.
Test Unbound Directly
On the Pi itself, send a query directly to Unbound:
Terminal (on the Pi)dig google.com @127.0.0.1 -p 5533
The response contains status: NOERROR and resolved IP addresses in
the ANSWER SECTION. The ad flag (Authenticated Data)
indicates that DNSSEC validation succeeded.
Test Pi-hole as DNS
From a device on the network, use the Pi-hole IP as DNS:
Terminal (from another device on the LAN)# Adjust to your Pi's IP
dig google.com @192.168.1.x
Check Unbound Status
Terminalsudo systemctl status unbound
sudo journalctl -u unbound -n 50 --no-pager
Test DNSSEC Validation
This domain is intentionally signed with a broken DNSSEC record and should not be resolved by a correctly configured resolver:
Terminaldig sigfail.verteiltesysteme.net @127.0.0.1 -p 5533
status: SERVFAIL — Unbound detected the invalid signature and
discarded the response. This is the correct behaviour.
If both tests pass — successful resolution of normal domains and
SERVFAIL for the DNSSEC test domain — Pi-hole + Unbound with
DNSSEC validation is working correctly.
Bonus: WireGuard as an Additional Layer
Even with Unbound, your Internet Service Provider (ISP) can see all DNS queries that Unbound sends outbound — including the queried domain names in plaintext. DNS is unencrypted by default.
WireGuard addresses this as well: when DNS traffic is routed through a WireGuard tunnel, the ISP sees only encrypted WireGuard traffic, not the DNS queries themselves. The VPN endpoint (e.g. a VPS at a data centre) becomes the visible originator of the DNS queries.
For most home networks and small offices, Unbound alone already provides significant privacy gains — no DNS provider sees the complete traffic. WireGuard is an optional additional layer for use cases where connection metadata should also remain hidden from the ISP. A dedicated WireGuard guide will follow in this series.
Conclusion
Pi-hole without Unbound is like a letterbox with a lock, but the key is at the post office. Unbound makes the system complete.
What this setup concretely provides
- No external resolver: No DNS provider sees your complete network traffic. Pi-hole filters, Unbound resolves independently.
- DNSSEC validation: DNS responses are cryptographically verified. Manipulated responses (DNS spoofing, cache poisoning) are detected and discarded.
- QNAME minimisation: Each nameserver along the resolution path receives only the part of the name it needs — no server knows the complete queried domain except the authoritative one.
- Hardened network layer: UFW and TCP Wrappers ensure that Pi-hole and Unbound are only reachable from your own network.
- Affordable setup: A Raspberry Pi 4 (~70 euros) runs this stack without difficulty for small networks of up to 50 devices. No cloud, no subscriptions, no dependency on third-party providers.
- Extensible: WireGuard can be added to encrypt DNS traffic and hide queried domain names from your ISP.