There you go!

Essentially, this setup achieves 5 features I wanted my DNS to have:

- Confidentiality: from my ISP; and from anyone listening to the air for plain-text DNS questions when I'm on public WiFi. Solution: DNS-over-TLS[1]

- Integrity: of the answers I get. Solution: DNS-over-TLS authenticates the server

- Privacy: from web trackers, ads, etc. Solution: domain name blacklist

- Speed: as in, fast resolution times. Solution: caching and cache prefetching[2]

- Observability: my previous DNS was Dnsmasq[3], AFAIK Dnsmasq doesn't log requests, only gives a couple stats[4], etc. Solution: a Prometheus endpoint

CoreDNS ticks all of the above, and a couple others I found interesting to have.

To set it up, I wrote my own (better) CoreDNS Docker image[7] to run on my Kubernetes cluster; mounted my Corefile[8] and my certificates as volumes, and exposed it via a Kubernetes Service.

The Corefile[8] essentially sets up CoreDNS to:

- Log all requests and errors

- Forward DNS questions to Cloudflare's DNS-over-TLS servers

- Cache questions for min(TTL, 24h), prefetching any domains requested more than 5 times over the last 10 minutes before they expire

- If a domain resolves to more than one address, it automatically round-robins between them to distribute load

- Serve Prometheus-style metrics on 9153/TCP, and provide readiness and liveness checks for Kubernetes

- Load the /etc/hosts.blacklist hosts file (which has just short of 1M domains resolved to 0.0.0.0), reloads it every hour, and does not provide reverse lookups for performance reasons

- Listens on 53/UDP for regular plain-text DNS questions (LAN only), and on 853/TCP for DNS-over-TLS questions, which I have NAT'd so that I can use it when I'm outside

The domain blacklist I generate nightly with a Kubernetes CronJob that runs a Bash script[9]. It essentially pulls and deduplicates the domains in the "safe to use" domain blacklists compiled by https://firebog.net/, as well as removing (whitelisting) a couple hosts at the end.

That's pretty much it. The only downside to this set up is that CoreDNS takes just short of 400MiB of memory (I guess it keeps the resolve table on memory, but 400MiB!?) and lately I'm seeing some OOM restarts by Kubernetes, as it surpasses the 500MiB hard memory limit I have on it. A possible solution might be to keep the resolve table on Redis, which might take up less memory space, but I'm still to try that out.

[1] Which I find MUCH superior to DNS-over-HTTPS. The latter is simply a L7 hack to speed up adoption, but the correct technical solution is DoT, and operating systems should already support it by now (AFAIK, the only OS that supports DoT natively is Android 9+).

[2] It was when I discovered CoreDNS' cache prefetching that I convinced myself to switch to CoreDNS.

[3] http://www.thekelleys.org.uk/dnsmasq/doc.html

[4] It gives you very few stats. I also had to write my own Prometheus expoter[5] because Google's[6] had a fatal flaw and no one answered to the issue. In fact, they closed the Issues tab on GitHub a couple months after my request, so fuck you, Google!

[5] https://github.com/ricardbejarano/dnsmasq_exporter

[6] https://github.com/google/dnsmasq_exporter (as you can see the Issues tab is no longer present)

[7] https://github.com/ricardbejarano/coredns, less bloat than the official image, runs as non-root user, auditable build pipeline, compiled from source during build time. These are all nice to have and to comply with my non-root PodSecurityPolicy. I also like to run my own images just so that I know what's under the hood.

[8]

local:65535 {
ready
health
}

(global) {
log
errors

cache 86400 {
prefetch 5 10m 10%
}
dnssec
loadbalance

prometheus :9153
}

(cloudflare) {
forward . tls://1.1.1.1 tls://1.0.0.1 {
tls_servername cloudflare-dns.com
}
}

(blacklist) {
hosts /etc/hosts.blacklist {
reload 3600s
no_reverse
fallthrough
}
}

.:53 {
import global
import blacklist
import cloudflare
}

tls://.:853 {
import global
import blacklist
import cloudflare
tls /etc/tls/fullchain.pem /etc/tls/privkey.pem
}

[9]

#!/bin/bash

HOSTS_FILE="/tmp/hosts.blacklist"
HOSTS_FILES="$HOSTS_FILE.d"

mkdir -p "$HOSTS_FILES"
download() {
echo "download($1)"
curl \
--location --max-redirs 3 \
--max-time 20 --retry 3 --retry-delay 0 --retry-max-time 60 \
"$1" > "$(mktemp "$HOSTS_FILES"/XXXXXX)"
}

# https://firebog.net/
## suspicious domains
download "https://hosts-file.net/grm.txt"
download "https://reddestdream.github.io/Projects/MinimalHosts/etc/MinimalHostsBlocker/minimalhosts"
download "https://raw.githubusercontent.com/StevenBlack/hosts/master/data/KADhosts/hosts"
download "https://raw.githubusercontent.com/StevenBlack/hosts/master/data/add.Spam/hosts"
download "https://v.firebog.net/hosts/static/w3kbl.txt"
## advertising domains
download "https://adaway.org/hosts.txt"
download "https://v.firebog.net/hosts/AdguardDNS.txt"
download "https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt"
download "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt"
download "https://hosts-file.net/ad_servers.txt"
download "https://v.firebog.net/hosts/Easylist.txt"
download "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts;showintro=0"
download "https://raw.githubusercontent.com/StevenBlack/hosts/master/data/UncheckyAds/hosts"
download "https://www.squidblacklist.org/downloads/dg-ads.acl"
## tracking & telemetry domains
download "https://v.firebog.net/hosts/Easyprivacy.txt"
download "https://v.firebog.net/hosts/Prigent-Ads.txt"
download "https://gitlab.com/quidsup/notrack-blocklists/raw/master/notrack-blocklist.txt"
download "https://raw.githubusercontent.com/StevenBlack/hosts/master/data/add.2o7Net/hosts"
download "https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt"
## malicious domains
download "https://s3.amazonaws.com/lists.disconnect.me/simple_malvertising.txt"
download "https://mirror1.malwaredomains.com/files/justdomains"
download "https://hosts-file.net/exp.txt"
download "https://hosts-file.net/emd.txt"
download "https://hosts-file.net/psh.txt"
download "https://mirror.cedia.org.ec/malwaredomains/immortal_domains.txt"
download "https://www.malwaredomainlist.com/hostslist/hosts.txt"
download "https://bitbucket.org/ethanr/dns-blacklists/raw/8575c9f96e5b4a1308f2f12394abd86d0927a4a0/bad_lists/Mandiant_APT1_Report_Appendix_D.txt"
download "https://v.firebog.net/hosts/Prigent-Malware.txt"
download "https://v.firebog.net/hosts/Prigent-Phishing.txt"
download "https://phishing.army/download/phishing_army_blocklist_extended.txt"
download "https://gitlab.com/quidsup/notrack-blocklists/raw/master/notrack-malware.txt"
download "https://ransomwaretracker.abuse.ch/downloads/RW_DOMBL.txt"
download "https://ransomwaretracker.abuse.ch/downloads/CW_C2_DOMBL.txt"
download "https://ransomwaretracker.abuse.ch/downloads/LY_C2_DOMBL.txt"
download "https://ransomwaretracker.abuse.ch/downloads/TC_C2_DOMBL.txt"
download "https://ransomwaretracker.abuse.ch/downloads/TL_C2_DOMBL.txt"
download "https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist"
download "https://v.firebog.net/hosts/Shalla-mal.txt"
download "https://raw.githubusercontent.com/StevenBlack/hosts/master/data/add.Risk/hosts"
download "https://www.squidblacklist.org/downloads/dg-malicious.acl"

cat "$HOSTS_FILES"/* | \
sed \
-e 's/0.0.0.0//g' \
-e 's/127.0.0.1//g' \
-e '/255.255.255.255/d' \
-e '/::/d' \
-e '/#/d' \
-e 's/ //g' \
-e 's/ //g' \
-e '/^$/d' \
-e 's/^/0.0.0.0 /g' | \
awk '!a[$0]++' | \
sed \
-e '/gamovideo.com/d' \
-e '/openload.co/d' > "$HOSTS_FILE"

rm -rf "$HOSTS_FILES"