Deploying DoH and DoT with CoreDNS on Linux: Secure Internal DNS Queries Against Eavesdropping

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

Quick Start: Spin Up Encrypted CoreDNS in 5 Minutes

Plaintext DNS on port 53 transmits every query in the clear. Anyone with access to your internal network—or a compromised container in your VPC—can sniff queries or inject spoofed answers. Encrypting internal DNS traffic closes this gap. CoreDNS handles both DNS over TLS (DoT) and DNS over HTTPS (DoH) out of the box with zero external dependencies.

Here is a battle-tested setup to get an encrypted resolver running on Ubuntu 22.04 or Debian 12 in minutes.

1. Download and Install CoreDNS

# Pull the Linux amd64 binary (v1.11.3)
curl -LO https://github.com/coredns/coredns/releases/download/v1.11.3/coredns_1.11.3_linux_amd64.tgz
tar -zxvf coredns_1.11.3_linux_amd64.tgz
sudo mv coredns /usr/local/bin/
sudo chmod +x /usr/local/bin/coredns

# Create config and cert directories
sudo mkdir -p /etc/coredns /etc/coredns/certs

2. Generate Local TLS Certificates

For internal infrastructure, generate an RSA private key and self-signed certificate (or sign one with your organization’s internal CA):

sudo openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout /etc/coredns/certs/dns.key \
  -out /etc/coredns/certs/dns.crt \
  -days 365 -subj "/CN=dns.internal.lan"

3. Write the Corefile

Create /etc/coredns/Corefile to bind DoT (port 853) and DoH (port 443), while proxying unresolved queries to upstream privacy resolvers:

# DNS over TLS (DoT)
tls://.:853 {
    tls /etc/coredns/certs/dns.crt /etc/coredns/certs/dns.key
    forward . 1.1.1.1 8.8.8.8
    cache 300
    errors
    log
}

# DNS over HTTPS (DoH)
https://.:443 {
    tls /etc/coredns/certs/dns.crt /etc/coredns/certs/dns.key
    forward . 1.1.1.1 8.8.8.8
    cache 300
    errors
    log
}

4. Run and Test the Server

# Start CoreDNS directly in your terminal
sudo /usr/local/bin/coredns -conf /etc/coredns/Corefile

Open a second terminal and verify the DoT listener using kdig (part of knot-dnsutils):

kdig @127.0.0.1 -p 853 +tls +tls-ca=/etc/coredns/certs/dns.crt example.com

Architecture Breakdown: DoT vs DoH in Production

Choosing between DoT and DoH depends entirely on where your clients live and who controls the network firewalls.

DoT (RFC 7858) vs DoH (RFC 8484)

  • DoT (DNS over TLS) wraps binary DNS wire format directly into a TLS session over TCP port 853. It adds minimal protocol overhead—typically under 1KB per TLS session handshake. It fits server-to-server traffic, Kubernetes node resolvers, and site-to-site VPNs. Security teams also appreciate that port 853 is straightforward to monitor and filter at perimeter firewalls.
  • DoH (DNS over HTTPS) packages DNS queries inside HTTP/2 or HTTP/3 streams on TCP port 443. It blends in with standard HTTPS traffic. This makes it ideal for roaming developer laptops, branch offices, and networks behind restrictive proxies. The trade-off is a slight increase in memory overhead (~15-20% higher than DoT) and HTTP framing overhead.

How CoreDNS Handles Encrypted Queries

CoreDNS executes plugins sequentially for every incoming packet:

  1. TLS Termination: CoreDNS handles the handshake using your configured certificates.
  2. Zone Resolution: Local plugins like hosts or file evaluate static records first.
  3. In-Memory Cache: The cache plugin intercepts repeated lookups, serving hits in under 1ms.
  4. Upstream Forwarding: Cache misses pass to the forward plugin, which forwards queries to external resolvers over encrypted channels.

In a cluster running 120 microservice replicas, a single CoreDNS instance idling at 45MB RAM easily serves 18,000 queries per second, maintaining sub-3ms latency for cached entries.

Production Setup: Hardened Systemd Service and Encrypted Upstreams

Running binaries interactively is fine for validation. Production workloads require process isolation, auto-restarts, and encrypted upstream hops.

1. Secure Upstream Lookups with DoT Forwarding

Never forward queries to your ISP or upstream resolvers in cleartext. Route upstream traffic over DoT with strict SNI hostname validation:

# /etc/coredns/Corefile
tls://.:853 https://.:443 {
    tls /etc/coredns/certs/dns.crt /etc/coredns/certs/dns.key

    # Local split-horizon entries
    hosts {
        10.10.0.15 db-primary.internal.lan
        10.10.0.16 db-replica.internal.lan
        fallthrough
    }

    # Upstream DoT with TLS hostname verification
    forward . tls://1.1.1.1 tls://1.0.0.1 {
        tls_servername cloudflare-dns.com
        health_check 5s
    }

    cache 3600 {
        success 4096
        denial 1024
    }
    prometheus :9153
    errors
    log
}

2. Configure an Unprivileged Systemd Service

Create a dedicated system user. Use Linux capabilities to let the binary bind to ports 443 and 853 without root privileges:

sudo useradd -r -d /var/lib/coredns -s /usr/sbin/nologin coredns
sudo chown -R coredns:coredns /etc/coredns
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/coredns

Write the systemd service unit to /etc/systemd/system/coredns.service:

[Unit]
Description=CoreDNS Encrypted DNS Server
Documentation=https://coredns.io/manual/toc/
After=network.target

[Service]
Type=simple
User=coredns
Group=coredns
ExecStart=/usr/local/bin/coredns -conf /etc/coredns/Corefile
Restart=on-failure
RestartSec=5s
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Reload systemd and launch the daemon:

sudo systemctl daemon-reload
sudo systemctl enable --now coredns
sudo systemctl status coredns

Verification and Troubleshooting

Testing DoH Queries with Curl

Query your DoH endpoint directly with curl by sending a base64url-encoded DNS wire query:

curl -k -H "accept: application/dns-message" \
  --resolve dns.internal.lan:443:127.0.0.1 \
  "https://dns.internal.lan/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB" \
  --output - | xxd

Common Pitfalls

  • Port 53 Binding Conflicts: Ubuntu runs systemd-resolved on 127.0.0.53:53. If you want CoreDNS to handle unencrypted port 53 fallback queries, disable the stub listener. Set DNSStubListener=no in /etc/systemd/resolved.conf, symlink /etc/resolv.conf to /run/systemd/resolve/resolv.conf, and restart systemd-resolved.
  • Automated Cert Renewals: CoreDNS watches certificate files and reloads them on change. If you automate renewals with Certbot or Vault, point the tls directive to the target symlinks. No service restart is required.
  • Client Trust Stores: Client machines must trust your root CA to query DoH/DoT endpoints without error. Copy your root CA to /usr/local/share/ca-certificates/internal-ca.crt and run sudo update-ca-certificates on every Linux client.
Share: