This tutorial shows how to set up a secure DNS server in your home network, enable DNS-over-TLS and DNSSEC to protect your DNS privacy. I will also show how to test and examine the setup to make sure everything is configured correctly.

This is what you need before you begin:

This tutorial assumes that Raspbian Buster Lite (September 2019) and Pi-hole (v4.3.2) are up and running.

Installing unbound

sudo apt install -y unbound
sudo systemctl enable unbound

Basic resolver

This is the minimal configuration you can have. It will just forward requests and cache DNS responses.

/etc/unbound/unbound.conf.d/pihole.conf

server:
	port: 5300

forward-zone:
	name: "."
    forward-addr: 46.182.19.48  # digitalcourage.de

Check config-file for errors with unbound-checkconf

henry@pizero:[~]: unbound-checkconf
unbound-checkconf: no errors in /etc/unbound/unbound.conf

Restart unbound: sudo systemctl restart unbound

Test resolver: dig @::1 -p 5300 mozilla.org

henry@pizero:[~]: dig @::1 -p 5300 mozilla.org

; <<>> DiG 9.11.5-P4-5.1-Raspbian <<>> @::1 -p 5300 mozilla.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 618
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;mozilla.org.			IN	A

;; ANSWER SECTION:
mozilla.org.		28	IN	A	63.245.208.195

;; Query time: 317 msec
;; SERVER: ::1#5300(::1)
;; WHEN: Wed Jan 15 18:14:19 GMT 2020
;; MSG SIZE  rcvd: 56

Check DSN-Traffic with tcpdump and wireshark:

First, restart unbound to clear the cache: sudo systemctl restart unbound

Open another terminal and run tcpdump:

henry@pizero:[~]: sudo tcpdump port 53 -w basic_dns.pcap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

Re-run the DNS query in the first terminal dig @::1 -p 5300 mozilla.org. When it’s done, exit tcpdump with CTRL-C:

14 packets captured
14 packets received by filter
0 packets dropped by kernel

Opening the trace with wireshark reveals the DNS traffic which is currently unencrypted:

dns-simple

DNS-over-TLS

Update the certificates with: sudo update-ca-certificates

Modify the configuration file /etc/unbound/unbound.conf as follows:

server:
    port: 5300
    tls-upstream: yes                                          
    tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"

forward-zone:
    name: "."
    forward-addr: 2a05:fc84::42@853#dns.digitale-gesellschaft.ch

Test with dig @::1 -p 5300 mozilla.org

henry@pizero:[~]: dig @::1 -p 5300 mozilla.org

; <<>> DiG 9.11.5-P4-5.1-Raspbian <<>> @::1 -p 5300 mozilla.org
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31328
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;mozilla.org.			IN	A

;; ANSWER SECTION:
mozilla.org.		60	IN	A	63.245.208.195

;; Query time: 1019 msec
;; SERVER: ::1#5300(::1)
;; WHEN: Sun Jan 12 07:49:16 GMT 2020
;; MSG SIZE  rcvd: 56

Check DNS-Traffic

Make a DNS request dig @::1 -p 5300 mozilla.org while capturing the traffic with sudo tcpdump host 2a05:fc84::42 -w tls.pcap

DoT

All DNS traffic is now wrapped in a TLS connection.

DNSSEC

To protect the DNS-responses against modification, we will use DNSSEC. Unbound checks DNS responses against known public keys. These keys MUST be updated initially and kept up to date regularly. The initial update must be done manually, whereas unbound updates them regularly while running.

Make sure that the key-file ist part of your unbound-configuration:

henry@pizero:[~]: cat /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf 
server:
    # The following line will configure unbound to perform cryptographic
    # DNSSEC validation using the root trust anchor.
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

Update the keys sudo -u unbound unbound-anchor and restart unbound sudo systemctl restart unbound.

Testing with a valid DNSSEC enabled domain:

henry@pizero:[~]: dig @::1 -p 5300 mozilla.org +dnssec

; <<>> DiG 9.11.5-P4-5.1-Raspbian <<>> @::1 -p 5300 mozilla.org +dnssec
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20519
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 4096
;; QUESTION SECTION:
;mozilla.org.			IN	A

;; ANSWER SECTION:
mozilla.org.		44	IN	A	63.245.208.195
mozilla.org.		44	IN	RRSIG	A 7 2 60 20200115032436 20200112022436 45629 mozilla.org. DlXn7EgUdqlwyMtSDExb4t+d1534Aef9jMkZZFXVERjths9Lhe2i9eWh tgbmz9AS6eGh8K1M0ZMaEztzsBaDn8e6JsrCAnGk+rU51iwAwk5pJVFP ERMk+Sq1fGoeKaWNlImD4S3sidr3N/zRDdh76s9zgBtIHY8RCqagRBP7 L1k=

;; Query time: 162 msec
;; SERVER: ::1#5300(::1)
;; WHEN: Sun Jan 12 14:25:35 GMT 2020
;; MSG SIZE  rcvd: 227

Note the NOERROR-status and ad-flag.

Negative-test:

henry@pizero:[~]: dig @::1 -p 5300 dnssec-failed.org +dnssec

; <<>> DiG 9.11.5-P4-5.1-Raspbian <<>> @::1 -p 5300 dnssec-failed.org +dnssec
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 24444
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 4096
;; QUESTION SECTION:
;dnssec-failed.org.		IN	A

;; Query time: 2313 msec
;; SERVER: ::1#5300(::1)
;; WHEN: Sun Jan 12 14:27:36 GMT 2020
;; MSG SIZE  rcvd: 46

Status is SERVFAIL and no address was resolved.

Tweaks

The final config with 4 upstream resolvers incl. some minor tweaks for speed/privacy:

server:
# basic
    port: 5300

# IPv4
    interface: 0.0.0.0@5300
    access-control: 192.168.178.0/24 allow

# IPv6
    interface: ::0@5300
    access-control: fe80::/10 allow

# TLS settings
    tls-upstream: yes
    tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"

# performance optimizations (costs more traffic and/or CPU)
    prefetch: yes
    prefetch-key: yes
    rrset-roundrobin: yes
    qname-minimisation-strict: yes
    hide-identity: yes
    hide-version: yes

# upstream resolver settings
forward-zone:
    name: "."
    forward-addr: 2a05:fc84::42@853#dns.digitale-gesellschaft.ch
    forward-addr: 2a01:3a0:53:53::@853#unicast.censurfridns.dk
    forward-addr: 2a02:2970:1002::18@853#dns2.digitalcourage.de
    forward-addr: 2a03:b0c0:0:1010::e9a:3001@853#dot.securedns.eu

Pi-hole config

The only thing left is to set the local unbound-instance as upstream resolver:

Pi-Hole DNS

Privacy

Encrypting DNS-Traffic with DNS-over-TLS can NOT completely protect your privacy. The DNS-requests and -responses will be encrypted and are authenticated with DNSSEC. But on connecting to the resolved domain via TLS the visited domain is readable in plaintext in the HTTPS-Traffic via the Server Name Indication (SNI) extension:

Capture traffic in 1st terminal: sudo tcpdump "tcp port 443" -w https.pcap

henry@pizero:[~]: sudo tcpdump "tcp port 443" -w https.pcap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

Create HTTPS request in another terminal: curl -I https://cyclemap.link

henry@pizero:[~]: curl -I https://cyclemap.link
HTTP/2 200 
content-type: text/html
content-length: 33001
date: Sun, 12 Jan 2020 09:28:56 GMT
last-modified: Sat, 19 Oct 2019 08:33:39 GMT
etag: "a0a1914e588d03e34546d4ef595627b0"
accept-ranges: bytes
server: AmazonS3
vary: Accept-Encoding
x-cache: Hit from cloudfront
via: 1.1 c5c25772c7f14e267596e0f8ce51d9bc.cloudfront.net (CloudFront)
x-amz-cf-pop: FRA53-C1
x-amz-cf-id: HYQeKS19JhtyPhvI2b-z-kJBOxJaq-rFiIltrtorW8710w7EnYj-lQ==
age: 155

SNI

To prevent leaking this information we need to encrypt the SNI.

Debugging

Check unbound logs with:

sudo journalctl -u unbound -n 10 -f

Set-up the Raspberry Pi with Network Watchdog to prevent downtime due to network issues.

Persist logfiles over reboots: Beginners Guide to “journalctl”

References

Know-how

Privacy-aware public Resolver