LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

DNS over HTTPS and DNS over TLS: Encrypting the Internet's Phone Book

dnssecurityprivacynetworkinghomelabself-hosteddohdot

Every website you visit starts with a DNS lookup. Your browser asks a resolver “what’s the IP address for github.com?” and gets an answer. For most of the internet’s history, this exchange happened in plaintext over UDP — visible to your ISP, your network provider, anyone on the path between you and the resolver.

DNS over HTTPS (DoH) and DNS over TLS (DoT) fix this. Both protocols encrypt DNS queries, preventing eavesdropping and tampering. They differ in how they do it, which has implications for deployment, privacy, and control.

This guide covers why encrypted DNS matters, how both protocols work under the hood, running your own DoH/DoT resolver, configuring clients across platforms, and the nuanced debate around whether centralizing DoH at the browser level is actually better for privacy.

Why Plain DNS Is a Problem

Your ISP Can Read Every Query

DNS traffic to your ISP’s resolver — or to 8.8.8.8 — is unencrypted UDP on port 53. Your ISP can:

  • Log every domain you query, building a browsing profile
  • Sell that data (legal in many jurisdictions)
  • Inject responses (DNS hijacking for monetization or censorship)
  • Block domains by returning NXDOMAIN

Even when you use a VPN, the VPN provider sees your DNS queries unless your VPN also handles DNS.

On-Path Attackers

On public Wi-Fi, an attacker doing ARP spoofing can intercept and modify DNS responses — pointing bank.example.com to a phishing server. Since DNS has no authentication by default (DNSSEC helps but has low adoption), your device accepts whatever the “resolver” says.

DNS Leaks

With split tunneling or misconfigured VPN clients, DNS queries can leak outside the VPN tunnel, revealing browsing activity to the local ISP even when you believe traffic is encrypted.

HTTPS encrypts the HTTP body and verifies server identity, but the DNS lookup that precedes it has historically been in the clear. DoH/DoT close this gap.

DNS over TLS (DoT)

How It Works

DoT wraps standard DNS protocol in a TLS connection:

Client                          Resolver
  |                                |
  |── TCP connect → port 853 ─────>|
  |<──────────── TLS handshake ───>|
  |── DNS query (inside TLS) ─────>|
  |<── DNS response (inside TLS) ──|
  |── DNS query ───────────────────>|  (reuse TLS session)
  |<── DNS response ────────────────|

Key properties:

  • Port 853: DoT uses a dedicated port, making it identifiable (and blockable)
  • Same DNS wire format: The DNS protocol doesn’t change — it’s just transported inside TLS
  • Connection reuse: Multiple queries share one TLS connection, amortizing handshake cost
  • Certificate validation: The resolver’s certificate is verified against a CA, preventing spoofing

The dedicated port means DoT is easy for network administrators to allow or block. Enterprise environments can permit DoT to trusted resolvers while blocking everything else. Conversely, restrictive networks can block port 853 entirely.

DoT Query Flow

1. TCP handshake to resolver:853
2. TLS handshake (SNI: dns.resolver.example.com)
3. DNS query (2-byte length prefix + standard DNS message)
4. DNS response (2-byte length prefix + standard DNS message)
5. Connection kept alive for subsequent queries

The 2-byte length prefix distinguishes DoT from plain DNS-over-TCP — it allows multiple queries to be multiplexed on one connection without delimiters.

DNS over HTTPS (DoH)

How It Works

DoH sends DNS queries as HTTPS requests to a web endpoint:

POST /dns-query HTTP/2
Host: dns.example.com
Accept: application/dns-message
Content-Type: application/dns-message

[binary DNS message]

Or via GET with base64url encoding:

GET /dns-query?dns=AAABAAABAAAAAAAAB... HTTP/2

Key properties:

  • Port 443: DoH uses standard HTTPS port, indistinguishable from normal web traffic
  • HTTP/2 or HTTP/3: Modern implementations support multiplexing many DNS queries over one TLS connection, or QUIC for HTTP/3
  • Existing web PKI: Uses the same certificate infrastructure as HTTPS
  • Can’t be easily blocked: Blocking port 443 would break the entire web

The inability to selectively block DoH without breaking HTTPS is both a feature (censorship resistance) and a concern (enterprise network visibility).

DoH Query Flow (HTTP/2)

1. TLS handshake to port 443 (standard HTTPS)
2. HTTP/2 connection established
3. POST /dns-query with binary DNS message body
4. 200 OK with binary DNS message body
5. HTTP/2 connection multiplexes subsequent queries

DNS over HTTPS/3 (DoH3)

Newer resolvers support DoH over HTTP/3 (QUIC). Benefits:

  • Faster connection establishment (0-RTT for known servers)
  • No head-of-line blocking (each DNS query is an independent QUIC stream)
  • Connection migration (queries survive IP changes)

Cloudflare’s 1.1.1.1 and Google’s 8.8.8.8 both support DoH3.

DoT vs DoH: Practical Differences

Property DoT DoH
Port 853 (dedicated) 443 (shared with HTTPS)
Blockable Yes, block port 853 No, would break all HTTPS
Protocol visibility Identifiable as DNS Looks like HTTPS traffic
Network admin control Easy to manage Difficult to intercept
Client support OS-level (systemd-resolved) Browser + OS level
Latency Slightly lower (DNS-native) Slightly higher (HTTP overhead)
Firewall friendliness Variable High

For homelab use: DoT is excellent. You control your network, so there’s no need for DoH’s port-443 bypass. Your DNS traffic stays DNS-shaped.

For public-facing privacy: DoH provides stronger censorship resistance. Even on restrictive networks, DoH queries blend into HTTPS traffic.

For enterprise: DoT on port 853 allows controlled rollout to a specific resolver while maintaining network visibility. DoH bypass in browsers (Firefox’s canary domain approach) can undermine enterprise DNS policies.

DoH vs DoT vs DNSSEC

These solve different problems:

Technology Protects Against Does NOT Protect Against
DoH/DoT Eavesdropping on query/response Malicious resolver returning wrong answers
DNSSEC Tampered responses (validator verifies signatures) Eavesdropping on queries
DoH/DoT + DNSSEC Both Nothing (best current practice)

DNSSEC is complementary. It ensures the resolver didn’t lie about the answer; DoH/DoT ensures nobody on the path can read the question.

Running Your Own Encrypted Resolver

Public DoH/DoT resolvers (Cloudflare, Google, NextDNS) solve the ISP problem but shift trust to those providers. For maximum privacy, run your own resolver that queries authoritative nameservers directly.

Architecture Options

Option 1: Forwarder (Simpler)

Client → Local DoT/DoH resolver → Upstream DoT resolver (Cloudflare/NextDNS)

Your local resolver encrypts queries to an upstream. The upstream still sees your queries, but your ISP doesn’t.

Option 2: Recursive Resolver (Full Control)

Client → Local DoT/DoH resolver → Root nameservers → TLD nameservers → Authoritative nameservers

Your resolver queries root servers directly. No upstream provider sees your query patterns. More setup, but no third-party trust required.

Option A: Unbound (Recursive, Self-Hosted)

Unbound is a validating, caching resolver that can serve as the authoritative recursive resolver for your network. Pair it with stunnel or nginx to add TLS.

1
2
3
4
5
6
# Install Unbound
apt install unbound

# Generate a local certificate for DoT
apt install certbot
certbot certonly --standalone -d dns.yourdomain.com
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# /etc/unbound/unbound.conf

server:
    # Listen on all interfaces for local queries
    interface: 0.0.0.0
    port: 53

    # Access control
    access-control: 127.0.0.1/32 allow
    access-control: 192.168.1.0/24 allow
    access-control: 0.0.0.0/0 refuse

    # DNSSEC validation
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

    # Root hints (updated periodically)
    root-hints: "/etc/unbound/root.hints"

    # Performance
    num-threads: 2
    cache-min-ttl: 3600
    cache-max-ttl: 86400
    rrset-cache-size: 256m
    msg-cache-size: 128m

    # Privacy: send minimal queries (RFC 7816 QNAME minimization)
    qname-minimisation: yes

    # Don't leak private reverse DNS queries to public resolvers
    private-address: 192.168.0.0/16
    private-address: 10.0.0.0/8
    private-address: 172.16.0.0/12

    # Log queries (optional, disable for privacy)
    log-queries: no

    # Prefetch popular records before they expire
    prefetch: yes
    prefetch-key: yes

    # Aggressive NSEC for faster negative caching
    aggressive-nsec: yes

    # Harden against various attacks
    harden-dnssec-stripped: yes
    harden-glue: yes
    harden-referral-path: yes
    use-caps-for-id: yes  # 0x20 encoding

tls-service-key: "/etc/letsencrypt/live/dns.yourdomain.com/privkey.pem"
tls-service-pem: "/etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem"
tls-port: 853

# DoH via nginx proxy (Unbound doesn't natively speak HTTP)

Add DoH support via nginx proxy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# /etc/nginx/sites-available/doh

server {
    listen 443 ssl http2;
    server_name dns.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/dns.yourdomain.com/privkey.pem;

    location /dns-query {
        proxy_pass http://127.0.0.1:8053;  # dnsdist or doh-proxy
        proxy_set_header Content-Type application/dns-message;
    }
}

You’ll need an intermediate DNS-to-HTTP adapter like dnsdist or Cloudflare’s doh-proxy.

Option B: dnsdist (Flexible Frontend)

dnsdist is a DNS load balancer that handles DoH and DoT natively and forwards to upstream resolvers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- /etc/dnsdist/dnsdist.conf

-- Listen for plain DNS (local network)
addLocal("0.0.0.0:53")

-- Listen for DoT (port 853)
addTLSLocal("0.0.0.0:853",
    "/etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem",
    "/etc/letsencrypt/live/dns.yourdomain.com/privkey.pem",
    { provider = "openssl" }
)

-- Listen for DoH (port 443)
addDOHLocal("0.0.0.0:443",
    "/etc/letsencrypt/live/dns.yourdomain.com/fullchain.pem",
    "/etc/letsencrypt/live/dns.yourdomain.com/privkey.pem",
    "/dns-query",
    { provider = "openssl" }
)

-- Upstream: Unbound on localhost for recursive resolution
newServer({ address = "127.0.0.1:5300", name = "unbound" })

-- Or forward to upstream DoT resolver (Cloudflare)
-- newServer({
--   address = "1.1.1.1:853",
--   tls = "openssl",
--   subjectName = "cloudflare-dns.com",
--   validateCertificates = true
-- })

-- Block known malware/ad domains (optional, integrates with Pi-hole blocklists)
-- addAction(QNameSuffixRule({"doubleclick.net", "ads.example.com"}), RCodeAction(DNSRCode.NXDOMAIN))

-- Query logging (optional)
-- addAction(AllRule(), LogAction("/var/log/dnsdist/queries.log", false, true))

setMaxTCPClientThreads(10)
setMaxUDPOutstanding(65535)
1
2
apt install dnsdist
systemctl enable --now dnsdist

This gives you a single service handling plain DNS, DoT, and DoH, all forwarding to a local Unbound for recursive resolution.

AdGuard Home combines a DNS resolver, ad blocker, DoH/DoT server, and web UI in one package. It’s the easiest path to an encrypted home resolver.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# docker-compose.yml
services:
  adguardhome:
    image: adguard/adguardhome:latest
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "853:853/tcp"    # DoT
      - "853:853/udp"    # DoQ (DNS over QUIC)
      - "3000:3000/tcp"  # Initial setup UI
      - "80:80/tcp"      # Web UI (redirect to HTTPS)
      - "443:443/tcp"    # DoH + Web UI HTTPS
    volumes:
      - ./adguard/work:/opt/adguardhome/work
      - ./adguard/conf:/opt/adguardhome/conf
      - ./certs:/opt/adguardhome/certs:ro

After initial setup via the web UI:

  1. Encryption: Settings → Encryption → Enable. Upload your TLS certificate and private key. Set the server name to dns.yourdomain.com.
  2. Upstream DNS: Settings → DNS → Upstream DNS servers. Add https://dns.cloudflare.com/dns-query (DoH) or tls://1.1.1.1 (DoT) as upstream. For full recursion, use Unbound as upstream.
  3. Bootstrap DNS: Plain DNS for resolving the upstream DoH hostname itself (before encrypted DNS is established): 1.1.1.1, 9.9.9.9
  4. Blocklists: Use AdGuard’s built-in lists or import Pi-hole compatible lists.

Your DoH endpoint becomes: https://dns.yourdomain.com/dns-query Your DoT endpoint becomes: tls://dns.yourdomain.com

Option D: Pi-hole + cloudflared (DoH Upstream Only)

If you already run Pi-hole, add cloudflared as a DoH proxy to encrypt upstream queries:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# docker-compose.yml addition
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: proxy-dns --port 5053 --upstream https://1.1.1.1/dns-query --upstream https://1.0.0.1/dns-query
    networks:
      - pihole_net

  pihole:
    image: pihole/pihole:latest
    environment:
      PIHOLE_DNS_: "cloudflared#5053"
    # ... rest of pi-hole config
    networks:
      - pihole_net

networks:
  pihole_net:

This doesn’t give you DoT/DoH to clients — Pi-hole still accepts plain DNS from the local network. But all upstream queries from Pi-hole to Cloudflare are encrypted.

For full DoH/DoT to clients, use AdGuard Home or dnsdist as described above.

Obtaining TLS Certificates

Your DoT/DoH server needs a valid TLS certificate. Options:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Install certbot
apt install certbot

# Issue certificate (your server must be publicly reachable on port 80, or use DNS challenge)
certbot certonly --standalone -d dns.yourdomain.com

# Or with Cloudflare DNS challenge (no port 80 needed)
pip install certbot-dns-cloudflare
cat > /etc/letsencrypt/cloudflare.ini << EOF
dns_cloudflare_api_token = YOUR_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini

certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d dns.yourdomain.com

Set up auto-renewal and reload your DNS service:

1
2
3
4
5
# /etc/letsencrypt/renewal-hooks/deploy/reload-dns.sh
#!/bin/bash
systemctl reload adguardhome
# or: systemctl reload dnsdist
# or: systemctl reload unbound

Self-Signed (Internal Use Only)

For a fully internal resolver not accessible from the internet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Generate CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/CN=Homelab DNS CA"

# Generate server certificate
openssl genrsa -out dns.key 2048
openssl req -new -key dns.key -out dns.csr \
  -subj "/CN=dns.home.local"

openssl x509 -req -days 365 -in dns.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out dns.crt \
  -extfile <(echo "subjectAltName=DNS:dns.home.local,IP:192.168.1.1")

Distribute ca.crt to all clients as a trusted root. You’ll need to do this manually for each device.

Client Configuration

Linux (systemd-resolved)

Modern systemd versions support DoT natively:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/systemd/resolved.conf

[Resolve]
DNS=dns.yourdomain.com
FallbackDNS=1.1.1.1
Domains=~.
DNSOverTLS=yes
# For opportunistic (try TLS, fall back to plain): opportunistic
# For strict (fail if TLS unavailable): yes
DNSSEC=yes
1
2
3
4
5
6
7
8
systemctl restart systemd-resolved

# Verify DoT is working
resolvectl status
# Should show: DNS over TLS: yes

# Test resolution
resolvectl query github.com

macOS

macOS 11+ supports DoH and DoT via Configuration Profiles. The easiest way is to generate a profile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PayloadContent</key>
    <array>
        <dict>
            <key>DNSSettings</key>
            <dict>
                <key>DNSProtocol</key>
                <string>HTTPS</string>
                <key>ServerAddresses</key>
                <array>
                    <string>192.168.1.1</string>
                </array>
                <key>ServerURL</key>
                <string>https://dns.yourdomain.com/dns-query</string>
            </dict>
            <key>PayloadDescription</key>
            <string>Homelab DoH</string>
            <key>PayloadDisplayName</key>
            <string>Homelab DNS</string>
            <key>PayloadIdentifier</key>
            <string>com.example.dnsSettings</string>
            <key>PayloadType</key>
            <string>com.apple.dnsSettings.managed</string>
            <key>PayloadUUID</key>
            <string>7a3d9f2c-1234-5678-abcd-ef0123456789</string>
            <key>PayloadVersion</key>
            <integer>1</integer>
        </dict>
    </array>
    <key>PayloadDescription</key>
    <string>Configures DoH for homelab</string>
    <key>PayloadDisplayName</key>
    <string>Homelab DoH Profile</string>
    <key>PayloadIdentifier</key>
    <string>com.example.dnsSettings.profile</string>
    <key>PayloadType</key>
    <string>Configuration</string>
    <key>PayloadUUID</key>
    <string>2b4e6f8a-9012-3456-cdef-012345678901</string>
    <key>PayloadVersion</key>
    <integer>1</integer>
</dict>
</plist>

Save as homelab-doh.mobileconfig and double-click to install. Or deploy via MDM.

Windows 11

Windows 11 has native DoH support in Settings → Network & Internet → Wi-Fi/Ethernet → DNS server assignment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Via PowerShell (Windows 11 22H2+)
# Get your network adapter name
Get-NetAdapter

# Set DoH server
Add-DnsClientDohServerAddress -ServerAddress "192.168.1.1" `
    -DohTemplate "https://dns.yourdomain.com/dns-query" `
    -AllowFallbackToUdp $false `
    -AutoUpgrade $true

# Apply to adapter
Set-DnsClientServerAddress -InterfaceAlias "Wi-Fi" -ServerAddresses "192.168.1.1"

iOS and iPadOS

iOS 14+ supports DoH/DoT via Configuration Profiles (same .mobileconfig format as macOS). Generate one at tools like dns.notjakob.com or write it manually as above.

For system-wide DoH on iOS without a profile:

Settings → Wi-Fi → (your network) → Configure DNS → Manual only sets plain DNS. You need a profile or a VPN/DNS app for encrypted DNS.

Popular options: NextDNS app, AdGuard DNS app — both create a local VPN tunnel that routes DNS queries over DoH/DoT.

Android

Android 9+ has “Private DNS” (DoT) built in:

Settings → Network & Internet → Private DNS → Private DNS provider hostname

Enter: dns.yourdomain.com

Android will connect to port 853 with TLS. If it can’t establish a DoT connection, it falls back to plain DNS (with a warning icon). To force strict mode only, use a third-party app.

For DoH on Android, use:

  • AdGuard: Creates a local VPN, routes DNS over DoH
  • Intra (Google/Jigsaw): DoH-only
  • NextDNS app: DoH/DoT to NextDNS

Firefox

Firefox has its own DoH implementation, independent of the OS:

about:preferences → Privacy & Security → DNS over HTTPS

Options:

  • Default protection: DoH when system DNS might be compromised
  • Increased protection: DoH, fall back to OS DNS if unavailable
  • Max protection: DoH only, fail if DoH unavailable

Set a custom resolver URL to use your own:

https://dns.yourdomain.com/dns-query

The canary domain: Firefox checks for use-application-dns.net. If this domain returns NXDOMAIN or a specific response, Firefox disables its built-in DoH — this is used by enterprise DNS policies to prevent DoH bypass. If you want Firefox to use your OS DoH configuration instead, return NXDOMAIN for use-application-dns.net from your local resolver.

Chrome / Chromium

Chrome uses DoH if the system’s DNS server is a known DoH provider:

chrome://settings/security → Use secure DNS

It recognizes providers like Cloudflare (1.1.1.1), Google (8.8.8.8), NextDNS, etc. For a custom resolver, enter the URL directly:

https://dns.yourdomain.com/dns-query

Chrome’s DoH implementation “upgrades” known providers automatically — if your system DNS is 1.1.1.1, Chrome automatically uses https://1.1.1.1/dns-query.

Testing and Verification

Verify DoT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Test DoT with kdig (from knot-dnsutils)
apt install knot-dnsutils

kdig -d @dns.yourdomain.com +tls github.com

# Should show TLS handshake details and then the DNS response
# Look for: "TLS session" and ";; MSG SIZE rcvd:"

# Or with drill
drill -T @dns.yourdomain.com github.com

Verify DoH

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Test DoH with curl
curl -sH 'accept: application/dns-json' \
  'https://dns.yourdomain.com/dns-query?name=github.com&type=A' | jq .

# Or with the binary DNS format
curl -s -X POST \
  -H "Content-Type: application/dns-message" \
  -H "Accept: application/dns-message" \
  --data-binary @<(printf '\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06github\x03com\x00\x00\x01\x00\x01') \
  https://dns.yourdomain.com/dns-query | xxd | head

# Easier: use doggo
doggo github.com @https://dns.yourdomain.com/dns-query

Check for DNS Leaks

Even with DoH/DoT configured, misconfigured fallbacks can leak queries:

  1. Visit dnsleaktest.com
  2. Run the Extended test
  3. All DNS servers shown should be your resolver (or your chosen upstream provider), not your ISP

If you see ISP DNS servers, there’s a leak somewhere:

  • Check browser-specific DNS settings
  • Check for hardcoded DNS in apps
  • Verify the OS is actually using your configured resolver

Verify DNSSEC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Test DNSSEC validation (should succeed)
kdig @dns.yourdomain.com +dnssec sigok.verteiltesysteme.net

# Test DNSSEC rejection of bad signatures (should fail with SERVFAIL)
kdig @dns.yourdomain.com +dnssec sigfail.verteiltesysteme.net
# Should return: status: SERVFAIL

# Check if your resolver validates DNSSEC
delv @dns.yourdomain.com github.com
# Should show: ; fully validated

Monitoring

Query Metrics with AdGuard Home

AdGuard Home’s dashboard shows:

  • Total queries per day/week
  • Blocked queries (ads, malware)
  • Query response time distribution
  • Top queried domains
  • Top blocked domains
  • Per-client query breakdown

Prometheus Metrics

Both AdGuard Home and dnsdist export Prometheus metrics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# prometheus.yml scrape config
scrape_configs:
  - job_name: adguardhome
    static_configs:
      - targets: ['homelab:3000']
    metrics_path: /control/stats
    # Note: AdGuard Home metrics are at a JSON endpoint, not standard Prometheus
    # Use adguardhome-exporter for proper Prometheus export

  - job_name: dnsdist
    static_configs:
      - targets: ['homelab:8083']
    metrics_path: /metrics

For AdGuard Home, use the adguardhome-sync sidecar or a dedicated exporter.

Query Logging

For troubleshooting, enable query logging in AdGuard Home (Settings → General → Query log). Be aware that logging all DNS queries creates a significant privacy record — disable it after debugging.

For Unbound:

1
2
3
4
5
# unbound.conf
server:
    log-queries: yes
    log-replies: yes
    log-tag-queryreply: yes

Privacy Considerations and the DoH Centralization Debate

DoH has generated genuine controversy in the security community. The technical properties that make it good for end-user privacy create complications elsewhere.

The Centralization Problem

When browsers implement DoH with hardcoded public resolvers (Firefox defaulting to Cloudflare), it concentrates DNS traffic at large providers. Cloudflare can see query patterns for all Firefox users using the default setting — a significant surveillance opportunity regardless of their stated privacy policies.

The alternative — querying root servers recursively — distributes trust across the DNS hierarchy but is slower and doesn’t encrypt queries to TLD nameservers or authoritative servers.

Running your own recursive resolver (Unbound + DoH/DoT frontend) gets the best of both worlds: encrypted client-to-resolver queries, no single provider seeing all your queries, and DNS responses validated with DNSSEC.

ISP vs. Provider Visibility

DoH/DoT moves trust from your ISP to your resolver provider. This is often (not always) an improvement:

  • ISP: Has contractual relationship, knows your physical address, is subject to local law enforcement data requests, may sell data
  • Cloudflare/Google/NextDNS: Larger target for legal requests, but typically in different jurisdiction, may have stronger privacy policies, subject to public scrutiny

Self-hosting eliminates this entirely.

ESNI / ECH: The Remaining Gap

Even with DoH/DoT, the SNI (Server Name Indication) in the TLS handshake reveals which website you’re connecting to. Encrypted Client Hello (ECH, formerly ESNI) encrypts this field, plugging the remaining metadata leak.

ECH is gradually rolling out (Cloudflare supports it, Firefox supports it). When combined with DoH/DoT, it leaves very little DNS and connection metadata visible on the network path.

What DoH/DoT Doesn’t Hide

  • IP addresses: Traffic to 140.82.112.4 (GitHub’s IP) is visible even if the hostname lookup was encrypted
  • TLS SNI (until ECH is universal): Hostname in TLS handshake
  • Connection timing: Patterns of connections reveal behavior even without content
  • Authoritative nameserver queries: Your recursive resolver’s queries to authoritative servers are still plaintext (though yours is on a server you control)

DoH/DoT is a meaningful privacy improvement, not a complete solution. Combined with a VPN or Tor for the traffic itself, it closes most passive monitoring vectors.

For a homelab that balances privacy, control, and simplicity:

Clients (DoT/DoH) ──→ AdGuard Home ──→ Unbound (recursive, DNSSEC)
                            │
                            ├── Ad/malware blocking
                            ├── Query logging
                            └── Split-horizon for local names

1. Deploy Unbound for recursive resolution with DNSSEC validation. Unbound queries root servers directly — no upstream provider involved.

2. Deploy AdGuard Home as the frontend, with:

  • Unbound at 127.0.0.1:5300 as the upstream
  • DoT enabled on port 853 with a Let’s Encrypt certificate
  • DoH enabled on port 443
  • Blocklists for ads and malware
  • Rewrites for local hostnames (nas.home → 192.168.1.50)

3. Configure clients:

  • Android: Private DNS → dns.yourdomain.com
  • Linux: systemd-resolved DNSOverTLS
  • macOS/iOS: Configuration Profile with DoH URL
  • Browsers: Custom DoH URL

4. Fallback: Configure 1.1.1.1 as a fallback in case your local resolver is down — encrypted DNS even in degraded states.

The result: all DNS queries from your devices are encrypted in transit, validated against DNSSEC, and filtered for ads and malware. No ISP DNS visibility, no single-provider trust dependency. The phone book is finally sealed.

Comments