LAN-facing recursive resolver that forwards upstream over TLS, plus local zone overrides.
References used while building this¶
- https://quad9.net
- https://ev1z.be/2020/12/04/unbound-dns-over-tls-forwarding-server-on-debian-buster
- https://www.ctrl.blog/entry/unbound-tls-forwarding.html
- https://wiki.archlinux.org/title/unbound
- https://calomel.org/unbound_dns.html
- https://utcc.utoronto.ca/~cks/space/blog/sysadmin/UnboundLocalDNSOverride
Install (Debian)¶
sudo apt-get -y install unbound
Configure Unbound¶
/etc/unbound/unbound.conf.d/main.conf
server:
# print statistics to the log (for every thread) every N seconds.
# Set to "" or 0 to disable. Default is disabled.
statistics-interval: 300
# enable cumulative statistics, without clearing them after printing.
statistics-cumulative: yes
# enable extended statistics (query types, answer codes, status)
# printed from unbound-control. default off, because of speed.
extended-statistics: yes
# specify the interfaces to answer queries from by ip-address.
interface: 192.168.0.52
#interface: fd00:470:780b:120::102
# specify the interfaces to send outgoing queries to authoritative
# server from by ip-address. If none, the default (all) interface
# is used. Specify every interface on a 'outgoing-interface:' line.
outgoing-interface: 192.168.0.52
# Set this to yes to prefer ipv6 upstream servers over ipv4.
prefer-ip6: no
# Detach from the terminal, run in background, "yes" or "no".
# Set the value to "no" when unbound runs as systemd service.
do-daemonize: no
# control which clients are allowed to make (recursive) queries
# to this server. Specify classless netblocks with /size and action.
access-control: 192.168.0.0/24 allow
access-control: 0.0.0.0/0 refuse
# if given, a chroot(2) is done to the given directory.
chroot: "/var/lib/unbound"
# if given, user privileges are dropped (after binding port),
# and the given username is assumed. Default is user "unbound".
username: "unbound"
# the working directory. The relative files in this config are
# relative to this directory.
directory: "/etc/unbound"
# Log to syslog(3) if yes.
use-syslog: yes
# DNSSEC trust anchor, kept current by unbound-anchor
auto-trust-anchor-file: "/var/lib/unbound/root.key"
## Forward zones
forward-zone:
name: "."
forward-addr: 208.67.222.222
forward-addr: 208.67.220.220
forward-addr: 8.8.8.8
forward-addr: 8.8.4.4
Changes from the original draft, and why:
- Removed the bare
outgoing-interface: "192.168.0.52"quoting inconsistency — the interface value doesn't need (and shouldn't have) surrounding quotes; kept consistent withinterface:above it. - Added an explicit
access-control: 0.0.0.0/0 refuse. The original config relied onaccess-control: 192.168.0.0/24 allowalone with nothing else specified — Unbound does not implicitly deny everything else just because one allow rule exists; being explicit about the deny-everything-else case is what actually prevents this from becoming an open resolver if it's ever reachable from outside the LAN (e.g., a future firewall or routing change). - Added
auto-trust-anchor-file. The original haddnssec-enable-style validation implied but no trust anchor configured — without this, there's nothing for Unbound to validate signed answers against. On Debian this is normally pulled in automatically by a separate packaged include (root-auto-trust-anchor-file.conf); it's called out explicitly here since this config was being hand-assembled rather than left at package defaults, and because the module-configuration "todo" in the original notes was really pointing at exactly this gap along with forwarding. - Filled in the module-configuration "todo". The forwarders were already present in the original file, so the only missing piece for "module configuration i.e. forwards in forwarder.conf" was the trust-anchor line above and, if you want forwarding split into its own file rather than living in
main.conf, moving theforward-zoneblock into a separate/etc/unbound/unbound.conf.d/forwarder.conf— functionally identical, just organized the way the todo note implied it should be:
/etc/unbound/unbound.conf.d/forwarder.conf (optional split, if you want forwarding config separated from main.conf)
forward-zone:
name: "."
forward-addr: 208.67.222.222
forward-addr: 208.67.220.220
forward-addr: 8.8.8.8
forward-addr: 8.8.4.4
(remove the forward-zone block from main.conf if you split it out this way, to avoid two conflicting definitions loading at once.)
Configure Firewall for Unbound¶
UFW¶
sudo ufw allow dns
This opens 53/udp and 53/tcp. If DoT forwarding is enabled per below, also allow the outbound TLS port to your chosen upstream:
sudo ufw allow out 853/tcp
Configure Unbound for TLS¶
The original notes installed ca-certificates and left tls.conf empty — this is where the actual DoT forwarding gets turned on, replacing the plaintext forwarders above with TLS-authenticated ones:
sudo apt-get -y install ca-certificates
/etc/unbound/unbound.conf.d/tls.conf
server:
tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 9.9.9.9@853#dns.quad9.net
forward-addr: 149.112.112.112@853#dns.quad9.net
Given the Quad9 link in your reference list, Quad9 is used here as the upstream — swap in Cloudflare (1.1.1.1@853#cloudflare-dns.com) or Google (8.8.8.8@853#dns.google) if preferred. A few things worth being deliberate about:
- This
forward-zoneblock replaces the plaintext one inmain.conf/forwarder.conf, rather than adding to it — Unbound doesn't merge twoforward-zone name: "."blocks meaningfully; whichever loads last (or the resulting duplicate) is the point of confusion to avoid. Comment out or delete the plaintext forwarders once TLS forwarding is confirmed working. - The
#dns.quad9.netsuffix matters. Without it, Unbound still encrypts the connection but doesn't verify the certificate is actually Quad9's — encrypted-but-unauthenticated is a materially weaker guarantee than what DoT is meant to provide, so don't drop that suffix even though the config still "works" without it. tls-cert-bundlemust point at a real, present CA bundle —/etc/ssl/certs/ca-certificates.crtis correct for Debian/Ubuntu onceca-certificatesis installed (as above); remember this path is interpreted inside the chroot oncechroot: "/var/lib/unbound"is active (see next section), so confirm the bundle is actually reachable at/var/lib/unbound/etc/ssl/certs/ca-certificates.crton disk, or Unbound will fail to validate the upstream's certificate.
Verify DoT is actually in use rather than silently falling back to plaintext:
sudo unbound-control stats | grep -i tls
Add a Local Zone¶
This was the other "todo" in the original notes. A minimal example for overriding a couple of names on the LAN without standing up a full authoritative zone:
/etc/unbound/unbound.conf.d/local-zone.conf
server:
local-zone: "home.arpa." static
local-data: "nas.home.arpa. IN A 192.168.0.10"
local-data: "printer.home.arpa. IN A 192.168.0.20"
local-data-ptr: "192.168.0.10 nas.home.arpa."
local-data-ptr: "192.168.0.20 printer.home.arpa."
home.arpa. is the IANA-reserved name specifically intended for this kind of private/local use (RFC 8375), which is preferable to inventing an arbitrary internal TLD. static means anything in this zone not explicitly listed above returns NXDOMAIN rather than falling through to the real internet — use transparent instead of static if you want unlisted names under the same zone to resolve normally rather than fail.
Apply and check:
sudo unbound-checkconf
sudo systemctl restart unbound
dig @192.168.0.52 nas.home.arpa
dig @192.168.0.52 example.com # confirm normal + DoT-forwarded resolution still works
Troubleshooting Notes¶
- Config loads but DNSSEC validation fails everything (
SERVFAILacross the board): check the trust anchor file actually exists at the chrooted path (/var/lib/unbound/root.key) and thatunbound-anchorhas run at least once; also check system clock — signature validity windows are time-based. - DoT forwarding silently not encrypting: confirm the
tls-cert-bundlepath resolves correctly under the chroot (see above) — a missing bundle is a common silent-fallback cause depending on build/version. - Server unreachable from LAN clients despite
access-controlallowing it: check the firewall rule from the UFW section actually applied, and thatinterface:is bound to the address clients are pointed at, not just127.0.0.1.