Zum Inhalt

Sovereign Recursive DNS: Unbound Root-Zone Resolution, QNAME Minimization & DNSSEC Validation

Executive Summary: Forwarding your LAN's DNS queries to commercial upstream resolvers (1.1.1.1, 8.8.8.8, 9.9.9.9)—even over DoH or DoT—concentrates a complete chronological profile of every domain your infrastructure resolves at a single third-party corporate entity. This architectural blueprint details the deployment of NLnet Labs Unbound as a self-hosted, validating, recursive caching resolver that walks the global DNS hierarchy directly from the 13 IANA Root Server clusters using RFC 9156 QNAME Minimization, RFC 8198 Aggressive NSEC Caching, RFC 8806 Local Root Zone (auth-zone), and multi-threaded Slab Cache tuning.

  • Direct Root-Zone Recursion


    Eliminate upstream DNS middlemen by resolving iteratively from IANA Root Servers (a.root-servers.net–m.root-servers.net) down to authoritative nameservers.

  • RFC 9156 QNAME Minimization


    Send only the minimal required domain label to each delegation layer (.de only sees .de, never mail.internal.nejat-hakan.de).

  • Cryptographic DNSSEC Chain


    Verify DNSKEY, DS, and RRSIG cryptographic signatures from the IANA Root Trust Anchor (root.key) and synthesize negative answers via Aggressive NSEC.

  • RFC 8806 Local Root Loopback


    Mirror the entire IANA Root Zone locally via AXFR/HTTPS (auth-zone: ".") to eliminate external root lookups and prevent root-level traffic analysis.


1. Architecture: Iterative Root Recursion with QNAME Minimization

When a standard recursive resolver without QNAME minimization resolves vault.infra.nejat-hakan.de, it leaks the full hostname to the Root Server, the .de TLD server, and the authoritative server. With qname-minimisation: yes, Unbound strips all unnecessary sub-labels at every step of the delegation tree:

%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
    subgraph LocalStack["Sovereign DNS Stack"]
        Sinkhole["Pi-hole / AdGuard Home<br/>127.0.0.1:53"]
        Unbound["Unbound Recursive Engine<br/>127.0.0.1:5335 (DNSSEC)"]
        LocalRoot[("RFC 8806 Local Root<br/>auth-zone '.' Mirror")]
    end

    subgraph GlobalHierarchy["Global Authoritative Hierarchy"]
        TLD["DENIC (.de TLD NS)<br/>Sees ONLY: nejat-hakan.de NS?"]
        AuthNS["Authoritative NS<br/>Sees: vault.nejat-hakan.de A?"]
    end

    Sinkhole -->|"Query: vault.nejat-hakan.de"| Unbound
    Unbound <-->|"1. Query: .de NS?<br/>(0ms Local Loopback)"| LocalRoot
    Unbound -->|"2. QNAME Minimized:<br/>nejat-hakan.de NS?"| TLD
    TLD -->|"Referral + DS Record"| Unbound
    Unbound -->|"3. Final Query:<br/>vault.nejat-hakan.de A?"| AuthNS
    AuthNS -->|"Signed A + RRSIG"| Unbound

Cryptographic & Privacy Mechanisms

  1. 0x20 Bit Fiendish Trick (use-caps-for-id: yes): Unbound randomizes uppercase and lowercase ASCII bits in outgoing query names (e.g., vAuLt.NeJaT-hAkAn.De). Because authoritative DNS servers must echo the exact case in the question section of their reply, this adds entropy beyond the 16-bit Transaction ID and randomized source port to defeat off-path Kaminsky cache-poisoning attacks.
  2. Aggressive NSEC/NSEC3 Caching (aggressive-nsec: yes): Defined in RFC 8198, Unbound uses cached DNSSEC NSEC and NSEC3 cryptographic range proofs to synthesize NXDOMAIN and NODATA responses locally without querying external nameservers for non-existent domains.
  3. Cache Prefetching (prefetch: yes & serve-expired: yes): When a cached record is requested within the final 10% of its TTL, Unbound immediately returns the cached answer to the client and triggers an asynchronous background refresh, keeping popular domain latencies below 0.3 ms.

2. Installation & Root Trust Anchor Bootstrap

Install unbound and dns-root-data on Debian/Ubuntu. Debian automatically populates the IANA Root Hints (/usr/share/dns/root.hints) and the RFC 5011 automated DNSSEC Root Trust Anchor (/var/lib/unbound/root.key).

Installing Unbound & Verifying RFC 5011 Trust Anchor
sudo apt update && sudo apt install -y unbound unbound-anchor dns-root-data dnsutils

# Verify or refresh the DNSSEC root trust anchor (/var/lib/unbound/root.key)
sudo -u unbound unbound-anchor -a /var/lib/unbound/root.key -v

3. Production Hardened Configuration (pi-hole-unbound.conf)

Create a modular drop-in configuration tuned for a 4-core server running alongside Pi-hole or AdGuard Home on 127.0.0.1:5335. Note that slab counts (msg-cache-slabs, rrset-cache-slabs, infra-cache-slabs, key-cache-slabs) must be a power of 2 (2, 4, 8, 16) close to num-threads to minimize lock contention across CPU cores.

/etc/unbound/unbound.conf.d/sovereign-recursive.conf
server:
    # --- Network & Interface Binding ---
    interface: 127.0.0.1
    interface: ::1
    port: 5335
    do-ip4: yes
    do-ip6: yes
    do-udp: yes
    do-tcp: yes
    prefer-ip6: no

    # --- Access Control Lists (Loopback + RFC1918 if queried directly) ---
    access-control: 127.0.0.0/8 allow
    access-control: ::1/128 allow
    access-control: 0.0.0.0/0 refuse
    access-control: ::0/0 refuse

    # --- Root Hints & Trust Anchor ---
    root-hints: "/usr/share/dns/root.hints"
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

    # --- Privacy & Anti-Spoofing Hardening ---
    hide-identity: yes
    hide-version: yes
    qname-minimisation: yes
    qname-minimisation-strict: no
    harden-glue: yes
    harden-dnssec-stripped: yes
    harden-below-nxdomain: yes
    harden-referral-path: no
    harden-algo-downgrade: yes
    use-caps-for-id: no
     aggressive-nsec: yes

    # --- DNS Rebinding Protection (Block RFC1918 in public responses) ---
    private-address: 10.0.0.0/8
    private-address: 172.16.0.0/12
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: fd00::/8
    private-address: fe80::/10
    # Allow internal domain to return RFC1918 addresses
    private-domain: "lan.nejat-hakan.de"

    # --- Multi-Core Performance & Slab Memory Allocation ---
    num-threads: 4
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4

    # Allocate 128MB to Message Cache and 256MB to RRset Cache (Always 2:1 ratio)
    msg-cache-size: 128m
    rrset-cache-size: 256m
    key-cache-size: 64m
    neg-cache-size: 32m

    # --- Socket Buffer & EDNS Tuning ---
    so-rcvbuf: 4m
    so-sndbuf: 4m
    so-reuseport: yes
    edns-buffer-size: 1232
    num-queries-per-thread: 4096
    outgoing-range: 8192

    # --- Low-Latency Prefetch & Stale Cache Resilience ---
    prefetch: yes
    prefetch-key: yes
    serve-expired: yes
    serve-expired-ttl: 86400
    serve-expired-client-timeout: 1800
    cache-min-ttl: 300
    cache-max-ttl: 86400

# --- RFC 8806: Mirror the IANA Root Zone Locally ---
auth-zone:
    name: "."
    primary: 199.9.14.201         # b.root-servers.net
    primary: 192.33.4.12          # c.root-servers.net
    primary: 199.7.91.13          # d.root-servers.net
    primary: 192.5.5.241          # f.root-servers.net
    primary: 192.112.36.4         # g.root-servers.net
    primary: 193.0.14.129         # k.root-servers.net
    primary: 192.0.47.132         # xfr.cjr.dns.icann.org
    primary: 192.0.32.132         # xfr.lax.dns.icann.org
    fallback-enabled: yes
    for-downstream: no
    for-upstream: yes
    zonefile: "/var/lib/unbound/root.zone"

Why edns-buffer-size: 1232 is Critical

Following the 2020 DNS Flag Day standard, setting edns-buffer-size: 1232 prevents IPv4/IPv6 UDP packet fragmentation over the minimum IPv6 MTU (1280 bytes minus 40-byte IPv6 header and 8-byte UDP header = 1232 bytes). Large DNSSEC responses exceeding 1,232 bytes cleanly signal TC (Truncated) and retry over TCP rather than suffering silent UDP fragment drops at intermediate ISP routers.


4. Split-Horizon Local Zones (local-zone & local-data)

Define internal infrastructure records directly inside Unbound so internal services resolve with valid forward (A/AAAA) and reverse (PTR) records even if the external internet link is severed:

/etc/unbound/unbound.conf.d/local-lan-zones.conf
server:
    local-zone: "lan.nejat-hakan.de." static

    local-data: "router.lan.nejat-hakan.de. 3600 IN A 192.168.10.1"
    local-data: "nas.lan.nejat-hakan.de. 3600 IN A 192.168.10.10"
    local-data: "proxmox.lan.nejat-hakan.de. 3600 IN A 192.168.10.20"

    # Automatic Reverse PTR Generation
    local-data-ptr: "192.168.10.1 router.lan.nejat-hakan.de."
    local-data-ptr: "192.168.10.10 nas.lan.nejat-hakan.de."
    local-data-ptr: "192.168.10.20 proxmox.lan.nejat-hakan.de."

5. Validation, DNSSEC Verification & Telemetry

Validate the configuration syntax, restart the service, and verify cryptographic AD (Authenticated Data) flags:

Validating Unbound Syntax, DNSSEC AD Flag & Cache Statistics
# 1. Validate configuration syntax before restarting
sudo unbound-checkconf

# 2. Restart Unbound and verify systemd status
sudo systemctl restart unbound

# 3. Verify valid DNSSEC signature returns NOERROR + 'ad' (Authenticated Data) flag
dig @127.0.0.1 -p 5335 icann.org A +dnssec +multiline

# 4. Verify forged/expired DNSSEC signature returns SERVFAIL
dig @127.0.0.1 -p 5335 dnssec-failed.org A

# 5. Inspect live thread cache hit rates and recursion latency histograms
sudo unbound-control stats_noreset | grep -E "total.num|histogram"