Configuring Encrypted DNS with local_unbound (DNS-over-TLS)

Hi guys!

I'm writing this post to provide you a quick step by step guide that I successfully implemented in my laptop after weeks of searching and following the test-error-test=success, it was a headache to me, but I made it.

This is my way to give something back to the BSD community. And I really hope this might be helpful for you. :)


DNS DoT, a quick guide.

===============================================================================================
Step 1 — Take back the control of the resolver.

Open the resolvconf control file:

sudo ee /etc/resolvconf.conf (either ee, pico, nano etc)

Point the auto-generated outputs to the null device so nothing overwrites your DNS files automatically:

resolv_conf="/dev/null"
unbound_conf="/dev/null"


The finished /etc/resolvconf.conf should look like this:

# This file was generated by local-unbound-setup.
# Modifications will be overwritten.
libc="NO"
unbound_conf="/dev/null"
resolv_conf="/dev/null"
unbound_pid="/var/run/local_unbound.pid"
unbound_service="local_unbound"
unbound_restart="service local_unbound reload"

=============================================================================================
Step 2 — Make sure the system points at the local resolver.

sudo ee /etc/resolv.conf

It must contain exactly:

shnameserver 127.0.0.1
options edns0


Because of the fact we have sent resolvconf to /dev/null and this file is no longer updated automatically, so now our system relies entirely on local_unbound.

==================================================================================================

Step 3. Edit the rc.conf file by adding the local unbound service and make sure to keept empty the dhclient.conf:

sudo ee /etc/rc.conf

local_unbound_enable="YES"

if you want to do a quick check: grep local_unbound /etc/rc.conf


Now, take a look on the file located in /etc/dhclient.conf it has to be empty now, so DHCP cannot inject its own nameservers:

sudo ee /etc/dhclient.conf (it should return nothing)

===========================================================================

Step 4 — Install the CA bundle and copy it into the chroot.

(It installs the root certificates from certificate authorities, which are necessary for Unbound to verify that dns.quad9.net is truly Quad9 and not an impostor
(defense against MITM- man-in-the-middle)

sudo pkg install ca_root_nss

# Create the target directory inside the chroot:

sudo mkdir -p /var/unbound/etc/ssl


# Copy the certificate INSIDE the chroot, (this is crucial and extremely important):

sudo cp /usr/local/share/certs/ca-root-nss.crt /var/unbound/etc/ssl/cert.pem


Quick note: The reason why: now the certificate lives inside "the world" that Unbound can see.
From its caged perspective, the path will be /etc/ssl/cert.pem (relative to its chroot), which does exist

(Below these steps are explanations)
==========================================================================================================

Step 5. Define the encrypted forwarders (I decided go for quad9 but you can choose the DNS provider you like)

sudo ee /var/unbound/forward.conf and add:

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
forward-addr: 2620:fe::fe@853#dns.quad9.net
forward-addr: 2620:fe::9@853#dns.quad9.net

====================================================================================================

Finally the step 6 — Point Unbound at the certificate bundle (chroot-relative path).

sudo ee /var/unbound/conf.d/tls.conf

server:
tls-cert-bundle: "/etc/ssl/cert.pem"


===============================================================================

Restart and verify:

sudo service local_unbound restart

sudo local-unbound-control list_forwards

it should come back with
. IN forward 9.9.9.9 149.112.112.112 2620:fe::fe 2620:fe::9

Make the quad9 test (it requires to have installed already the bind-tools)

dig +short txt proto.on.quad9.net.

it should return in the terminal:

"dot"

Now all good :)


==============================================

The reasons:



Why should we add /dev/null in resolvconf.conf?


Because it switches off FreeBSD's automatic DNS "helpers." Ussually the resolvconf and DHCP rewrite our DNS files whenever the network changes.
Sending their output to /dev/null freezes our hand-written configuration so nothing can silently overwrite it ;)


Why does resolv.conf point to 127.0.0.1?

That is the address of our own machines.
It means you are telling every app, "send all DNS questions to the resolver running locally," instead of to the router or the ISP.
The "options edns0" enables the protocol extension that DNSSEC and larger, modern responses require.


Why do we copy the certificate into /var/unbound?

local_unbound runs in a chroot (a jailed mini-filesystem) for safety. Inside that jail it cannot reach /usr/local/share/certs, so it needs its own copy of the CA bundle to check that the upstream server's TLS certificate is authentic.

Why do we need to install the CA Bundle and Copy it into the Chroot Jail?

The Chroot Paradox

For security reasons, FreeBSD runs Unbound inside a chroot jail isolated at /var/unbound.
To Unbound, this directory is its entire universe (its new root directory /). It cannot see or access any files outside of it.
If an attacker ever compromises the DNS service, they remain trapped inside this cage, unable to touch the rest of your operating system.

However, configuring encrypted DNS protocols like DNS-over-TLS (DoT) or DNS-over-HTTPS (DoH) creates a paradox.

To protect you against Man-in-the-Middle attacks, Unbound must use Root Certificate Authority (CA) bundles to verify that upstream servers (like Quad9) are legitimate.

The standard FreeBSD certificate package installs these global Mozilla certificates outside the jail at /usr/local/share/certs/ca-root-nss.crt. Because Unbound is trapped in its cage, it is completely blind to them.

To fix this, you must manually place a copy of the certificate bundle inside Unbound's isolated world so it can successfully validate your encrypted upstream DNS servers.

By copying the file here, it now lives within the boundaries of Unbound's restricted environment. From its caged perspective, the path resolves perfectly to /etc/ssl/cert.pem, giving your encrypted DNS service the tools it needs to secure your traffic while remaining safely sandboxed.


Why forward-tls-upstream: yes and @853#dns.quad9.net? (this is my personal preference)

This is the core of encryption. forward-tls-upstream: yes wraps every query in TLS. @853 uses the dedicated DNS-over-TLS port.
#dns.quad9.net is the name the server's certificate must match (this is what stops any attacker from impersonating Quad9)


Why Quad9 (9.9.9.9)? Its secured endpoint validates DNSSEC and blocks known malicious domains at the resolver level, so a compromised or phishing host name never even resolves.



Some possible advantages:


Privacy: every lookup travels encrypted, so your ISP or anyone on the local network cannot read which sites you visit.

Authenticity: certificate verification (#dns.quad9.net) prevents man-in-the-middle spoofing of the resolver.

Integrity: DNSSEC validation rejects forged or tampered DNS records.

Threat blocking: Quad9's secured endpoint drops known-malicious domains before they resolve.

Speed: the local cache answers repeated queries instantly, cutting latency.

Stability & control: DHCP can no longer hijack or overwrite your DNS settings — the configuration stays exactly as you set it.

Smaller attack surface: you no longer depend on the router's or ISP's resolver behavior.



Unbound vs. No Unbound

The one-line difference:
Without Unbound, your machine trusts whatever resolver the network hands it and asks its questions out loud in plain-text
With Unbound, your machine runs its own private, trusted resolver that remembers, verifies, and encrypts.

Without local_unbound, your /etc/resolv.conf points to an external server — usually the one your router or ISP supplies over DHCP.

As a result:

Plain-text (port 53): every query and answer is visible to the ISP and to anyone on the same network.
No local cache: each lookup is a fresh round trip to the upstream, adding latency.
No local validation: you trust the upstream to have done DNSSEC checking — and to be honest about it.
No leverage: the network can silently redirect or filter your DNS, and you would not notice.


With local_unbound (as configured here)

A caching, validating resolver runs at 127.0.0.1. Every program on the system talks only to it, and it does the heavy lifting:


Caches answers, so repeated lookups are near-instant.
Validates DNSSEC locally, rejecting forged records.
Forwards over TLS to Quad9, so the queries leaving your machine are encrypted and the upstream is cryptographically authenticated.
Insulates the rest of the OS from network-level DNS tampering — the system never speaks to the outside resolver directly.
 
Back
Top