Freorit

Open Source & Security Practitioner
DNS Privacy Raspberry Pi Linux ~20 min read Intermediate Updated 2025

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.

Relevant for Small Businesses and Freelancers

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.

Note: do not use 8.8.8.8

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

1

Install Pi-hole

If Pi-hole is not yet installed, the official installer script provides the established path:

Terminal
curl -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.

2

Install Unbound

Unbound is included in the standard repositories of all common Debian-based systems:

Terminal
sudo apt update
sudo apt install unbound

Check the status after installation:

Terminal
sudo systemctl status unbound

Unbound starts with a minimal default configuration after installation. This will be replaced with a hardened configuration in the next step.

3

Update Root Hints

Unbound needs a list of the DNS root servers. This file should be updated occasionally:

Terminal
sudo 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.conf
server:
# ──────────────────────────────────────────────
# 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:

Terminal
sudo unbound-checkconf
sudo systemctl restart unbound
sudo systemctl enable unbound
Adjust LAN subnet

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:

/etc/pihole/pihole.toml
[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.

Important: no other upstream

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:

Terminal
sudo 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
Why allow Unbound port in UFW?

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
hosts.deny: PARANOID instead of ALL

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
Expected output:

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

Terminal
sudo 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:

Terminal
dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5533
Expected result:

status: SERVFAIL — Unbound detected the invalid signature and discarded the response. This is the correct behaviour.

Setup complete

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.

Useful depending on your threat model

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.

Open Source Projects Used