Autonomous DNS: Pi-hole & Unbound for Network Privacy

 

The Privacy Cost of Traditional DNS

Traditional DNS resolvers, controlled by Internet Service Providers (ISPs) or major cloud providers, pose privacy risks due to data collection and monetization.

  • ISPs: Collect and monetize unencrypted DNS queries, often linking them to subscriber identity. They may also engage in NXDOMAIN hijacking for ad revenue.
  • Major Cloud DNS Providers: While offering speed and security, their business models can impact privacy.
    • Cloudflare (1.1.1.1): High privacy stance, purges logs within 25 hours, audited by KPMG. Monetizes via enterprise upsells.
    • Google Public DNS (8.8.8.8): Moderate privacy. Logs IP addresses for 24-48 hours, retains anonymized/aggregated data. Contributes to Google's broader ecosystem understanding.
    • NextDNS / AdGuard: High privacy, user-centric. Freemium/subscription models offer advanced features.
    • Quad9 (9.9.9.9): Very high privacy. Nonprofit, supported by donations, strict no-logging policy, based in Switzerland.

The "anonymization" of data by some providers is often reversible, highlighting the limitations of third-party trust.

Architecting Your Autonomous DNS Stack: Pi-hole and Unbound

A two-tier DNS stack, comprising Pi-hole and Unbound, creates a self-contained system for DNS resolution.

Pi-hole (Layer 1: Filter/Sinkhole)

Intercepts all DNS requests. It matches queries against blocklists and returns `0.0.0.0` for blocked domains (e.g., ad servers). Allowed domains are served from its cache or forwarded to Unbound.

Unbound (Layer 2: Recursive Resolver)

Acts as a pure recursive resolver. It queries root name servers directly and iteratively resolves domain names without relying on upstream providers. This ensures no single entity logs the complete browsing activity.

Resolution Lifecycle:

  1. Client sends DNS query to Pi-hole.
  2. Pi-hole checks its cache; if found, returns to client.
  3. Pi-hole checks blocklist; if blocked, returns `0.0.0.0`.
  4. If not cached or blocked, Pi-hole forwards the request to Unbound.
  5. Unbound checks its cache; if found, returns to Pi-hole.
  6. If not cached, Unbound performs recursive resolution: queries Root servers (for TLD), then TLD servers (for domain), then Authoritative servers (for IP).
  7. Unbound validates DNSSEC signatures cryptographically.
  8. Unbound receives the final IP address.
  9. Pi-hole returns the IP to the client.

Zero-Trust DNS: Why Third-Party Resolvers Are a Vulnerability

A Zero-Trust approach mandates rejecting blind trust in any third-party resolver.

  • Visibility Loss: Third-party DNS obscures critical visibility into DNS-based cyberattacks (malware C2, phishing) from internal security infrastructure.
  • Exfiltration Path: DNS is an open protocol ideal for covert channels like "DNS Tunneling," where attackers encode data in subdomain queries. Third-party resolvers cannot detect organization-specific exfiltration patterns.
  • Identity-Aware Resolution: Zero-Trust DNS should be identity-aware, verifying user/device authorization before resolving. Third-party resolvers lack this internal context.
  • Shadow Encrypted DNS: DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) allow applications to bypass local DNS, hiding malware communications. A Protective DNS (PDNS) strategy involves blocking third-party DoH/DoT endpoints at the firewall.

Implementing Unbound: Core Configuration for Security & Privacy

Unbound's DNSSEC Validation and Trust Anchors

Unbound performs DNSSEC validation by building a chain of trust from the root.

  • Root Servers: Provide cryptographic signatures (RRSIG) and public keys (DNSKEY) for the root zone.
  • Trust Anchor (`root.key`): Located at `/var/lib/unbound/root.key`, this file contains the root zone's authentic public key. The `auto-trust-anchor-file` directive in `unbound.conf` points to this.
  • Automated Updates (RFC 5011): Unbound automatically manages trust anchor rollovers.
  • Root Hints: A list of root server IPs, provided via `root-hints` directive.

Crucial Security and Privacy Flags in `unbound.conf`

Within the `server:` block:

  • `harden-glue: yes`: Prevents DNS cache poisoning by ensuring glue records are within the authority of the providing nameserver.
  • `qname-minimisation: yes`: Enhances privacy by sending only the minimum necessary domain labels to servers in the resolution chain (RFC 7816).
  • `harden-dnssec-stripped: yes`: Treats unsigned data from zones that should be signed as "Bogus" to prevent downgrade attacks.
  • `edns-buffer-size: 1232`: Sets an optimal EDNS0 buffer size to prevent IP fragmentation issues while allowing larger DNSSEC packets.

Example `unbound.conf` Snippet:

server:
    port: 5335
    interface: 127.0.0.1
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no # Set to yes if IPv6 is native

    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no
    edns-buffer-size: 1232

    qname-minimisation: yes
    rrset-roundrobin: yes

    prefetch: yes
    prefetch-key: yes
    serve-expired: yes
    serve-expired-client-timeout: 0

    num-threads: 1 # Tune to CPU cores
    msg-cache-slabs: 1
    rrset-cache-slabs: 1
    infra-cache-slabs: 1
    key-cache-slabs: 1
    rrset-cache-size: 256m
    msg-cache-size: 128m
    so-reuseport: yes # Linux only
                

Always verify with `unbound-checkconf` and restart Unbound.

Performance Optimization: Mitigating Initial Latency

Unbound's local recursive resolution can have higher latency for initial queries (cache misses) compared to public resolvers. Optimizations mitigate this "cold cache penalty."

Latency Characteristics:

  • Local Cache Hit: < 1ms
  • Recursive Miss: 100ms – 500ms+ (due to multiple network round-trips and DNSSEC validation).

Critical Optimizations:

Maximizing Cache Hits:

  • `prefetch: yes`: Proactively refreshes popular cached records nearing TTL expiration.
  • `serve-expired: yes`: Serves expired data instantly if upstream servers are unreachable, updating cache in background.
  • `serve-expired-ttl: 86400`: Defines how long expired data can be served (e.g., 24 hours).
  • `prefetch-key: yes`: Ensures DNSSEC keys are always fresh.

Resource Management and Multi-threading:

  • `num-threads: [Number of CPU Cores]`: Parallelizes resolution tasks.
  • `msg-cache-size` & `rrset-cache-size`: Increase cache sizes based on available RAM (e.g., `256m` and `128m`).
  • `msg-cache-slabs`, `rrset-cache-slabs`, etc.: Set to powers of 2, ideally matching `num-threads`.
  • `so-reuseport: yes`: (Linux) Improves UDP performance on multi-core systems.
  • `so-rcvbuf: 4m` & `so-sndbuf: 4m`: Increase socket buffer sizes to handle high query volumes.

Smart Server Selection:

  • `fast-server-permil: 900`: Prioritizes the fastest known upstream nameservers.

Deployment Strategies: Hardware Considerations

Pi-hole and Unbound can be deployed on various platforms.

Deployment Comparison Matrix:

Environment Primary Method Resource Usage Pros Cons
Raspberry Pi Bare Metal Minimal Simple, dedicated, low power SD card wear, limited redundancy
Docker `docker-compose.yml` Low Portable, easy updates, isolation Network complexity
Proxmox (LXC) Linux Container Extremely Low Near-bare-metal, snapshots, high density Requires Proxmox, some Linux/LXC knowledge
Proxmox (VM) Virtual Machine Moderate Highest isolation, easy migration Higher overhead, slower boot times

Deployment Details:

  • Raspberry Pi (Bare Metal): Use Raspberry Pi OS Lite. Install Pi-hole via one-line installer, then Unbound via `apt`. Use high-quality SD cards or SSDs.
  • Docker: Use `network_mode: "host"` for simplicity and direct `127.0.0.1` communication. `MACVLAN` offers greater isolation but is more complex. Pi-hole v6 uses new environment variables for upstream DNS.
  • Proxmox (LXC): Highly recommended for efficiency. Share host kernel, minimal resources. Enable "Nesting" if running Docker inside LXC.
  • Proxmox (VM): Use only if strict isolation is paramount due to higher overhead.

Key Configuration Best Practices:

  1. Port 5335: Configure Unbound to listen on `5335` to avoid conflict with Pi-hole on Port 53.
  2. DNSSEC: Enable in Pi-hole; Unbound handles validation.
  3. Redundancy: Run two instances (e.g., Pi and Proxmox LXC) for high availability. Use Gravity Sync for blocklist synchronization.
  4. Static IPs: Assign a static IP to the Pi-hole host via DHCP reservation or direct configuration.

Network Integration: Pointing Your Router to Pi-hole

Configure your router's DHCP server to direct all devices to Pi-hole for DNS resolution.

  1. Static IP for Pi-hole: Ensure Pi-hole has a static IP (e.g., `192.168.1.10`) via DHCP reservation or host configuration.
  2. Router DHCP Settings:
    • Primary DNS: Pi-hole's static IP.
    • Secondary DNS: Leave BLANK to prevent bypass.
    • IPv6 DNS: Configure Pi-hole's IPv6 or disable IPv6 DNS.
  3. Connect Pi-hole to Unbound:
    • In Pi-hole Web UI (Settings > DNS), uncheck all default upstream servers.
    • In Custom 1 (IPv4), enter `127.0.0.1#5335`.
    • Enable DNSSEC.
  4. Optional: Conditional Forwarding: Enable to display client hostnames in Pi-hole logs. Enter your network range, router IP, and local domain name.
  5. Verification: Force DHCP lease renewal on client devices (reboot or `ipconfig /renew`). Use `nslookup pi.hole` to confirm it returns Pi-hole's IP.

Maintenance and Blocklist Curation

Blocklist Selection:

  • OISD (Optimized for No False Positives): Highly curated meta-list with manual whitelisting. Recommended URLs: `https://big.oisd.nl` (full), `https://nsfw.oisd.nl` (NSFW). Requires Pi-hole v5.22/v6.0+.
  • StevenBlack: Default Pi-hole list, aggregates reputable sources. URL: `https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts`.
  • HaGezi's Multi Pro: Recommended for modern stealth tracker protection. URL: `https://hagezi.github.io/dns-blocklists/`.

Curation Strategy:

  • "Clean" Setup: Use OISD Big as the primary list to minimize redundancy and conflicts.
  • "Modern" Alternative: Consider HaGezi's Multi Pro.

Updating Pi-hole Gravity:

  • UI: Group Management > Adlists > Add URL > Tools > Update Gravity > Update.
  • CLI: `pihole -g`.
  • Automatic updates occur weekly via cron job.

Whitelisting and Blacklisting:

  • Whitelisting: Domains > Whitelist.
  • Blacklisting: Domains > Blacklist.
  • Wildcard Blocking: Use `*.domain.com` for broader blocking.

Pre-Installation Audit: Securing Port 53

A pre-installation audit is crucial to resolve Port 53 conflicts before installing Pi-hole and Unbound.

Phase 1: Identifying Port 53 Conflicts

Use `sudo lsof -i :53` or `sudo ss -tulpn | grep :53`. Common culprits include:

  • `systemd-resolved` (Ubuntu/Debian)
  • `dnsmasq`
  • `named` (BIND)
  • `libvirt-dnsmasq`

Phase 2: Resolving Common Conflicts

  • Disabling `systemd-resolved`: Edit `/etc/systemd/resolved.conf`, set `DNSStubListener=no`, create symlink `/etc/resolv.conf`, and restart `systemd-resolved`.
  • Handling Standalone `dnsmasq`: Stop and disable the service: `sudo systemctl stop dnsmasq && sudo systemctl disable dnsmasq`.
  • Handling `libvirt`: Destroy and disable the default virtual network: `sudo virsh net-destroy default && sudo virsh net-autostart default --disable`.

Phase 3: Pi-hole + Unbound Port Allocation Strategy

  1. Install Pi-hole first (binds to Port 53).
  2. Install Unbound; it will likely fail to start.
  3. Edit Unbound's configuration to listen on Port `5335` (e.g., `port: 5335`, `interface: 127.0.0.1`).
  4. Restart Unbound.
  5. Configure Pi-hole's upstream DNS to `127.0.0.1#5335`.

Audit Checklist Summary:

Action Item Command/Check Resolution
Check Port 53 `sudo lsof -i :53` Disable conflicting service (`systemd-resolved`, `dnsmasq`, `libvirt`).
Check Port 80 `sudo lsof -i :80` Ensure no other web server conflicts with Pi-hole's web interface.
OS Version `cat /etc/os-release` Identify `systemd-resolved` on Debian/Ubuntu.
Unbound Port Verify Unbound config Ensure Unbound listens on `5335` after Pi-hole installation.

Labels / Tags

DNS, Pi-hole, Unbound, Privacy Security, Network Tutorial

Comments