Howto: secure your DNS with a Raspberry Pi, Unbound and Cloudflare



Everything you access on the Internet starts with a Domain Name System (DNS) query to turn a name like into an IP address like Typically the DNS server that provides that answer is run by your Internet Service Provider (ISP) but you might also use alternative DNS servers like Google ( Either way regular DNS isn’t encrypted – it’s just plain text over UDP Port 53, which means that anybody along the way can snoop on or interfere with your DNS query and response (and it’s by no means unheard of for unscrupulous ISPs to do just that).

Cloudflare recently launched their DNS service, which is fast (due to their large network of local data centres plus some clever engineering) and offers some good privacy features. One of the most important capabilities on offer is DNS over TLS, where DNS queries are sent over an encrypted connection, but you don’t get that by simply changing the DNS settings on your device or your home router. To take advantage of DNS over TLS you need your own (local) DNS server that can answer queries in a place where they won’t be snooped or altered; and to do that there are essentially two approaches:

  1. Run a DNS server on every machine that you use. This is perhaps the only workable approach for devices that leave the home like laptops, mobiles and tablets, but it isn’t the topic of his Howto – maybe another day.
  2. Run a DNS server on your home network that encrypts queries that leave the home. I already wrote about doing this with OpenWRT devices, but that assumes you’re able and willing to find routers that run OpenWRT and re-flash firmware (which usually invalidates any warranty). Since many people have a Raspberry Pi, and even if you don’t have one they’re inexpensive to buy and run, this guide is for using a Pi to be the DNS server.


This guide is based on the latest version of Raspbian – Stretch. It should work equally well with the full desktop version or the lite minimal image version.

If you’re new to Raspberry Pi then follow the stock guides on downloading Raspbian and flashing it onto an SD card.

If you’re already running Raspbian then you can try bringing it up to date with:

sudo apt-get update && sudo apt-get upgrade -y

though it may actually be faster to download a fresh image and reflash your SD card.

Installing Unbound and DNS utils

Unbound is a caching DNS server that’s capable of securing the connection from the Pi to Other options are available. This guide also uses the tool dig for some testing, which is part of the DNS utils package.

First ensure that Raspbian has up to date package references:

sudo apt-get update

Then install Unbound and DNS utils:

sudo apt-get install -y unbound dnsutils

At the time of writing this installs Unbound v1.6.0, which is a point release behind the latest v1.7.0, but good enough for the task at hand. Verify which version of Unbound was installed using:

unbound -h

which will show something like:

usage:  unbound [options]
        start unbound daemon DNS resolver.
-h      this help
-c file config file to read instead of /etc/unbound/unbound.conf
        file format is described in unbound.conf(5).
-d      do not fork into the background.
-v      verbose (more times to increase verbosity)
Version 1.6.0
linked libs: libevent 2.0.21-stable (it uses epoll),
OpenSSL 1.1.0f  25 May 2017
linked modules: dns64 python validator iterator
BSD licensed, see LICENSE in source package for details.
Report bugs to [email protected]

At this stage Unbound will already be running, and the installer will have taken care of reconfiguring Raspbian to make use of it; but it’s not yet set up to provide service to other machines on the network, or to use

Configuring Unbound

The Raspbian package for unbound has a very simple configuration file at /etc/unbound/unbound.conf that includes any file placed into /etc/unbound/unbound.conf.d, so we need to add some config there.

First change directory and take a look at what’s already there:

cd /etc/unbound/unbound.conf.d

There should be two files: qname-minimisation.conf and root-auto-trust-anchor-file.conf – we don’t need the former as it will be part of the config we introduce, so remove it with:

sudo rm qname-minimisation.conf

Create an Unbound server configuration file:

sudo bash -c 'cat >> /etc/unbound/unbound.conf.d/unbound_srv.conf \
 tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
 qname-minimisation: yes
 do-tcp: yes
 prefetch: yes
 rrset-roundrobin: yes
 use-caps-for-id: yes
 do-ip6: no
 access-control: allow

It’s necessary to use ‘sudo bash’ and quotes around the content here as ‘sudo cat’ doesn’t work with file redirection.

These settings are mostly lifted from ‘What is DNS Privacy and how to set it up for OpenWRT‘ by Torsten Grote, but it’s worth taking a look at each in turn to see what they do (based on the unbound.conf docs):

  • qname-minimisation: yes – Sends the minimum amount of information to upstream servers to enhance privacy.
  • do-tcp: yes – This is the default, but better safe than sorry – DNS over TLS needs a TCP connection (rather than UDP that’s normally used for DNS).
  • prefetch: yes – Makes sure that items in the cache are refreshed before they expire so that the network/latency overhead is taken before a query needs an answer.
  • rrset-roundrobin: yes – Rotates the order of Round Robin Set (RRSet) responses according to a random number based on the query ID.
  • use-caps-for-id: yes – Use 0x20-encoded random bits in the query to foil spoof attempts. This perturbs the lowercase and uppercase of query names sent to authority servers and checks if the reply still has the correct casing. Disabled by default. This feature is an experimental implementation of draft dns-0x20.
  • do-ip6: no – Turns off IPv6 support – obviously if you want IPv6 then drop this line.
  • interface: – Makes Unbound listen on all IPs configured for the Pi rather than just localhost ( so that other machines on the local network can access DNS on the Pi.
  • access-control: allow – Tells Unbound to allow queries from any IP (this could/should be altered to be the CIDR for your home network).

Next configure Unbound to use Cloudflare’s DNS servers:

sudo bash -c 'cat >> /etc/unbound/unbound.conf.d/unbound_ext.conf \
    name: "."
    forward-addr: [email protected]
    forward-addr: [email protected]
    forward-addr: [email protected]
    forward-addr: [email protected]
    forward-tls-upstream: yes

There are two crucial details here:

  1. @853 at the end of the primary and secondary server IPs tells unbound to connect to Cloudflare using port 853, which is the secured end point for the service.
  2. forward-ssl-upstream: yes – is the instruction to use DNS over TLS, in this case for all queries (name: “.”)

Restart Unbound to pick up the config changes:

sudo service unbound restart


First check that the Unbound server is listening on port 53:

netstat -an | grep 53

Should return these two lines (not necessarily together):

tcp    0    0*    LISTEN
udp    0    0*

Then try looking up


The output should end something like:

;; ANSWER SECTION:    169    IN    A

;; Query time: 3867 msec
;; WHEN: Wed Apr 04 14:46:09 UTC 2018
;; MSG SIZE  rcvd: 55

Note that the Query time is pretty huge when the cache is cold[1]. Try again and it should be much quicker.

;; ANSWER SECTION:    169    IN    A
;; Query time: 0 msec
;; WHEN: Wed Apr 04 14:46:09 UTC 2018
;; MSG SIZE rcvd: 55

Using it

Here’s where you’re somewhat on your own – you have a Pi working as a DNS server, but you now need to reconfigure your home network to make use of it. There are two essential elements to this:

  1. Make sure that the Pi has a known IP address – so set it up with a static IP, or configure DHCP in your router (or whatever else is being used for DHCP) to have a reservation for the Pi.
  2. Configure everything that connects to the network to use the Pi as a DNS server, which will usually be achieved by putting the (known from the step above) IP of the Pi at the top of the list of DNS servers passed out by DHCP – so again this is likely to be a router configuration thing.


10 Apr 2018 – Thanks to comments from Quentin and TJ I’ve added the access-control line to the server config. Also check out Quentin’s repo (and Docker Hub) for putting Unbound into a Docker container.
4 Jun 2018 – Modified the config to use per comment from Daniel Aleksandersen (see this thread for background).
20 Jul 2018 – I was just trying to use the Pi that I first tested this on, and it was stubbornly refusing to work. The issue turned out to be time. The Pi hadn’t got an NTP sync when it came up, and was unable to establish a trusted connection with Cloudflare when the clock was too far off.
27 Apr 2021 – Updated configs for correct name validation.


[1] Almost 4 seconds is ridiculous, but that’s what I’m getting with a Pi on a WiFi link to a US cable connection. From my home network it’s more like 160ms. I suspect that Spectrum might be tarpitting TCP to


40 Responses to “Howto: secure your DNS with a Raspberry Pi, Unbound and Cloudflare”

  1. Hey, thanks for the tutorial ! I’m trying to connect to my Raspberry Pi directly from my Windows 10 computer. So I set the DNS server to the LAN IP address of my Raspberry Pi but it seems like it tries to connect on port 53 UDP only (which fails) and not 53 TCP. Any idea how to resolve this ? Thanks!

    • The clients should be connecting on 53 UDP, so that’s fine, but a firewall (on the Pi) could be standing between the Windows PC and the Pi. Are you using nslookup on the PC to test (using ‘server yo.ur.pi.ip’ so that’s). You can also use netstat on the Pi to see the incoming connections.

      • 3 qmcgaw

        Hi Chris, thanks for your reply ! Hopefully I won’t go too much out of scope from the Raspberry Pi ! I’m trying to run unbound on a Docker container (based on Alpine, quite simple) to connect to Cloudflare over TLS. Using nslookup.exe, when I enter `server` (IP of my Docker host – NAS machine) I obtain :

        Default Server: []

        and I would obtain a “DNS request timed out.” if the container is not running, as expected. That’s promising. On the other hand, when configuring it in the Internet Protocol Version 4 (TCP/IPv4) of the Ethernet adapter properties in Windows 10, I still obtain the error “Your DNS server might be unavailable”. If the container is not running, I obtain the expected error “The DNS server isn’t responding”.

        My unbound configuration is:

        verbosity: 5
        qname-minimisation: yes
        do-tcp: yes
        prefetch: yes
        rrset-roundrobin: yes
        use-caps-for-id: yes
        do-ip4: yes
        do-ip6: yes
        name: “.”
        forward-addr: [email protected]
        forward-addr: [email protected]
        forward-addr: 2606:4700:4700::[email protected]
        forward-addr: 2606:4700:4700::[email protected]
        forward-ssl-upstream: yes

        The repository is available at if you want to have a look. Thanks !

      • Docker adds some complexity around port mapping, but looking at your Dockerfile and docker-compose.yml you have that all figured out.

        So it becomes the usual step by step troubleshooting process…

        1. Throw dig (and bash) into the container, and get a shell to it and test from within
        2. Test from the Pi itself
        3. Test from beyond the Pi

        One of the other comments noted that they had to add an ‘access-control:’ which wasn’t needed for my testing, but that might also be worth a shot.

      • 5 qmcgaw

        Hi Chris, thanks again for the help. It’s now working and was indeed caused by the access-control setting in unbound.conf. Thanks to TJ for the help as well ! Now the container is fully working with “access-control:”.

      • Cool – I’m glad to hear you got it working. I’ve backported that line into the post and noted your repo in an update. Will you be putting it on Docker Hub (as I can link to that too and provide an example Docker run line)?

      • 7 qmcgaw

        It’s at

        Note that might be too permissive, I will test with more restrictive configurations.

      • is a good match for the interface:, and yes it’s permissive, but if somebody’s opening up their Pi to the outside world then this parameter isn’t what they should be depending on for access control.

  2. 9 TJ

    Thank you for the nice writeup. FWIW, I had to add an access control to the unbound configuration file to get it to work with my local network clients (I think unbound blocks everything but localhost by default). I added the following to unbound_srv.conf (my local network is and it worked:
    access-control: allow

    Again – thanks for taking the time to write this up – very helpful.

  3. 10 george

    Thanks so much for putting this together. It really is well written and I appreciate the details provided on the settings that were chosen.

    Quick comment regarding Quentin’s Docker repo. I wasn’t able to get that to run in docker on raspi without updating to work on an ARM platform. I didn’t work through it because I’m not sure I could come up with a good use case to run unbound in docker on a raspberry pi (other than I wanted to try it ;-)). I did however, steal his inclusion of the malicious sites blacklist.

    Thanks again

  4. You’re not validating the TLS certificates in this setup. Your DNS requests can still be intercepted and modifid with a self-signed certificate.

    It should be:

    forward-addr: [email protected]
    forward-addr: [email protected]

    The # character as used here is not a comment but actually tells unbound what domain to validate the certificates against.

  5. Hi Chris! Thanks for the excellent guide.

    Do you have a way to verify that this is working properly? I have it set up as you described, but I’m not sure that Unbound is actually using TLS. With Wireshark, I can see that normal DNS (53) traffic is going out to the Rpi. I’m assuming it’s being forwarded from there over TLS, but I’d like to confirm.

    • I just used netstat on the Pi to confirm it was connecting to the right ports on CloudFlare when I made DNS queries to it.

      There’s probably a lot more I could do to confirm the right certificates are being exchanged etc.

    • Hi mattersubject,

      I believe you could see more information from unbound by setting the verbosity to 5 (maximum) in the unbound.conf file and by launching unbound with the flags “-v -v -v” for extra verbosity. You can check the Unbound website if you are unsure about it or check my github at

      • 16 darave

        @quentin mcgaw, thanks for the help.
        Building the client from scratch made the difference!
        I had to upgrade my poor Raspberry Pi 1 from jessie to stretch (took a day on that old hardware), but now it runs.
        Appreciate your help!

  6. 17 darave


    Thanks for the nice tutorial.
    I went through all the steps, but the service fails to run for some reason.
    It looks like it tries to bind twice the listening ports:

    [1536472175] unbound[3827:0] notice: Start of unbound 1.6.0.
    [1536472175] unbound[3827:0] debug: increased limit(open files) from 1024 to 4164
    [1536472175] unbound[3827:0] debug: creating udp4 socket 53
    [1536472175] unbound[3827:0] debug: creating tcp4 socket 53
    [1536472175] unbound[3827:0] debug: creating udp4 socket 53
    [1536472175] unbound[3827:0] debug: creating tcp4 socket 53
    [1536472175] unbound[3827:0] error: can’t bind socket: Address already in use for port 53 (len 16)
    [1536472175] unbound[3827:0] fatal error: could not open ports

    In any case, no one listening on port 53:
    tcp6 0 0 ESTABLISHED
    udp 0 0*
    udp 0 0*
    udp6 0 0 :::5353 :::*
    unix 2 [ ACC ] STREAM LISTENING 15485 /root/.forever/sock/worker.15364161623804AM.sock
    unix 3 [ ] STREAM CONNECTED 14853 /run/systemd/journal/stdout
    unix 3 [ ] STREAM CONNECTED 14534 /var/run/docker/containerd/docker-containerd.sock
    unix 3 [ ] STREAM CONNECTED 8453 /run/systemd/journal/stdout
    unix 3 [ ] STREAM CONNECTED 14533

    Any idea…?

    • That’s a bit of a head scratcher. There’s probably some other reason why it’s failing to bind, but an explanation of why that might be doesn’t spring to mind.

    • 20 quentin mcgaw

      What does the command:
      netstat -tulpn | grep 53
      give you? There must something occupying port 53.
      It is also odd it gives you “unbound[3827:0] debug: creating tcp4 socket 53” twice (for both tcp4 and udp4).

      • 21 darave

        I did run netstat and the output is in the original message, nothing is occupying port 53:

        (Not all processes could be identified, non-owned process info
        will not be shown, you would have to be root to see it all.)
        udp 0 0* -
        udp 0 0* -
        udp6 0 0 :::5353 :::* -

        Very strange.. :(
        M assumption is that for some obscure reason, unbound is trying to doubly bind and fails. Any idea…?
        BTW, trying on other ports fails the same way.

      • It sounds almost like unbound is failing to get the ports because of a permission issue rather than because the port is already in use, which would lead me to ask what user is it running as, and whether the same issue happens with ports >1024? Beyond that my head goes to peculiar cgroup and kernel capability configs possibly getting in the way. Are you doing this with stock (latest) Raspian or an image that’s accumulated a bunch of cruft from a variety of past projects?

      • 23 quentin mcgaw

        What’s your unbound.conf file? Maybe try with:
        verbosity: 2
        use-syslog: no
        qname-minimisation: yes
        do-tcp: yes
        prefetch: yes
        rrset-roundrobin: yes
        use-caps-for-id: yes
        do-ip4: yes
        do-ip6: no
        access-control: allow
        name: “.”
        forward-addr: [email protected]
        forward-addr: [email protected]
        forward-ssl-upstream: yes

    • 24 Alex

      Having almost the same issue except port 53 is being occupied by pihole-FTL. Have you figured out the fix?

      • You can’t have two services on the same port, and you also need to decide which is first in the chain. I’d suggest that PiHole needs to be first, and then unbound and then, so run unbound elsewhere (like 5353) and configure PiHole to use that.

  7. 26 darave

    I tried port 2222, got exactly the same result, so I don’t think it is permissions but I might be wrong.
    I am on Raspberry PI 1, with Debian stretch, updated to last apt-get update/upgrade…

    [email protected] ~ $ cat /etc/os-release
    PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)"
    NAME="Raspbian GNU/Linux"
    VERSION="9 (stretch)"

    Unbound is from apt-get, version 1.6.
    The conf files are exactly as described in the @chris's wonderefull howto above, I changed nothing (very similar to quentin's post).

    • 27 quentin mcgaw

      Did you try running Unbound not as a daemon with
      unbound -d
      Did you check your Iptables? Otherwise installing Unbound without a package manager?

      • 28 darave

        Yes, running without /d yields same results.
        When can I check re. iptables?
        And installing w/o a package manager, you mean build it from sources? I haven’t tried this. Not sure which version still buildable on the pi 1… :|

      • 29 quentin mcgaw

        Try running nc -l -k -p 53 to see if port 53 is indeed free (probably is). If so then:

        First uninstall unbound:
        sudo apt-get uninstall unbound -y
        Then download and compile it (see for more information):
        tar -xvzf unbound-latest.tar.gz && rm unbound-latest.tar.gz && cd unbound-1.6.4/
        sudo ./configure && sudo make && sudo make install
        Now try to run unbound. You might have to search where to place your unbound.conf or recompile it with different flags for ./configure to specify where you want the unbound.conf file to be.

  8. 30 darave

    Thanks quentin, I’ll give it a try.

  9. 31 me

    i know its a bit old post, but comes up high in google, so someone may use it and end up with issues
    remember to add root-ca as described here to avoid ssl handshake error:

  10. 33 ztjuh

    After I restart unbound pages are not being resolved.

    [email protected]:/etc/unbound/unbound.conf.d $ dig

    ; <> DiG 9.11.5-P4-5.1+deb10u1-Raspbian <>
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 45638
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

    ; EDNS: version: 0, flags:; udp: 1472
    ; IN A

    ;; Query time: 1 msec
    ;; SERVER:

    ;; WHEN: Thu May 21 19:30:14 CEST 2020
    ;; MSG SIZE rcvd: 39
    This is my unbound configuration:
    # If no logfile is specified, syslog is used
    logfile: "/var/log/unbound/unbound.log"
    verbosity: 0

    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes

    # May be set to yes if you have IPv6 connectivity
    do-ip6: yes

    # You want to leave this to no unless you have *native* IPv6. With 6to4 and
    # Terredo tunnels your web browser should favor IPv4 for the same reasons
    prefer-ip6: no

    # Use this only when you downloaded the list of primary root servers!
    root-hints: "/var/lib/unbound/root.hints"

    # Trust glue only if it is within the server's authority
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
    harden-dnssec-stripped: yes

    # Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
    # see for further details
    use-caps-for-id: no

    # Reduce EDNS reassembly buffer size.
    # Suggested by the unbound man page to reduce fragmentation reassembly problems
    edns-buffer-size: 1472

    # Perform prefetching of close to expired message cache entries
    # This only applies to domains that have been frequently queried
    prefetch: yes

    # One thread should be sufficient, can be increased on beefy machines. In reality for most users running on small networks or on a single machine, it should be unnecessary to seek performance enhancement by increasing num-threads above 1.
    num-threads: 1

    # Ensure kernel buffer is large enough to not lose messages in traffic spikes
    so-rcvbuf: 1m

    qname-minimisation: yes
    prefetch: yes
    rrset-roundrobin: yes
    use-caps-for-id: yes

    # Ensure privacy of local IP ranges
    private-address: fd00::/8
    private-address: fe80::/10

    name: "."
    forward-addr: [email protected]
    forward-addr: [email protected]
    forward-ssl-upstream: yes

  11. 34 Jason

    I have used this before, but recently I updated my raspberry pi and everything broke. I am not getting the error “DNSPI unbound: [673:0] error: ssl handshake failed crypto error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed”

    I am not sure how to proceed here. I am running raspberry pi buster dated march 4th 2021.

    Any suggestions?

  12. 35 Jason

    Just to add a little bit more to my previous comment, I removed the and it started working, but I don’t know if its being intercepted by my ISP.

    • Hi Jason, the original error “certificate verify failed” is saying that the certificate presented by doesn’t match the name as the stuff after # isn’t just a comment, it’s there to verify that your connection isn’t being misdirected and spoofed. If your ISP was up to no good then they could grab traffic meant for and have their own nefarious DNS server answer, but in theory they shouldn’t be able to spoof having a certificate for the correct name (though in practice we’re often too broad in which CAs we trust, and there are plenty of dodgy CAs where a rogue telco could get a spoof certificate – though the risk to them is being caught doing that by the various folk doing certificate observatory work).

      I’d love to update the post above with correct examples, but the propensity of WordPress’s new editor to mangle code samples is just too great. Check out this gist instead.

  13. 37 Halfdan Knudsen

    very helpful how-to including the comments – I had NOT twigged to the fact that the DNS names after # were being validated against certs.
    As a minor addition: running this on dietpi with pihole+unbound behind ddwrt router and vpn (lots of config going on). Be mindful that:
    a. default unbound config file in DietPi is /etc/unbound/unbound.conf.d/dietpi.conf and already includes all the options specified in the how-to above EXCEPT tls-cert-bundle…… line
    b. DietPi recommends using port 5335 instead of 5353 since mDNS runs here
    c. on DietPi ‘sudo nano ….’ works to edit / create conf files (no need for bash)

    very helpful how-to thanks a mill! And also not the best covered subject on the interwebs, so spot on for doing it and going through the details.

  14. 38 Halfdan Knudsen

    oh and ps netstat cmd returns error on dietpi (debian) system because apparently deprecated and replaced by ‘ss -l’ :)

  1. 1 Cambridge Analytica – JamesBook
  2. 2 secure your dns on a a raspberry pi | leonslessons

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: