unbound DNS-over-TLS forwarding server on Debian Buster

Table of Contents

Why write yet another HowTo on unbound?

There’s a real galore of unbound-related HowTo’s, including how to setup DNS-over-TLS for increased privacy.  The point of this article is not to go over why it’s important for the privacy-minded.

When setting it up myself in a Debian Buster environment, I stumbled upon several issues.  I’m probably not alone, so I decided to share how to actually get it working properly.

A little bit of context

I believe in sharing actual configuration files that actually works. So, in order for you to understand what’s going on, here is what we’re going to achieve:

  • Our hosts reside in the 172.16.120.0/24 – fd00:470:780b:120::/64 subnet, in a dual stack setup.
  • Our caching DNS server has the 172.16.120.102/fd00:470:780b:120::102 address in that subnet.
  • Our caching DNS server accesses the Internet through another network interface, which also runs in dual stack.

Getting unbound to work on Buster

Debian Buster has a reasonably recent unbound package available. So we can just go ahead and install it:

apt install unbound

As pointed out in https://github.com/NLnetLabs/unbound/issues/56, there are some subtleties about the chroot of unbound of Buster that are not properly documented:

  • The chroot standard location is /var/lib/unbound.
  • If you want the systemd service to work properly, you need to bind mount the /run/systemd folder inside the chroot for proper PID file creation.

First, add the following line to your /etc/fstab to solve the second point:

/run/systemd /var/lib/unbound/run/systemd none defaults,bind 0 0

And refresh your mounts with mount -a.

Next, we need to edit our /etc/unbound/unbound.conf file. Here is a minimum working example based on the reference file found in /usr/share/doc/unbound.

I used Google and OpenDNS as my forwarding servers.

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: 172.16.120.102
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: [IPv6 outgoing interface]
outgoing-interface: [IPv4 outgoing interface]

# Set this to yes to prefer ipv6 upstream servers over ipv4.
prefer-ip6: yes

# upstream connections use TCP only (and no UDP), "yes" or "no"
# useful for tunneling scenarios, default no.
# tcp-upstream: yes

# 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: 172.16.120.0/24 allow
access-control: fd00:470:780b:120::/64 allow

# 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".
# If you give "" no privileges are dropped.
username: "unbound"

# the working directory. The relative files in this config are
# relative to this directory. If you give "" the working directory
# is not changed.
# If you give a server: directory: dir before include: file statements
# then those includes can be relative to the working directory.
directory: "/etc/unbound"

# Log to syslog(3) if yes. The log facility LOG_DAEMON is used to
# log to. If yes, it overrides the logfile.
use-syslog: yes

# request upstream over TLS (with plain DNS inside the TLS stream).
# Default is no. Can be turned on and off with unbound-control.
# tls-upstream: yes

## 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
forward-addr: 2001:4860:4860::8844
forward-addr: 2001:4860:4860::8888

Restart unbound and there it is, you have a working DNS caching server.

Switch to DNS-over-TLS

This is the second step that took me a lot of time because the documentation does contain all the info, but in a non-obvious way.

https://www.ctrl.blog/entry/unbound-tls-forwarding.html covers the issue quite well.

DNS-over-TLS means that the queries from unbound to the upstream servers will be encrypted in a TLS tunnel.  The public servers of Google and OpenDNS support this.  I know, but haven’t used them yet, that other actors like Quad9 also support this.

You should already have them installed, but ensure that you have the CA certificates store from Debian installed:

apt install ca-certificates

Now, we must edit the unbound configuration. The important points are:

  • Ensure the TCP feature of unbound is enabled. Even if you only want to serve UDP answers from the cache, the TCP stack must be enabled in order for the outgoing DNS-over-TLS queries to happen.
  • Add various DNS-over-TLS stanzas to tell unbound to forward queries that way.
  • Change the forwarders definition to specify the port (853) and FQDN of the server. The last part is what is not obvious from other howto’s I’ve read. When you use DNS-over-TLS, you must follow the IP/port couple with a #FQDN stanza. That pretty much looks like a comment, but it is not.

The unbound configuration file then becomes (chosen parts):

server:
  [...]
  tcp-upstream: yes
  tls-upstream: yes
  tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
[...]
forward-zone:
  name: "."
  forward-tls-upstream: yes
  forward-addr: 208.67.222.222@853#resolver1.opendns.com
  forward-addr: 208.67.220.220@853#resolver2.opendns.com
  forward-addr: 8.8.8.8@853#dns.google
  forward-addr: 8.8.4.4@853#dns.google
  forward-addr: 2001:4860:4860::8844@853#dns.google
  forward-addr: 2001:4860:4860::8888@853#dns.google

Observ the forward-addr stanzas: they are in the form ip@port#fqdn, without any whitespace. It is very important that you follow this syntax in order for DNS-over-TLS to work.

After this, just restart unbound, and voilà. You have a working DNS-over-TLS caching server.

The resulting, complete configuration file

For reference, here is my complete, commented configuration file. Dont’ use it as-is in your network, it won’t work. Refer to the beginning of the article to understand how to adapt it.

Anything not specified is left to its default value, as can be found in /usr/share/doc/unbound/examples/unbound.conf.

# The server clause sets the main parameters.
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: 172.16.120.102 
  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: [IPv6 outgoing interface] 
  outgoing-interface: [IPv4 outgoing interface] 

  # Set this to yes to prefer ipv6 upstream servers over ipv4. 
  prefer-ip6: yes 

  # upstream connections use TCP only (and no UDP), "yes" or "no" 
  # useful for tunneling scenarios, default no. 
  tcp-upstream: yes 

  # 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: 172.16.120.0/24 allow 
  access-control: fd00:470:780b:120::/64 allow 

  # 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". 
  # If you give "" no privileges are dropped. 
  username: "unbound" 

  # the working directory. The relative files in this config are 
  # relative to this directory. If you give "" the working directory 
  # is not changed. 
  # If you give a server: directory: dir before include: file statements 
  # then those includes can be relative to the working directory. 
  directory: "/etc/unbound" 

  # Log to syslog(3) if yes. The log facility LOG_DAEMON is used to 
  # log to. If yes, it overrides the logfile. 
  use-syslog: yes 

  # request upstream over TLS (with plain DNS inside the TLS stream). 
  # Default is no. Can be turned on and off with unbound-control. 
  tls-upstream: yes 

  # Certificates used to authenticate connections made upstream. 
  tls-cert-bundle: "/var/lib/unbound/etc/ssl/certs/ca-certificates.crt"

## Forward zones
forward-zone:
  name: "."
  forward-tls-upstream: yes
  forward-addr: 208.67.222.222@853#resolver1.opendns.com
  forward-addr: 208.67.220.220@853#resolver2.opendns.com
  forward-addr: 8.8.8.8@853#dns.google
  forward-addr: 8.8.4.4@853#dns.google
  forward-addr: 2001:4860:4860::8844@853#dns.google
  forward-addr: 2001:4860:4860::8888@853#dns.google

Add a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.