DNS over HTTPS (+ModSecurity WAF)

One of the problems with DNS is that a query is sent over an unencrypted connection, anyone listening to the packets knows the websites you visit.

DNS over HTTPS (+ModSecurity WAF)

One of the main security problems with DNS is that a query is sent over an unencrypted connection. That means that anyone listening to packets between you and your DNS server could know what websites you are visiting, even if the website that you are browsing is secured with HTTPS. In this article I'll show you how I was able to use the CloudFlare as your DNS over a HTTPS resolver, and also how to filter out phishing domains using libModSecurity at the same time.

Another problem you can solve with DNSSEC is the Man-In-The-Middle scenario, when unsecured it's easy to change DNS answers and forward visitors to a phishing site and unfortunately only a small percentage of website domains use DNSSEC.

Can DNS over HTTPS be a solution?

DNS over HTTPS (DoH) is a protocol for performing remote DNS resolution via the HTTPS protocol. A goal of the method is to increase user privacy and security by preventing eavesdropping and manipulation of DNS data by Man-In-The-Middle attacks. DNS over HTTPS is a standard as RFC 8484 under the IETF. It uses HTTP/2 and HTTPS and supports "wire format" DNS response data, as returned in existing UDP responses, in an HTTPS payload with the application/dns-message MIME type.

DoH could let the user send an unencrypted DNS query to his localhost (for example to where there's something similar to a DNS cache server that takes his query and forwards it to CloudFlare DNS via HTTPS. In this case, the unencrypted DNS query stay away from prying eyes (and noses) and we can even get advantages from the HTTP protocol in order to filter out, blacklist and protect the user's browsing. If we are able to transform a standard DNS query to an encrypted HTTPS request, why don't we add some filters on it? The open source WAF ModSecurity is useful to make rules on what users can and can not query!

Sure, we can add a "Pi-Hole" blacklist in order to block users to resolve certain "bad guy" hostnames, but we can do more than this! As you know, many phishing websites usually choose a hostname to trick users, like login.google.com.access.pure-evil-phishing.xyz with a valid ssl certificate from letsencrypt to polish the illusion.

I want to test and see if I can send a DNS over HTTPS query to an Nginx with ModSecurity and write a rule that says: "if DNS query contains google.com but google is not the 2nd level domain and com is not the top level domain, then block it!".

What I'm going to show you:

CloudFlare HTTPS resolver

The easiest way to query for a domain name via HTTPS to CloudFlare is to send a simply GET request to https://cloudflare-dns.com with two parameters: name the domain name to resolve, and type the record type you want. Don't forget to specify application/dns-json on the Accept header:

$ http 'https://cloudflare-dns.com/dns-query?name=google.com&type=A' 'accept:application/dns-json'
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
CF-RAY: 4a6750253a36be52-MXP
Connection: keep-alive
Content-Length: 203
Content-Type: application/dns-json
Date: Sat, 09 Feb 2019 15:19:13 GMT
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server: cloudflare
Vary: Accept-Encoding
cache-control: max-age=36

    "AD": false,
    "Answer": [
            "TTL": 36,
            "data": "",
            "name": "google.com.",
            "type": 1
    "CD": false,
    "Question": [
            "name": "google.com.",
            "type": 1
    "RA": true,
    "RD": true,
    "Status": 0,
    "TC": false

Easy, isn't it? As an alternative you can even use the DNS wireformat (that will be useful when we'll create the ModSecurity rule) but it requires you to know a little bit deep the DNS protocol.

echo -n 'q80BAAABAAAAAAAAA3d3dwZnb29nbGUDY29tAAABAAE=' | base64 -d | \
curl -s -H 'content-type: application/dns-message' \
    --data-binary @- https://cloudflare-dns.com/dns-query | \
    hexdump -C
00000000  ab cd 81 80 00 01 00 01  00 00 00 01 03 77 77 77  |.............www|
00000010  06 67 6f 6f 67 6c 65 03  63 6f 6d 00 00 01 00 01  |.google.com.....|
00000020  c0 0c 00 01 00 01 00 00  00 ab 00 04 d8 3a cd 64  |.............:.d|
00000030  00 00 29 05 ac 00 00 00  00 00 00                 |..)........|

Anyway, as you might know, the domain name inside a DNS query is represented as a sequence of labels that consists of a length octet followed by that number of octets. Let's take, for example, www.google.com: inside a query it becomes:

\x03www\x06google\x03com then others 2 bytes that represents the record type (for example A \x00\x01 or NS \x00\x02 etc...) and, at the end, others 2 bytes for the class (IN \x00\x01). The domain name terminates always with the zero length octet for the null label of the root.

So, we can easily create the sequence in bash with something like:

echo -ne '\xab\xcd\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00'\
    '\x00\x01\x00\x01' | \
    hexdump -C
00000000  ab cd 01 00 00 01 00 00  00 00 00 00 03 77 77 77  |.............www|
00000010  06 67 6f 6f 67 6c 65 03  63 6f 6d 00 00 01 00 01  |.google.com.....|

I don't want to go deep too much on DNS protocol, but if you're wondering what's the meaning of the first line \xab\xcd\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00 it is just the header that define some useful information like the ID, the flag "i'm a query", etc...

Now you need to send it as the body of a POST request. You can pipe a curl syntax to the previous command like this:

echo -ne '\xab\xcd\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01' | \
    curl -s -H 'content-type: application/dns-message' \
    --data-binary @- \
    https://cloudflare-dns.com/dns-query | \
    hexdump -C

00000000  ab cd 81 80 00 01 00 01  00 00 00 01 03 77 77 77  |.............www|
00000010  06 67 6f 6f 67 6c 65 03  63 6f 6d 00 00 01 00 01  |.google.com.....|
00000020  c0 0c 00 01 00 01 00 00  00 34 00 04 d8 3a cd 44  |.........4...:.D|
00000030  00 00 29 05 ac 00 00 00  00 00 00                 |..)........|

As you can see, CloudFlare answer that the record A of www.google.com is 4 bytes long 0x04 and the IP is 0xd83acd44

The CloudFlare DoH client

CloudFlare has open sourced his DoH client that you can find at https://developers.cloudflare.com/argo-tunnel/downloads/ once installed you can just run sudo cloudflared proxy-dns to get started:

Hello, World!

Assuming that you already have a running Nginx listening on HTTPS and with HTTP/2 active (or a CloudFlare free account) and a working libModSecurity inside it (if not, please take a look at https://www.nginx.com/blog/compiling-and-installing-modsecurity-for-open-source-nginx/) we can tell to cloudflared to forward all requests to our website (I'll use my website doh.rev3rse.it) with:

# ./cloudflared proxy-dns --address --upstream https://doh.rev3rse.it/dns-query
INFO[0000] Adding DNS upstream                           url="https://doh.rev3rse.it/dns-query"
INFO[0000] Starting DNS over HTTPS proxy server          addr="dns://"
INFO[0000] Starting metrics server                       addr=""

Now we just need to handle all requests coming from our cloudflared and forward them to https://cloudflare-dns.com. With Nginx is really easy:

location ~* /dns-query {
    modsecurity on;
    modsecurity_rules_file conf/modsecurity.conf;

    proxy_redirect off;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host "cloudflare-dns.com";
    proxy_pass https://cloudflare-dns.com:443;

after reloading the nginx configuration, it works like a charm!

secjuice.com resolved via https://doh.rev3rse.it

My libModSecurity logs every request in JSON, and that logs are collected by logstash and sent to elasticsearch. One of the first coolest thing of DoH is that I can immediately see all queries on my Kibana 😍

ModSecurity DNS Filter

Now the fun part, let's create a ModSecurity rule that blocks DNS query for something like accounts.google.com.signin.rev3rse.it but allows something like login.google.com

SecRule REQUEST_HEADERS:Content-Type "application/dns-udpwireformat" \
SecRule REQUEST_BODY "@rx (\x06google\x03com(?!\x00).*)" \
    msg:'Blocked DNS Query',\

As you can see in the two rules above, at fist I check if the Content-Type is application/dns-udpwireformat then I look for something different then \x00 at the end of com TLD. This regular expression should match everything like google.com.something and should allow something.google.com. Let's do a test:

this is fucking awesome! :D
My WAF dashboard

IT COULD WORK! 😎 The first query is sent to the Google DNS and, as you can see, it returns the correct value for the accounts.google.com.signin.rev3rse.it hostname. The second query is sent to my local DNS server (cloudflared) and forwarded to my Nginx + ModSecurity and, as you can see, Nginx reply with a 403 Forbidden response status and I haven't received any response from my DNS server!


If you are thinking that DoH is not the best approach in terms of performance you might be surprised to learn that it passes Mozilla performance test! Their blog shows that "During July 2018, about 25,000 Firefox Nightly 63 users who had previously agreed to be part of Nightly experiments participated in some aspect of this study. Cloudflare operated the DoH servers [...] The experiment generated over a billion DoH transactions and is now closed."

"Using HTTPS with a cloud service provider had only a minor performance impact on the majority of non-cached DNS queries as compared to traditional DNS. Most queries were around 6 milliseconds slower, which is an acceptable cost for the benefits of securing the data. However, the slowest DNS transactions performed much better with the new DoH based system than the traditional one – sometimes hundreds of milliseconds better."

Maybe, if in the future the DNS protocol will be always more delivered over HTTPS and the TCP protocol as we know it will be replaced by QUIC (or something else anyway better than TCP), IMHO this could be a cool way to protect users using something that already exists!

If you liked this post, please share and follow me!

The awesome image used in this article is called 'Visual Persistence' by Burnt Toast Creative.