DNS over HTTPS (DoH) encrypts DNS queries within HTTPS connections, preventing ISPs and network observers from seeing your DNS lookups. This guide covers setting up both a DoH client (for privacy) and a DoH server (to provide encrypted DNS to your users).
Why DoH?
- Privacy — DNS queries are encrypted and hidden within normal HTTPS traffic
- Security — prevents DNS spoofing and man-in-the-middle attacks
- Bypass censorship — DoH traffic looks like regular HTTPS, harder to block than traditional DNS
DoH Client Setup
System-Wide with dnscrypt-proxy
# Install dnscrypt-proxy
sudo apt install dnscrypt-proxy
# /etc/dnscrypt-proxy/dnscrypt-proxy.toml
listen_addresses = ['127.0.0.1:53']
server_names = ['cloudflare', 'google']
[sources.public-resolvers]
urls = ['https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md']
cache_file = '/var/cache/dnscrypt-proxy/public-resolvers.md'
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
# Point system DNS to local proxy
# /etc/resolv.conf
nameserver 127.0.0.1
Browser-Level DoH
# Firefox: Settings → Privacy & Security → DNS over HTTPS → Enable
# Chrome: Settings → Security → Use secure DNS → Choose provider
# Common DoH providers:
# Cloudflare: https://cloudflare-dns.com/dns-query
# Google: https://dns.google/dns-query
# Quad9: https://dns.quad9.net/dns-query
DoH Server Setup
Using CoreDNS
# Corefile with DoH endpoint
https://.:443 {
tls /etc/letsencrypt/live/dns.example.com/fullchain.pem /etc/letsencrypt/live/dns.example.com/privkey.pem
forward . 1.1.1.1 8.8.8.8
cache 600
log
prometheus :9153
}
Using Nginx as DoH Proxy
# Nginx proxies DoH requests to a local DNS resolver
upstream dns_backend {
server 127.0.0.1:8053;
}
server {
listen 443 ssl http2;
server_name dns.example.com;
ssl_certificate /etc/letsencrypt/live/dns.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dns.example.com/privkey.pem;
location /dns-query {
proxy_pass http://dns_backend/dns-query;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Using doh-server (Rust)
# Install doh-server
cargo install doh-server
# Run
doh-server \
--listen-address 0.0.0.0:3000 \
--server-address 127.0.0.1:53 \
--tls-cert-path /etc/letsencrypt/live/dns.example.com/fullchain.pem \
--tls-cert-key-path /etc/letsencrypt/live/dns.example.com/privkey.pem \
--path /dns-query
Testing DoH
# Test with curl
curl -s -H "accept: application/dns-json" "https://dns.example.com/dns-query?name=example.com&type=A"
# Test with kdig (knot-dnsutils)
kdig @dns.example.com +https example.com A
# Test with dog
dog example.com A --https @https://dns.example.com/dns-query
Best Practices
- Use trusted DoH providers (Cloudflare, Google, Quad9) for client-side setup
- Self-host DoH servers for organizational privacy requirements
- Enable DNSSEC validation alongside DoH for complete DNS security
- Monitor DoH server performance — encrypted DNS adds TLS overhead
- Consider DoT (DNS over TLS, port 853) as a simpler alternative when stealth is not required