DNS-over-HTTPS (DoH) can actually make your DNS lookups more trackable to your ISP, not less, if you’re not careful.

Let’s get CoreDNS serving DNS-over-HTTPS requests. This isn’t just about privacy; it’s about bypassing restrictive DNS policies and ensuring your DNS traffic isn’t easily snooped on by middleboxes. We’ll set up CoreDNS to listen on a specific port for HTTPS requests and proxy them to an upstream DoH resolver.

Here’s a basic CoreDNS setup that will act as our DoH server. We’ll use tls and forward plugins.

.:53 {
    errors
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . 8.8.8.8 1.1.1.1 {
       tls_servername tls.cloudflare.com
    }
    cache 30
    loop
    reload
    loadbalance
}

.:853 {
    errors
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . 8.8.8.8 1.1.1.1 {
       tls_servername tls.cloudflare.com
    }
    cache 30
    loop
    reload
    loadbalance
}

.:443 {
    errors
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . 8.8.8.8 1.1.1.1 {
       tls_servername tls.cloudflare.com
    }
    cache 30
    loop
    reload
    loadbalance
}

This Corefile defines three server blocks:

  • .:53: This is your standard DNS listener on port 53. It’s included for completeness, though it won’t serve DoH.
  • .:853: This is the standard DNS-over-TLS (DoT) port. CoreDNS will listen here for DoT requests, which are essentially DNS over TLS but without the HTTP framing.
  • .:443: This is where the magic happens for DNS-over-HTTPS. CoreDNS will listen on the standard HTTPS port.

The forward plugin is configured to send queries to Google’s 8.8.8.8 and Cloudflare’s 1.1.1.1. The tls_servername tls.cloudflare.com is crucial here. It tells the forward plugin to use TLS for these upstream lookups, specifically targeting Cloudflare’s DoH endpoint by its SNI (Server Name Indication). This is how CoreDNS will talk to the upstream DoH service.

Now, we need to make CoreDNS serve DoH on port 443. This requires the tls plugin to handle the HTTPS part and the forward plugin to proxy the actual DNS queries. The http plugin is what processes the DoH requests.

Here’s the Corefile that actually enables DoH on port 443:

.:53 {
    errors
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . 8.8.8.8 1.1.1.1 {
       tls_servername tls.cloudflare.com
    }
    cache 30
    loop
    reload
    loadbalance
}

.:853 {
    errors
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . 8.8.8.8 1.1.1.1 {
       tls_servername tls.cloudflare.com
    }
    cache 30
    loop
    reload
    loadbalance
}

https://.:443 {
    tls <path/to/your/cert.pem> <path/to/your/key.pem>
    http {
        path /dns-query
        proxy_upstream .
    }
    forward . 8.8.8.8 1.1.1.1 {
       tls_servername tls.cloudflare.com
    }
    cache 30
    loop
    reload
    loadbalance
}

In this updated Corefile:

  • The https://.:443 block signifies that CoreDNS will listen for HTTPS traffic on port 443.
  • tls <path/to/your/cert.pem> <path/to/your/key.pem>: You must replace <path/to/your/cert.pem> and <path/to/your/key.pem> with the actual paths to your TLS certificate and private key. For testing, you can generate a self-signed certificate using openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem -subj "/CN=mydomain.com".
  • http { ... }: This block enables HTTP/2 processing.
    • path /dns-query: This is the standard path for DoH requests as defined by the IETF draft.
    • proxy_upstream .: This tells the http plugin to forward the processed DNS requests to the next plugin in the chain, which is our forward plugin.
  • The forward plugin remains configured to use TLS upstream to 8.8.8.8 and 1.1.1.1 via Cloudflare’s DoH endpoint.

To run this, save the Corefile and execute:

coredns -conf /path/to/your/Corefile

You’ll need to ensure the specified certificate and key files exist and are readable by the CoreDNS process.

Now, to test it, you can use curl or a DoH-compatible client.

Using curl:

curl -H 'accept: application/dns-json' \
     -H 'content-type: application/dns-json' \
     --data '{"name":"www.google.com", "type":"A"}' \
     https://<your-server-ip>:443/dns-query

Replace <your-server-ip> with the IP address of the machine running CoreDNS. This sends a DNS query for www.google.com (A record) in the DoH JSON format and expects a response in the same format.

A more robust test uses dig with the +https option (requires a recent dig version):

dig @<your-server-ip> www.google.com A +https

This command tells dig to query <your-server-ip> using DNS-over-HTTPS.

The CoreDNS forward plugin’s tls_servername directive is key here. When CoreDNS forwards a request to 8.8.8.8 or 1.1.1.1, it’s not just opening a plain UDP DNS connection. It’s establishing a TLS connection to those upstream resolvers, and the tls_servername tells the upstream resolver which certificate it should present. By setting it to tls.cloudflare.com, we’re telling the upstream (which is likely Google’s DoH endpoint, but it can handle SNI requests) that we want to connect using the certificate for tls.cloudflare.com. This is how CoreDNS negotiates a secure connection to the DoH service of the upstream provider. The http plugin then takes the incoming HTTPS request, extracts the DNS query from the JSON payload, and passes it to the forward plugin, which encrypts it using TLS and sends it upstream. The response comes back, is decrypted by the forward plugin, and then the http plugin formats it into the DoH JSON response before sending it back to the client over HTTPS.

The most surprising thing about setting up DoH this way is that you’re essentially turning your CoreDNS instance into a client for an upstream DoH provider, just like any browser or OS would be. You’re not terminating DoH at your CoreDNS server in the sense of directly handling the DNS query over HTTPS without an upstream; rather, you’re using CoreDNS to bridge standard DNS clients to an upstream DoH service. Your CoreDNS server acts as a local endpoint that accepts DoH requests, then it uses its own DoH client capabilities (via the forward plugin with tls_servername) to query an external DoH service. This means the privacy benefits you get are largely from the upstream provider’s DoH service, and your CoreDNS server itself needs to be secured with a valid TLS certificate.

The proxy_upstream . directive within the http plugin is critical. It tells the HTTP server to forward the request to the next plugin in the chain. In this case, the next plugin is the forward plugin. The forward plugin then handles the actual DNS resolution, using its own TLS connection to the upstream resolvers. This separation of concerns means the http plugin focuses on the web/HTTPS layer, while the forward plugin handles the DNS resolution and its associated upstream TLS connection.

You might run into issues with clients not being able to resolve domains if your forward plugin doesn’t have valid upstream DoH endpoints or if your tls_servername doesn’t match what the upstream expects.

Want structured learning?

Take the full Coredns course →