Andrew Mercer
on this page

1. What Unbound Is

Unbound is a validating, recursive, and caching DNS resolver developed by NLnet Labs (originally with VeriSign and Nominet involvement), designed from the ground up around two priorities BIND's older codebase wasn't originally built for: DNSSEC validation as a first-class feature and a small, security-focused codebase. Where BIND is a general-purpose DNS server that can be authoritative, recursive, or both, Unbound is deliberately scoped to the resolver half of that job — it does not serve as an authoritative server for zones you own (though it can answer authoritatively for small local overrides; see §7). This narrower scope is a design choice, not a limitation: it means less attack surface and a simpler mental model for what the daemon is doing.

Typical roles Unbound fills: - A local caching resolver on a single machine or router, cutting lookup latency and reducing upstream query volume. - A LAN-wide recursive resolver, similar to the RHEL caching-BIND role covered in the companion BIND notes. - A DNS-over-TLS forwarding server — accepting plain DNS from LAN clients and re-encrypting outbound queries over TLS to an upstream provider, which is the specific role your notes below are building. - A DNSSEC-validating layer in front of an otherwise non-validating environment.

2. Installation

Debian / Ubuntu

sudo apt-get update
sudo apt-get install -y unbound

RHEL / CentOS / Rocky / Alma

sudo dnf install -y unbound

Alpine (common in minimal containers)

apk add unbound

On Debian/Ubuntu, the package ships a working default configuration and — importantly — a separate include file that seeds Unbound's DNSSEC trust anchor automatically (/etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf, pulling in root-auto-trust-anchor-file: "/var/lib/unbound/root.key", refreshed via unbound-anchor). This is easy to miss when hand-rolling a config from scratch, and without it DNSSEC validation has no trust anchor to validate against.

3. Configuration File Structure

Unbound's main config is /etc/unbound/unbound.conf, which on most distro packaging simply includes everything under /etc/unbound/unbound.conf.d/*.conf:

include: "/etc/unbound/unbound.conf.d/*.conf"

This lets you split concerns into separate files (main server options, forwarding, TLS, local overrides) the same way BIND's named.conf is split via include statements — each file just needs a server: block (or whichever top-level clause it's extending) rather than one being defined once globally.

Top-level clauses:

Clause Purpose
server: Global daemon behavior — listening addresses, access control, logging, DNSSEC, chroot, performance tuning
forward-zone: Send queries for a zone (. for everything) to specific upstream resolvers instead of doing iterative resolution from the root
stub-zone: Query specific authoritative servers directly for a zone, bypassing normal delegation lookup — useful for internal/private zones
auth-zone: Serve a zone authoritatively from a local zone file, alongside normal resolving
local-zone: / local-data: Define local overrides/answers without needing a full zone file (see §7)
remote-control: Configuration for unbound-control, Unbound's equivalent of rndc
view: Split-horizon style per-client answer sets, similar in spirit to BIND views

4. Core server: Options

server:
    interface: 192.168.0.52
    outgoing-interface: 192.168.0.52
    access-control: 192.168.0.0/24 allow

    do-daemonize: no
    username: "unbound"
    chroot: "/var/lib/unbound"
    directory: "/etc/unbound"

    statistics-interval: 300
    statistics-cumulative: yes
    extended-statistics: yes
    use-syslog: yes

Key points:

  • interface — addresses to listen on. Unlike BIND, Unbound doesn't implicitly bind everything; each address (and, if non-default, port) needs an explicit interface: line. Multiple lines stack.
  • outgoing-interface — the source address Unbound uses when sending queries upstream. For a multi-homed box, pin this the same way you'd pin BIND's listen-on to avoid queries leaving on an unintended interface.
  • access-control — Unbound's equivalent of BIND's allow-recursion/allow-query. Unlike BIND's ACL blocks, this isn't a separately named list — each line is <netblock> <action> (allow, deny, refuse, allow_snoop, etc.), evaluated with longest-prefix-match rather than first-match order. There is no implicit default-deny for anything not listed only if nothing at all is specified — an explicit access-control: 0.0.0.0/0 refuse (or simply never binding a public interface) is what actually keeps this resolver from becoming an open resolver; don't rely on omission alone.
  • chroot — confines the daemon to the given directory, the same protective idea as BIND's chroot jail, but built into Unbound's core config rather than a separate packaging variant. All other paths in the config (log files, key files, control socket) are resolved inside this jail once set — the single most common Unbound misconfiguration is a path that works fine with chroot: "" and silently breaks once chroot is enabled, because the referenced file doesn't exist at that path inside the jail.
  • directory — working directory for relative paths in the rest of the config, evaluated after chrooting.
  • username — user to drop privileges to after binding the (privileged, <1024) listening port.
  • do-daemonize — should be no when run under systemd, which handles backgrounding itself; set to yes only for old-style init/manual invocation.
  • Statistics — statistics-interval periodically logs summary counters; extended-statistics breaks them down by query type/response code, at a small CPU cost, and is only visible via unbound-control stats or in the log, not exposed any other way by default.

5. Recursive vs. Forwarding Mode

By default, with no forward-zone, Unbound resolves recursively from the root exactly as described in the general DNS guide — walking root → TLD → authoritative servers itself, caching along the way. This is the architecturally "purest" mode and what Unbound was originally optimized for.

Adding a forward zone changes this for the covered namespace:

forward-zone:
    name: "."
    forward-addr: 208.67.222.222   # OpenDNS
    forward-addr: 208.67.220.220
    forward-addr: 8.8.8.8          # Google
    forward-addr: 8.8.4.4

name: "." means "forward everything" — Unbound stops doing its own iterative resolution entirely and instead hands every query to one of the listed upstreams, still validating DNSSEC on the answers it gets back (validation and forwarding are independent — forwarding changes who answers, not whether the response is checked). Multiple forward-addr lines are tried for redundancy/load distribution, not concatenated into one combined answer.

This is a real trade-off, not just configuration mechanics: - Recursive (no forwarders): no dependency on any third party's resolver, but every uncached query pays the full root→TLD→authoritative latency, and Unbound's own IP is what shows up in query logs at every authoritative server it touches. - Forwarding: faster in practice (upstream providers run large, warm caches), but you're now trusting that provider with your query stream and depending on their uptime — worth pairing with DoT (§6) at minimum so that trust doesn't also extend to your network path.

A forward-zone can also be scoped narrowly instead of globally, e.g. forwarding only an internal corporate domain to a specific internal resolver while everything else resolves normally — set name: to that specific zone instead of ..

6. DNS over TLS (DoT) Forwarding

Since forwarding sends full plaintext queries to a third party over the open network by default, encrypting that leg is the natural next step — this is what turns a plain forwarding resolver into a "DoT forwarding server."

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
  • tls-cert-bundle — the CA bundle Unbound uses to validate the upstream's TLS certificate. Without this, forward-tls-upstream: yes will fail to establish trusted connections (behavior differs by build — some fail closed, some warn and fall through; don't rely on silent success).
  • forward-tls-upstream: yes — scoped to the forward-zone block, not a global server: setting; each forward zone independently chooses whether its upstream connections use TLS.
  • @853 — the DoT port (853, distinct from plain DNS's port 53).
  • #dns.quad9.net — the hostname Unbound authenticates the presented certificate against (this is what makes it authenticated TLS rather than merely encrypted-but-unverified; omitting the #hostname suffix still encrypts the connection but skips hostname verification, which meaningfully weakens the guarantee).

Common public DoT-capable resolvers, for reference:

Provider Address TLS auth name
Quad9 9.9.9.9, 149.112.112.112 dns.quad9.net
Cloudflare 1.1.1.1, 1.0.0.1 cloudflare-dns.com
Google 8.8.8.8, 8.8.4.4 dns.google

Verify DoT is actually being used (rather than silently falling back) with unbound-control stats | grep tls, or a packet capture on port 853 versus 53 outbound.

7. Local Zones and Overrides

Where BIND requires a full zone file even for a single overridden name, Unbound supports lightweight inline overrides via local-zone and local-data, without standing up an authoritative zone:

server:
    local-zone: "home.example.arpa." static
    local-data: "nas.home.example.arpa. IN A 192.168.0.10"
    local-data: "printer.home.example.arpa. IN A 192.168.0.20"
    local-data-ptr: "192.168.0.10 nas.home.example.arpa."

local-zone types control fallback behavior for names not explicitly covered by a local-data line within that zone:

Type Behavior for unmatched names in the zone
static NXDOMAIN / NODATA — nothing outside the explicit local-data entries exists
transparent Falls through to normal resolution for anything not explicitly overridden
redirect Every query in the zone gets the same single answer, regardless of the queried name
deny Silently drops queries into the zone
refuse Answers with REFUSED
nodefault Removes one of Unbound's compiled-in default reserved-space zones (e.g., to allow querying an RFC 1918 range normally blocked by default)

For a full authoritative-style local zone with more than a couple of records, auth-zone: pointing at a real zone file (in standard BBIND-style zone-file syntax) is cleaner than a long list of local-data lines:

auth-zone:
    name: "home.example.arpa."
    zonefile: "/etc/unbound/home.example.arpa.zone"

8. unbound-control

Unbound's equivalent of rndc, used to inspect and manage a running daemon without restarting it. Requires a control key/cert setup, generated once:

sudo unbound-control-setup

with a corresponding config block:

remote-control:
    control-enable: yes
    control-interface: 127.0.0.1
    control-port: 8953

Common commands:

unbound-control status               # daemon uptime, version
unbound-control stats                # cumulative counters (needs statistics-cumulative)
unbound-control stats_noreset        # same, without clearing counters after
unbound-control reload               # reload config + flush cache
unbound-control flush example.com    # flush one name from cache
unbound-control flush_zone example.com  # flush an entire zone's worth of cached entries
unbound-control dump_cache           # dump the full cache contents
unbound-control list_forwards        # show active forward-zone configuration

9. Logging and Diagnostics

server:
    use-syslog: yes
    verbosity: 1
    log-queries: no
  • verbosity ranges 0–5; 1 is a reasonable steady-state level (operational messages, errors), while 3+ approaches per-query tracing and is meant for active troubleshooting only, similar in spirit to leaving BIND's queries category permanently at debug — both log every lookup and should be dialed back down once a problem is resolved.
  • log-queries: yes independently logs every incoming query regardless of verbosity; same privacy/disk caveat applies as BIND's query logging.
  • With use-syslog: yes, output goes to the system logger (journalctl -u unbound under systemd) rather than a dedicated file unless you also configure logfile:.

10. DNSSEC Validation

Validation is on by default in modern Unbound packages via the auto-trust-anchor include mentioned in §2, but the underlying mechanics are worth understanding:

server:
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

unbound-anchor (run automatically by the packaged systemd unit before unbound starts, on distros that wire it up) fetches and verifies the current root trust anchor if the file is missing or looks stale, following RFC 5011 automated key rollover — meaning a running Unbound instance can follow a root key rollover without manual intervention, as long as it stays online through the rollover window. A resolver offline for an extended period spanning a key rollover may need unbound-anchor re-run manually on the next start.

Validation failures return SERVFAIL to the client by default rather than the (correct, but unsigned-looking) unvalidated answer — this is the intended fail-closed behavior, but it means a broken clock, a misconfigured trust anchor, or an actual on-path attack all present identically as "everything suddenly returns SERVFAIL," which is the first thing to check when diagnosing.

11. Firewall Considerations

Unbound needs inbound UDP+TCP/53 from whichever clients it serves (TCP for large/truncated responses and zone-transfer-adjacent operations even though Unbound isn't itself authoritative), and outbound UDP+TCP/53 (or TCP/853 for DoT upstreams) to whatever it resolves against or forwards to.

# UFW shorthand for local DNS service
sudo ufw allow dns

# More explicit equivalent
sudo ufw allow 53/udp
sudo ufw allow 53/tcp

If forwarding over DoT, also permit outbound 853/tcp to the specific upstream addresses.

12. Unbound vs. BIND, at a Glance

Unbound BIND
Primary role Recursive/caching resolver Both authoritative and recursive
Authoritative serving Minimal (local-zone/auth-zone overrides) Full-featured, the common choice for hosting real zones
DNSSEC validation On by default, core design goal Supported, opt-in (dnssec-validation)
Config style server:/forward-zone: blocks, include: globs named.conf + included files, zone {} blocks
Control utility unbound-control rndc
Typical fit LAN resolver, DoT/DoH forwarding gateway, validating layer Authoritative DNS hosting, combined authoritative+recursive small deployments

Running both in the same environment is common: Unbound as the LAN-facing recursive/validating/forwarding layer, BIND as the authoritative server for zones you actually own — each doing the job it's most purpose-built for.

  • Unbound project: https://nlnetlabs.nl/projects/unbound/about/
  • Unbound manual pages: https://unbound.docs.nlnetlabs.nl/
  • Quad9: https://quad9.net
  • Arch Wiki, Unbound: https://wiki.archlinux.org/title/unbound
  • Calomel Unbound DNS guide: https://calomel.org/unbound_dns.html