If you’re looking for an easy setup, checkout my review of NextDNS: DoT and DoH provider for easy ADBlocking.

Introduction

Traditional DNS queries and responses are sent over UDP or TCP without encryption. This is vulnerable to eavesdropping and spoofing (including DNS-based Internet filtering). Responses from recursive resolvers to clients are the most vulnerable to undesired or malicious changes, while communications between recursive resolvers and authoritative name servers often incorporate additional protection. (Google)

To simplify, anybody on your network, your ISP, etc … can easily spoof DNS response and decide to send you to a different website than the one you desired. Also, it has some privacy implication where anybody between you and the DNS server can know what website you visit.

Guide

The guide is divided in multiple part. The first one covers how to setup a DNS-over-HTTPS (DoH) while using dnscrypt-proxy as DNS server to answer the requests.

The second part explains how to make couple of changes to that configuration to have PiHole (dns server that block ads) as DNS server behind DoH.

The third part explains how to add DNS-over-TLS to your setup. Useful if you own Android 9 (Pie) devices.

The last  part will provide you with a list of client for Windows, Linux, Android and iOS that supports DoH natively to be able to use it on all your devices.

Server

I advise you to setup a free f1 micro instance at Google Cloud Computing. You can setup anywhere you want, I only advise there because they have a good image for Ubuntu 18.04 and the f1 micro instance is free forever. All the request the machine will do will be encrypted and not accessible by Google.

Again, if you’re more familiar with Digital Ocean, AWS, etc … please use the hosting provider you know the best. For this guide, I only advise you to have a Debian based image (Debian, Ubuntu, etc …)

Architecture

Dnscrypt-Proxy

Let’s start by installing dnscrypt-proxy. This is a client that will take care to forward securely all the DNS requests your devices are going to do to your server. It uses either its own protocol (dnscrypt) or DoH.

I’m choosing Dnscrypt-proxy because it provides a fair range of server in all the world provided by the community or by big players (like google, cloudflare, etc …).

One of the contributor provides a PPA to help us install and keep the program up-to-date.

sudo add-apt-repository ppa:shevchuk/dnscrypt-proxy
sudo apt install dnscrypt-proxy

Once installed, the service will start automatically with your machine.

Configuration

To make thing simple in the guide, I’m using the DNS server of Cloudflare. In case you don’t want to use their servers,  you have the full list of available servers on the application website. Keep in mind you can choose more than one.

Open the file /etc/dnscrypt-proxy/dnscrypt-proxy.toml in your favorite editor. Find the general section and change the server_name variable.

server_names = ['cloudflare']

Once done, restart the service.

sudo systemctl restart dnscrypt-proxy

By default, the program use the socket library of systemd to listen 127.0.2.1:53.

And that’s it for dnscrypt-proxy.

DNS-over-HTTPS server

The next step is to install the server that implement the DoH protocol to get an HTTP request and do a DNS request.

I provide 2 ways to install it, either you download the deb I provide or you compile the program (in golang) yourself.

Download

For this tutorial, I’ve taken the time to compile and package DNS-over-HTTPS (Golang) and provide a deb file easily installable.

Disclaimer

Disclaimer

Antoine Aflalo is furnishing this item “as is”. Antoine Aflalo does not provide any warranty of the item whatsoever, whether express, implied, or statutory, including, but not limited to, any warranty of merchantability or fitness for a particular purpose or any warranty that the contents of the item will be error-free.
In no respect shall Antoine Aflalo incur any liability for any damages, including, but limited to, direct, indirect, special, or consequential damages arising out of, resulting from, or any way connected to the use of the item, whether or not based upon warranty, contract, tort, or otherwise; whether or not injury was sustained by persons or property or otherwise; and whether or not loss was sustained from, or arose out of, the results of, the item, or any services that may be provided by Antoine Aflalo.

Compile

If you prefer to build it yourself, you can follow the guide provided in the GitHub repository.

After compiling you can use FPM to build the package.
fpm repository

fpm -s dir -t deb -n doh-server --config-files /etc/dns-over-https/doh-server.conf -v 2.0.1 \
  --deb-systemd systemd/doh-server.service \
  doh-server/doh-server=/usr/local/bin/ \
  doh-server/doh-server.conf=/etc/dns-over-https/

Install

If you compile it yourself, you won’t need to do this, the make install will have already taken care of it.

sudo dpkg -i doh-server_*_amd64.deb

This will install and start the service for you.

Configuration

Open the file /etc/dns-over-https/doh-server.conf in your favorite editor. Keep somewhere the listen IP/Port. We’ll need it when we’ll setup Nginx.

Change upstream variable.

# HTTP listen port
listen = [
    "127.0.0.1:8053",
    "[::1]:8053",
]

# TLS certification file
# If left empty, plain-text HTTP will be used.
# You are recommended to leave empty and to use a server load balancer (e.g.
# Caddy, Nginx) and set up TLS there, because this program does not do OCSP
# Stapling, which is necessary for client bootstrapping in a network
# environment with completely no traditional DNS service.
cert = ""

# TLS private key file
key = ""

# HTTP path for resolve application
path = "/dns-query"

# Upstream DNS resolver
# If multiple servers are specified, a random one will be chosen each time.
upstream = [
    "127.0.2.1:53",
]

# Upstream timeout
timeout = 60

# Number of tries if upstream DNS fails
tries = 10

# Only use TCP for DNS query
tcp_only = false

# Enable logging
verbose = false

This will tell DoH-server to use our dnscrypt-proxy to do its DNS requests.

Once done, restart the service.

sudo systemctl restart doh-server

Nginx

This section focus on installing and configuring Nginx to take care of the HTTPS part of DNS-over-HTTPS. To do this, we configure it as a reverse proxy and use let’s encrypt to generate a certificate.

Install

We add the PPA with TLS 1.3 of Nginx to get the latest stable version with TLS 1.3.

sudo add-apt-repository ppa:ondrej/nginx
sudo apt install nginx-full

Configuration

This is an example of a configuration. You need to change the server_name to the domain you’ll use for DoH. Also check that the uptream server point to doh-server ip and port. If you didn’t change anything in the configuration of doh-server, it’s already configured correctly.

For now, we don’t enable SSL, this will be done after with certbot & let’s encrypt.

upstream dns-backend {
    server 127.0.0.1:8053;
}

server {
        listen 80;
        server_name dns.example.com;
        root /var/www/html/dns;
        access_log /var/log/nginx/dns.access.log;

         location /dns-query {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
                proxy_set_header X-NginX-Proxy true;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_redirect off;
                proxy_set_header        X-Forwarded-Proto $scheme;
                proxy_read_timeout 86400;
                proxy_pass http://dns-backend/dns-query ;
        }
}

Put the content of the configuratione into /etc/nginx/sites-available/dns-over-https.

Then do a symlink to the enabled folder. Ask Nginx to check that to configuration works, and reload nginx.

sudo ln -s /etc/nginx/sites-available/dns-over-https /etc/nginx/sites-enabled/dns-over-https
sudo nginx -t
sudo systemctl reload nginx

And there you go, you have now Nginx that will takes care of serving HTTP request to doh-server.

Stapling

The idea is to make Nginx take care of checking if the certificate is expired and keep that information in cache. This is to avoid doing too many requests on the Certificate Authority (CA) of the certificate.

Definition

OCSP stapling, formally known as the TLS Certificate Status Request extension, is an alternative approach to the Online Certificate Status Protocol (OCSP) for checking the revocation status of X.509 digital certificates.[1] It allows the presenter of a certificate to bear the resource cost involved in providing OCSP responses by appending (“stapling”) a time-stamped OCSP response signed by the CA to the initial TLS handshake, eliminating the need for clients to contact the CA.[2][3] (Wikipedia)

Create a new file into /etc/nginx/conf.d/stapling.conf with the following content:

ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.2.1;

This will activate the stapling for all your website hosted with Nginx and using HTTPS.
Feel free to change the resolver variable. By default I made it use the dnscrypt-proxy we configured, but you can change it to any other DNS server.

Certbot

Certbot is the tool developed by EFF to help you request SSL certificate using let’s encrypt. Not only it will generate a certificate for your domain, it will also configure Nginx for you and take care of renewing the certificate.

Install

Usually the version available in the distribution is a little old. We’re going to use the official PPA.

sudo add-apt-repository ppa:certbot/certbot
sudo apt install python-certbot-nginx

Configuration

Certbot provides a variety of ways to obtain SSL certificates, through various plugins. The Nginx plugin will take care of reconfiguring Nginx and reloading the config whenever necessary:

sudo certbot --nginx -d dns.example.com

This runs certbot with the --nginx plugin, using -d to specify the names we’d like the certificate to be valid for.

If this is your first time running certbot, you will be prompted to enter an email address and agree to the terms of service. After doing so, certbot will communicate with the Let’s Encrypt server, then run a challenge to verify that you control the domain you’re requesting a certificate for.

If that’s successful, certbot will ask how you’d like to configure your HTTPS settings.

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
-------------------------------------------------------------------------------
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel):

I advise to choose redirect to be sure it use only HTTPS.

SSL Defaults

Certbot comes with “good-enough” SSL defaults, but they haven’t been updated in a while. It keeps support for TLS1.0 which has been deprecated for years. No device should use it anymore. Moreover the chosen cypher list contains weak cyphers. To resolve this issue, I compiled a new configuration file for you to replace the weak defaults of Certbot.

Edit the file /etc/letsencrypt/options-ssl-nginx.conf and replace its content by this.

# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.

ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;

ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;


# Enable modern TLS cipher suites
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';

# The order of cipher suites matters
ssl_prefer_server_ciphers on;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Then reload nginx.

sudo systemctl reload nginx

Renewal

By design, the certificate will expire in 90 days. Certbot will take care of renewing it 30 days before expiry. In the case you want to test the renewal process you can run this command.

If you remove the --dry-run, you’ll actively ask Certbot to renew the certificate.

sudo certbot renew --dry-run

Conclusion

Congratulation you have now a DNS-over-HTTPS server running that can accept request at https://dns.example.com/dns-query.

This conclude the first part of the guide. The second convers the differents clients available, like dnscrypt-proxy (windows/linux) and Intra (Android). And the third one how to make this DoH block advertising.