Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Category:

Security

Published:

Tags:
  • dns
  • dns-over-tls
  • unbound
  • dnssec
  • linux
  • privacy

How to Set Up DNS-over-TLS with Unbound for Network-Wide Privacy on Linux

Most guides to encrypted DNS focus on a single browser flipping on DNS-over-HTTPS. That fixes one application on one device. Everything else on your network, your phone, your smart TV, the laptop your housemate refuses to update, keeps sending plaintext DNS queries to whatever resolver your router hands out, usually your ISP's. Running Unbound as a local recursive resolver with DNS-over-TLS (DoT) upstream fixes this for the whole network at once, and it validates DNSSEC while it's at it.

This is a how-to, not a design debate, so a quick scope note before the commands: DoT (RFC 7858) encrypts the link between your resolver and whatever upstream server it talks to. It does nothing about what that upstream server sees or logs. You're not eliminating a party that can see your DNS traffic, you're choosing which one it is, and taking your local network and ISP out of that list. Pick an upstream with a privacy policy you actually trust before you start typing IP addresses into a config file.

What you need

  • A Linux box that stays on: a Raspberry Pi, a small VM, or a router running OpenWrt (the package name and paths differ slightly there, but the Unbound config below is the same).
  • Root access and a static LAN IP for that box.
  • An upstream resolver that supports DoT. Cloudflare (1.1.1.1), Quad9 (9.9.9.9) and NextDNS all do.

Install Unbound

On Debian or Ubuntu:

sudo apt update
sudo apt install unbound unbound-anchor dns-root-data

unbound-anchor bootstraps the DNSSEC root trust anchor into /var/lib/unbound/root.key. Run it once by hand to be sure it worked before you rely on the automatic version in the config:

sudo unbound-anchor -a /var/lib/unbound/root.key

It's normal for this to print nothing and exit 0 on success, or exit non-zero with a message about the anchor already being valid on repeat runs.

Free up port 53

On most current desktop and server distributions, systemd-resolved already has a stub listener bound to 127.0.0.53:53. Unbound will fail to bind port 53 if you don't deal with this first. Check what's listening:

sudo ss -tulnp | grep ':53 '

If systemd-resolved shows up, disable its stub listener rather than stopping the whole service, since other things may depend on it for name resolution during boot:

# /etc/systemd/resolved.conf
[Resolve]
DNSStubListener=no
sudo systemctl restart systemd-resolved

Then point this machine's own resolution at Unbound once it's running, by making sure /etc/resolv.conf (or the symlink target systemd-resolved manages) lists 127.0.0.1.

Configure Unbound for DoT upstream

Drop a new file into /etc/unbound/unbound.conf.d/ rather than editing the shipped unbound.conf directly, most distributions already include: that directory:

# /etc/unbound/unbound.conf.d/dot.conf
server:
    interface: 0.0.0.0
    interface: ::0
    port: 53
    do-ip4: yes
    do-ip6: yes
    do-udp: yes
    do-tcp: yes

    access-control: 127.0.0.0/8 allow
    access-control: 192.168.1.0/24 allow
    access-control: ::1/128 allow

    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: yes
    qname-minimisation: yes
    prefetch: yes

    auto-trust-anchor-file: "/var/lib/unbound/root.key"
    tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"

forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
    forward-addr: 1.0.0.1@853#cloudflare-dns.com
    forward-addr: 9.9.9.9@853#dns.quad9.net

Change 192.168.1.0/24 to your actual LAN. Without an access-control entry covering it, Unbound will refuse queries from other devices with a REFUSED response, which is Unbound's safe default against becoming an open resolver.

The bit worth actually reading closely is the #hostname suffix on each forward-addr line. Unbound sends that hostname via SNI during the TLS handshake and then checks it against the certificate the upstream presents. Leave it off and Unbound will still encrypt the connection, but it stops being able to tell you're talking to Cloudflare rather than something sitting between you and Cloudflare with a certificate for something else entirely. The encryption without the authentication check is not the same guarantee.

qname-minimisation is a smaller but genuinely useful line: instead of sending a full query name to every server in the resolution chain, Unbound only sends the minimum label needed at each step (RFC 7816). The root servers never see mail.internal.example.com, only .com.

Check the config, then start it

sudo unbound-checkconf
sudo systemctl enable --now unbound

unbound-checkconf catches syntax errors before you restart a service that half your network is about to depend on for name resolution, so don't skip it.

Verify it's actually working

First, a plain lookup against the resolver itself:

dig @127.0.0.1 example.com

You want a fast answer with SERVER: 127.0.0.1#53 in the output. Then confirm DNSSEC validation is live by querying a domain that deliberately serves a broken signature:

dig @127.0.0.1 sigfail.verteiltesysteme.net

This should come back SERVFAIL. If it resolves normally, validation isn't running, check that root.key exists and is non-empty, and that auto-trust-anchor-file points at it.

Now confirm the upstream leg is actually encrypted, rather than trusting the config file at its word. Watch the wire while a query goes out:

sudo tcpdump -ni any port 853 or port 53

In another terminal, run the dig command again. You should see outbound traffic to 1.1.1.1 or 9.9.9.9 on port 853, and nothing to any external address on port 53. If you see plaintext port 53 traffic leaving the box, Unbound has fallen back, most commonly because forward-tls-upstream: yes ended up outside the forward-zone block through a stray indentation error, which unbound-checkconf won't always flag as fatal.

Point the rest of the network at it

Log into your router and change the DHCP-advertised DNS server from the ISP default to this box's static LAN IP. Every device that picks up an address afterwards will use it automatically; existing leases pick it up on renewal or reconnect. If your router won't let you override its own upstream DNS (some ISP-supplied units genuinely don't expose this), you can usually still override the DHCP option for DNS servers even while the router keeps its own separately.

A couple of loose ends worth checking a day or two in, once traffic has actually flowed:

unbound-control stats_noreset | grep -E 'total.num.queries|total.num.cachehits'

A healthy cache hit ratio climbing over the first few hours confirms devices are actually using this resolver rather than falling back to a hardcoded 8.8.8.8 somewhere. It's worth grepping your phone's and smart TV's network settings specifically, plenty of devices ship with DNS-over-HTTPS hardcoded to Google or Cloudflare regardless of what DHCP tells them, which quietly routes around everything you just built.

Firewall-wise, only outbound 853/tcp to your chosen upstreams and inbound 53/udp+tcp from your LAN need to be open. There's no need to expose port 53 to the wider internet, and doing so would turn your resolver into exactly the kind of open recursive resolver that gets abused for DNS amplification.