Ajitabh Pandey's Soul & Syntax

Exploring systems, souls, and stories – one post at a time

Tag: DNSSEC

  • When Pi-hole + Unbound Stop Resolving: A DNSSEC Trust Anchor Fix

    I have my own private DNS setup in my home network, powered by Pi-hole running on my very first Raspberry Pi, a humble Model B Rev 2. It’s been quietly handling ad-blocking and DNS resolution for years. But today, something broke.

    I noticed that none of my devices could resolve domain names. Pi-hole’s dashboard looked fine. The DNS service was running, blocking was active, but every query failed. Even direct dig queries returned SERVFAIL. Here’s how I diagnosed and resolved the issue.

    The Setup

    My Pi-hole forwards DNS queries to Unbound, a recursive DNS resolver running locally on port 5335. This is configured in /etc/pihole/setupVars.conf.

    PIHOLE_DNS_1=127.0.0.1#5335
    PIHOLE_DNS_2=127.0.0.1#5335

    And my system’s /etc/resolv.conf points to Pi-hole itself

    nameserver 127.0.0.1

    Unbound is installed with the dns-root-data package, which provides root hints and DNSSEC trust anchors:

    $ dpkg -l dns-root-data|grep ^ii
    ii dns-root-data 2024041801~deb11u1 all DNS root hints and DNSSEC trust anchor

    The Symptoms

    Despite everything appearing normal, DNS resolution failed:

    $ dig google.com @127.0.0.1 -p 5335

    ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL

    Even root-level queries failed:

    $ dig . @127.0.0.1 -p 5335

    ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL

    Unbound was running and listening:

    $ netstat -tulpn | grep 5335

    tcp 0 0 127.0.0.1:5335 0.0.0.0:* LISTEN 29155/unbound

    And outbound connectivity was fine. I pinged one of the root DNS servers directly to ensure this:

    $ ping -c1 198.41.0.4 
    PING 198.41.0.4 (198.41.0.4) 56(84) bytes of data.
    64 bytes from 198.41.0.4: icmp_seq=1 ttl=51 time=206 ms

    --- 198.41.0.4 ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 205.615/205.615/205.615/0.000 ms

    The Diagnosis

    At this point, I suspected a DNSSEC validation failure. Unbound uses a trust anchor, which is simply a cryptographic key stored in root.key. This cryptographic key is used to verify the authenticity of DNS responses. Think of it like a passport authority: when you travel internationally, border agents trust your passport because it was issued by a recognized authority. Similarly, DNSSEC relies on a trusted key at the root of the DNS hierarchy to validate every response down the chain. If that key is missing, expired, or corrupted, Unbound can’t verify the authenticity of DNS data — and like a border agent rejecting an unverified passport, it simply refuses to answer, returning SERVFAIL.

    Even though dns-root-data was installed, the trust anchor wasn’t working.

    The Fix

    I regenerated the trust anchor manually:

    $ sudo rm /usr/share/dns/root.key
    $ sudo unbound-anchor -a /usr/share/dns/root.key
    $ sudo systemctl restart unbound

    After this, Unbound started resolving again:

    $ dig google.com @127.0.0.1 -p 5335

    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR
    ;; ANSWER SECTION:
    google.com. 300 IN A 142.250.195.78

    Why This Happens

    Even with dns-root-data, the trust anchor could become stale — especially if the system missed a rollover event or the file was never initialized. Unbound doesn’t log this clearly, so it’s easy to miss.

    Preventing Future Failures

    To avoid this in the future, I added a weekly cron job to refresh the trust anchor:

    0 3 * * 0 /usr/sbin/unbound-anchor -a /usr/share/dns/root.key

    And a watchdog script to monitor Unbound health:

    $ dig . @127.0.0.1 -p 5335 | grep -q 'status: NOERROR' || systemctl restart unbound

    This was a good reminder that even quiet systems need occasional maintenance. Pi-hole and Unbound are powerful together, but DNSSEC adds complexity. If you’re running a similar setup, keep an eye on your trust anchors, and don’t trust the dashboard alone.