Programster's Blog

Tutorials focusing on Linux, programming, and open-source

Deploy Stubby Server For DNS Over TLS (DoT)

Introduction

The Problem

  • I would like to use DNS over TLS in order to encrypt my DNS queries so that my ISP (or anyone else outside my network) can't see them.

  • I don't want to have to individually configure each of the machines inside my network to setup DNS over TLS. Instead, I want a "normal" DNS server inside my network perform the DoT work, and just have all my network machines set to use that server for normal DNS queries.

The Solution

This tutorial will show you how to install Stubby to act as a DNS stub server in your network, to take DNS requests and send them to an upstream resolver with DNS over TLS. Thus, the machines in your network can use this server as a traditional DNS server, and unencrypted DNS requests no longer leave your home network.

It's even better if you deploy your own dnsmasq server to act as a stub resolver on your network, and configure it to use stubby for its upstream DNS. That way you can benefit from the DNS caching that dnsmasq provides. Below is a diagram to help explain:

I ended up getting frustrated with performance/dropout issues with Stubby, and switched to using Unbound instead, which I have yet to have problems with, and generally better all around.

Steps

Deploy Debian 12 Server

Deploy a Debian 12 server inside your network. However you do this is up to you. E.g. a VPS inside Proxmox or a pure KVM server etc.

Install Stubby

Install stubby by running:

sudo apt update && sudo apt install stubby -y

Backup The Config

Now we need to configure Stubby to use our prefferred DNS over TLS provider. In this case we are going to use Cloudflare. To do this, first lets create a backup of the default configuration:

sudo cp /etc/stubby/stubby.yml /etc/stubby/stubby.yml.bak

Listen For External Requests

By default, the server will only listen to requests coming from the server itself. This means that it will listen for DNS requests coming from other machines on your network. To change this, we simply need to add 0.0.0.0 to the list of addresses to listen on. Find this section:

################################ LISTEN ADDRESS ################################
# Set the listen addresses for the stubby DAEMON. This specifies localhost IPv4
# and IPv6. It will listen on port 53 by default. Use <IP_address>@<port> to
# specify a different port
listen_addresses:
  - 127.0.0.1
  - 0::1

... and change it to:

################################ LISTEN ADDRESS ################################
# Set the listen addresses for the stubby DAEMON. This specifies localhost IPv4
# and IPv6. It will listen on port 53 by default. Use <IP_address>@<port> to
# specify a different port
listen_addresses:
  - 0.0.0.0
  - 127.0.0.1
  - 0::1

Enable Preferred Resolvers

Find this part:

## Cloudflare 1.1.1.1 and 1.0.0.1
## (NOTE: recommend reducing idle_timeout to 9000 if using Coudflare)
#  - address_data: 1.1.1.1
#    tls_auth_name: "cloudflare-dns.com"
#  - address_data: 1.0.0.1
#    tls_auth_name: "cloudflare-dns.com"

... and uncomment the YAML to become:

## Cloudflare 1.1.1.1 and 1.0.0.1
## (NOTE: recommend reducing idle_timeout to 9000 if using Coudflare)
  - address_data: 1.1.1.1
    tls_auth_name: "cloudflare-dns.com"
  - address_data: 1.0.0.1
    tls_auth_name: "cloudflare-dns.com"

I would also then comment out the servers in the DEFAULT UPSTREAMS section. E.g. this part:

############################ DEFAULT UPSTREAMS  ################################
####### IPv4 addresses ######
### Test servers ###
# The Surfnet/Sinodun servers
  - address_data: 145.100.185.15
    tls_auth_name: "dnsovertls.sinodun.com"
    tls_pubkey_pinset:
      - digest: "sha256"
        value: 62lKu9HsDVbyiPenApnc4sfmSYTHOVfFgL3pyB+cBL4=
  - address_data: 145.100.185.16
    tls_auth_name: "dnsovertls1.sinodun.com"
    tls_pubkey_pinset:
      - digest: "sha256"
        value: cE2ecALeE5B+urJhDrJlVFmf38cJLAvqekONvjvpqUA=
# The getdnsapi.net server
  - address_data: 185.49.141.37
    tls_auth_name: "getdnsapi.net"
    tls_pubkey_pinset:
      - digest: "sha256"
        value: foxZRnIh9gZpWnl+zEiKa0EJ2rdCGroMWm02gaxSc9Q=
####### IPv6 addresses ######
### Test servers ###
# The Surfnet/Sinodun servers
  - address_data: 2001:610:1:40ba:145:100:185:15
    tls_auth_name: "dnsovertls.sinodun.com"
    tls_pubkey_pinset:
      - digest: "sha256"
        value: 62lKu9HsDVbyiPenApnc4sfmSYTHOVfFgL3pyB+cBL4=
  - address_data: 2001:610:1:40ba:145:100:185:16
    tls_auth_name: "dnsovertls1.sinodun.com"
    tls_pubkey_pinset:
      - digest: "sha256"
        value: cE2ecALeE5B+urJhDrJlVFmf38cJLAvqekONvjvpqUA=
# The getdnsapi.net server
  - address_data: 2a04:b900:0:100::38
    tls_auth_name: "getdnsapi.net"
    tls_pubkey_pinset:
      - digest: "sha256"
        value: foxZRnIh9gZpWnl+zEiKa0EJ2rdCGroMWm02gaxSc9Q=

Optional - Reduce the Idle Timeout

Finally, the config comments said that if using Cloudflare, one should reduce the idle_timeout to 9000, so do that too, which can be easily done by executing the following commands:

SEARCH="idle_timeout: .*"
REPLACE="idle_timeout: 9000"
FILEPATH="/etc/stubby/stubby.yml"
sudo sed -i "s;$SEARCH;$REPLACE;g" $FILEPATH

Optional - Enable DNSSEC

Next, it would be good to enable DNS security. This will require DNSSEC validation on all answers, and withhold answers that failed.

You can do this by running the following commands:

SEARCH="# dnssec: .*"
REPLACE="dnssec: GETDNS_EXTENSION_TRUE"
FILEPATH="/etc/stubby/stubby.yml"
sudo sed -i "s;$SEARCH;$REPLACE;g" $FILEPATH

... or manually finding this section:

############################### DNSSEC SETTINGS ################################
# Require DNSSEC validation. This will withhold answers with BOGUS DNSSEC
# status and answers that could not be validated (i.e. with DNSSEC status
# INDETERMINATE). Beware that if no DNSSEC trust-anchor is provided, or if
# stubby is not able to fetch and validate the DNSSEC trust-anchor itself,
# (using Zero configuration DNSSEC) stubby will not return answers at all.
# If DNSSEC validation is required, a trust-anchor is also required.
# dnssec: GETDNS_EXTENSION_TRUE

... and changing it to:

############################### DNSSEC SETTINGS ################################
# Require DNSSEC validation. This will withhold answers with BOGUS DNSSEC
# status and answers that could not be validated (i.e. with DNSSEC status
# INDETERMINATE). Beware that if no DNSSEC trust-anchor is provided, or if
# stubby is not able to fetch and validate the DNSSEC trust-anchor itself,
# (using Zero configuration DNSSEC) stubby will not return answers at all.
# If DNSSEC validation is required, a trust-anchor is also required.
dnssec: GETDNS_EXTENSION_TRUE

Restart Stubby Service

Now restart the stubby service for the changes to take effect:

sudo service stubby restart

Test It's Working

Test that it is working by sending a DNS query to it. E.g.

STUBBY_SERVER_IP=192.168.1.3
dig yahoo.com @$STUBBY_SERVER_IP

If all is working, then you should get some output similar to below:

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> yahoo.com @192.168.1.3
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58423
;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;yahoo.com.         IN  A

;; ANSWER SECTION:
yahoo.com.      1167    IN  A   74.6.231.21
yahoo.com.      1167    IN  A   74.6.143.25
yahoo.com.      1167    IN  A   74.6.231.20
yahoo.com.      1167    IN  A   98.137.11.164
yahoo.com.      1167    IN  A   98.137.11.163
yahoo.com.      1167    IN  A   74.6.143.26

;; Query time: 60 msec
;; SERVER: 192.168.1.3#53(192.168.1.3) (UDP)
;; WHEN: Sat Mar 01 12:32:16 GMT 2025
;; MSG SIZE  rcvd: 188

Side Note - Additional Testing

I took the time to use my Proxmox firewall to block off port 53 outbound from the Stubby virtual machine, except to my computer (client). I then ran a dig query against the stubby server, which still came back with results. This is good as it indicates that Stubby wasn't just using traditional DNS over port 53 as otherwise that wouldn't have worked.

I then blocked off port 853 TCP from my stubby server before re-running the dig query. This came back with no results, indicating that stubby is indeed actually performing DNS over TLS with port 853.

Debugging

Truncated Responses

After having initially implemented this, I kept hitting DNS issues, with certain domains causing my downstream Dnsmasq server to show "reply is truncated". It turns out that this is because UDP packets are only so large, and DNSSEC responses can often not fit within them. I had initially configured my Dnsmasq server to only be listening on port 53 with UDP, but as soon as I reconfigured it to also listen for TCP, my issues went away. This way the information can be safely sent over the network, utilizing multiple packets. The key thing is there was nothing wrong with stubby, but my downstream Dnsmasq server.

Conclusion

You now have a stubby server configured to perform DNS over TLS for you. Now it's up to you how to configure your devices to use it, such as:

  • Configure your already existing DNS server to use it as it's upstream.
  • Configure your DHCP server to set it as the default DNS
  • Manually configure your devices to set the DNS to the IP address of your Stubby server.

References

Last updated: 9th March 2025
First published: 1st March 2025

This blog is created by Stuart Page

I'm a freelance web developer and technology consultant based in Surrey, UK, with over 10 years experience in web development, DevOps, Linux Administration, and IT solutions.

Need support with your infrastructure or web services?

Get in touch