
Lately the information from this domain has been of interest to me. Heard of linuxbabe.com ? Out of many links that i have looked for things pertaining to Linux, this one has some good stuff to go through. Just today I tried out the unbound DNS resolver, only to find out that all the code for unbound has not yet been debugged, especially the part about keeping it running in a chroot, so not being able to log to the eternal /var/log directory. What can I say? it’s working, except I would like to look into the logs of what could make it more efficient. The portion of DNSSEC should be working, however it doesn’t seem to like to load if the root.key is loaded. Well, I will tamper with it later, it is late. For now, unbound won’t have the privilege of systemctl enable.
Update: the rgbox Unbound setup, for anyone building their own
The struggles above were from the very first install. Since then the setup has matured into a full recursive resolver serving the whole home network, so here’s what it actually looks like now — shared for anyone doing the same thing on their own homelab.
Overview
- Unbound runs directly on the gateway host (not containerized), listening on all interfaces, serving DNS to every LAN/VPN subnet on the network.
- It’s a DNS-over-TLS forwarder, not a pure root-hints recursive resolver: all external queries go out encrypted to a handful of public resolvers rather than being resolved from the root down.
- A small number of internal zones are served authoritatively/locally, and kept in sync automatically from a separate internal DNS server (Technitium) via a scheduled script — so device records only need to be entered in one place.
- Validated caching is backed by Redis (via Unbound’s
cachedbmodule) rather than only Unbound’s in-memory cache, so the cache can survive an Unbound restart.
Core server tuning (unbound.conf)
The base config sizes caching and threading for the host it runs on — adjust these to your own CPU core count and RAM, they’re not one-size-fits-all:
server:
interface: 0.0.0.0
interface: ::0
interface-automatic: yes
username: unbound
chroot: ""
module-config: "validator cachedb iterator"
# Sized for a many-core host with plenty of RAM —
# scale these to your own hardware
num-threads: 24
msg-cache-slabs: 32
rrset-cache-slabs: 32
infra-cache-slabs: 32
key-cache-slabs: 32
rrset-cache-size: 512m
msg-cache-size: 256m
neg-cache-size: 64m
qname-minimisation: yes
hide-identity: yes
hide-version: yes
prefetch: yes
prefetch-key: yes
serve-expired: yes
serve-expired-ttl: 3600
edns-buffer-size: 1232
so-reuseport: yes
Access control is scoped to private address space only — every RFC1918/CGNAT range that might show up on a home or lab network is allowed, everything else is refused, so the resolver can’t be abused as an open recursive resolver from the public internet:
access-control: 127.0.0.0/8 allow
access-control: 10.0.0.0/8 allow
access-control: 192.168.0.0/16 allow
access-control: 100.64.0.0/10 allow # CGNAT range, covers Tailscale
access-control: 172.16.0.0/12 allow
access-control: ::1 allow
access-control: 0.0.0.0/0 refuse
access-control: ::/0 refuse
private-address: 192.168.0.0/16
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: fd00::/8
private-address: fe80::/10
harden-glue: yes
harden-dnssec-stripped: yes
harden-below-nxdomain: yes
val-clean-additional: yes
Forwarding over DNS-over-TLS, not raw recursion
Rather than resolving every query from the DNS root down, all non-local queries are forwarded — encrypted — to a set of public resolvers. Each forward address is pinned to its expected TLS certificate hostname where the provider supports it, so Unbound authenticates the upstream server rather than just trusting any certificate signed by a public CA:
forward-zone:
name: "."
forward-first: no
forward-tls-upstream: yes
# Family-filtering resolvers, DoT on port 853
forward-addr: 94.140.14.15@853 # AdGuard Family
forward-addr: 94.140.15.16@853 # AdGuard Family
forward-addr: 1.1.1.3@853#cloudflare-dns.com # Cloudflare Family
forward-addr: 1.0.0.3@853#cloudflare-dns.com # Cloudflare Family (secondary)
One gotcha worth saving other people the debugging time on: a couple of these DoT providers reply with a TLS internal_error alert to any ClientHello that includes SNI (Server Name Indication) — a server-side bug on their end, confirmed by packet capture, not something fixable client-side. Those entries are left without the #hostname authname pin and run opportunistic-TLS (still encrypted, just not certificate-authenticated) instead of failing outright.
Redis-backed cache (cachedb)
Unbound’s cachedb module can back the resolver cache with an external store over a Unix socket, so a resolver restart doesn’t cold-start every cache entry:
server:
module-config: "validator cachedb iterator"
cachedb:
backend: redis
redis-server-path: /run/redis/redis-server.sock
redis-timeout: 100
redis-expire-records: yes
Local zones + automatic sync from an internal DNS server
A handful of internal domains are declared as local zones so Unbound answers for them directly instead of forwarding out:
server:
local-zone: "example-lan.internal." static
local-zone: "example-lab.internal." static
local-zone: "example-services.internal." transparent
Rather than hand-editing those zone files whenever a device changes, a small Python script polls a separate internal DNS server (in this setup, a Technitium DNS Server instance) over its HTTP API every 15 minutes via cron, and rewrites only a clearly delimited, managed section of each Unbound conf file:
### TDNS11-SYNC-BEGIN example-zone ###
... records managed by the sync script live here ...
### TDNS11-SYNC-END example-zone ###
Anything outside those markers is left alone, so manual edits to the same file survive a sync run. Records that disappear from the source DNS server get commented out with a [REMOVED] tag rather than silently deleted, which has made it easy to notice when something vanished by mistake versus on purpose. The sync is wrapped in a flock so overlapping cron runs can’t race each other, and every run is timestamped to a dedicated log file for troubleshooting.
Credentials for the internal API live in a separate env file outside version control, loaded at runtime rather than hardcoded in the script — worth doing from day one if you’re writing anything similar, since it’s much easier than retrofitting it later.
None of the IPs, hostnames, or credentials above are real — they’re illustrative placeholders standing in for this network’s actual internal addressing.

Leave a Reply