coredns: DoT (:853) + DoH (:443) listeners with self-signed cert
- New Corefile snippet (common) shared across plain DNS / DoT / DoH so zone-loading + forward + cache stay DRY across all three transports - scripts/generate-certs.sh: openssl-only self-signed RSA cert with SANs for localhost / 127.0.0.1 / ::1 / coredns / dns.local. Idempotent — skips regeneration if cert is valid >24h ahead; FORCE=1 to rotate. - Key chmod is 0644 so the CoreDNS container's nonroot user can read it via the bind mount. Acceptable for local dev; production should mount real certs with proper UID/GID. - DOT_PORT=8853, DOH_PORT=8443 (avoids Caddy already-on-443 collision) - Makefile: `make certs`, `make test-tls` - All three transports verified end-to-end (dig +tls, dig +https, curl with raw RFC 8484 wire format)
This commit is contained in:
parent
1f11c314b9
commit
066ba1892a
8
.env
8
.env
@ -8,3 +8,11 @@ COREDNS_IMAGE=coredns/coredns:1.11.3
|
|||||||
DNS_PORT=1053
|
DNS_PORT=1053
|
||||||
METRICS_PORT=9153
|
METRICS_PORT=9153
|
||||||
HEALTH_PORT=8080
|
HEALTH_PORT=8080
|
||||||
|
|
||||||
|
# DoT (DNS-over-TLS, RFC 7858) — IANA port 853. Host port 8853 to
|
||||||
|
# stay unprivileged.
|
||||||
|
DOT_PORT=8853
|
||||||
|
|
||||||
|
# DoH (DNS-over-HTTPS, RFC 8484) — typically 443. Host port 8443
|
||||||
|
# because Caddy already owns 443 on this host.
|
||||||
|
DOH_PORT=8443
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,8 @@
|
|||||||
# Prepared zones are generated from zones/ by scripts/prepare-zones.sh
|
# Prepared zones are generated from zones/ by scripts/prepare-zones.sh
|
||||||
zones-prepared/*.zone
|
zones-prepared/*.zone
|
||||||
|
|
||||||
|
# Self-signed certs (re-generated by scripts/generate-certs.sh)
|
||||||
|
certs/*.pem
|
||||||
|
|
||||||
# Local-only env overrides
|
# Local-only env overrides
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
36
Corefile
36
Corefile
@ -1,25 +1,37 @@
|
|||||||
. {
|
# Shared zone-loading + recursive-forwarding config.
|
||||||
# Authoritative: load every <zone>.zone in /zones via the auto plugin.
|
# CoreDNS snippets are textually expanded by `import`, so we keep anything
|
||||||
# Filename pattern (.*)\.zone yields the zone name from the first group.
|
# that's not transport-specific (TLS) in here.
|
||||||
# CoreDNS reloads modified files every 30s.
|
(common) {
|
||||||
auto {
|
auto {
|
||||||
directory /zones (.*)\.zone {1}
|
directory /zones (.*)\.zone {1}
|
||||||
reload 30s
|
reload 30s
|
||||||
}
|
}
|
||||||
|
|
||||||
# Anything not authoritative falls through to upstream resolvers.
|
|
||||||
forward . 1.1.1.1 1.0.0.1 9.9.9.9 {
|
forward . 1.1.1.1 1.0.0.1 9.9.9.9 {
|
||||||
max_concurrent 1000
|
max_concurrent 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
# In-memory cache (TTL clamp 30s for both pos/neg).
|
|
||||||
cache 30
|
cache 30
|
||||||
|
|
||||||
# Operational plugins
|
|
||||||
health :8080
|
|
||||||
prometheus :9153
|
|
||||||
errors
|
errors
|
||||||
log
|
log
|
||||||
loop
|
loop
|
||||||
reload 10s
|
reload 10s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Plain DNS — UDP/TCP :53. Health + metrics live here only (one binding).
|
||||||
|
. {
|
||||||
|
import common
|
||||||
|
health :8080
|
||||||
|
prometheus :9153
|
||||||
|
}
|
||||||
|
|
||||||
|
# DNS-over-TLS — RFC 7858. Port 853 is the IANA-assigned DoT port.
|
||||||
|
tls://.:853 {
|
||||||
|
tls /etc/coredns/certs/cert.pem /etc/coredns/certs/key.pem
|
||||||
|
import common
|
||||||
|
}
|
||||||
|
|
||||||
|
# DNS-over-HTTPS — RFC 8484. Default path is /dns-query.
|
||||||
|
# Clients: curl -H 'accept: application/dns-message' https://host:8443/dns-query?dns=...
|
||||||
|
https://.:443 {
|
||||||
|
tls /etc/coredns/certs/cert.pem /etc/coredns/certs/key.pem
|
||||||
|
import common
|
||||||
|
}
|
||||||
|
|||||||
32
Makefile
32
Makefile
@ -2,7 +2,7 @@
|
|||||||
SHELL := /usr/bin/env bash
|
SHELL := /usr/bin/env bash
|
||||||
COMPOSE := docker compose
|
COMPOSE := docker compose
|
||||||
|
|
||||||
.PHONY: help prep up down restart logs ps test reload clean
|
.PHONY: help prep certs up down restart logs ps test test-tls reload clean
|
||||||
|
|
||||||
help: ## Show this help
|
help: ## Show this help
|
||||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
@ -10,7 +10,10 @@ help: ## Show this help
|
|||||||
prep: ## Re-inject SOA records into all zones (writes zones-prepared/)
|
prep: ## Re-inject SOA records into all zones (writes zones-prepared/)
|
||||||
@./scripts/prepare-zones.sh
|
@./scripts/prepare-zones.sh
|
||||||
|
|
||||||
up: prep ## Start CoreDNS (prepares zones first)
|
certs: ## Generate self-signed cert for DoT/DoH (re-run with FORCE=1 to rotate)
|
||||||
|
@./scripts/generate-certs.sh
|
||||||
|
|
||||||
|
up: prep certs ## Start CoreDNS (prepares zones + ensures certs exist first)
|
||||||
$(COMPOSE) up -d
|
$(COMPOSE) up -d
|
||||||
@sleep 2 && $(COMPOSE) logs --tail=20 coredns
|
@sleep 2 && $(COMPOSE) logs --tail=20 coredns
|
||||||
|
|
||||||
@ -29,11 +32,28 @@ logs: ## Tail CoreDNS logs
|
|||||||
ps: ## Show container status
|
ps: ## Show container status
|
||||||
$(COMPOSE) ps
|
$(COMPOSE) ps
|
||||||
|
|
||||||
test: ## Smoke-test against a known zone (uses DNS_PORT from .env)
|
test: ## Smoke-test plain DNS (uses DNS_PORT from .env)
|
||||||
@. ./.env && echo "Querying acrazy.org @ 127.0.0.1:$$DNS_PORT" && \
|
@. ./.env && echo "Querying acrazy.org @ 127.0.0.1:$$DNS_PORT (plain DNS)" && \
|
||||||
dig @127.0.0.1 -p $$DNS_PORT acrazy.org SOA +short && \
|
dig @127.0.0.1 -p $$DNS_PORT acrazy.org SOA +short && \
|
||||||
dig @127.0.0.1 -p $$DNS_PORT acrazy.org NS +short && \
|
dig @127.0.0.1 -p $$DNS_PORT acrazy.org NS +short && \
|
||||||
dig @127.0.0.1 -p $$DNS_PORT or.acrazy.org A +short
|
dig @127.0.0.1 -p $$DNS_PORT or.acrazy.org A +short
|
||||||
|
|
||||||
clean: down ## Remove containers + prepared zones
|
test-tls: ## Smoke-test DoT + DoH (pins self-signed cert via +tls-ca)
|
||||||
rm -rf zones-prepared/*.zone
|
@. ./.env && \
|
||||||
|
echo "=== DoT @ 127.0.0.1:$$DOT_PORT ===" && \
|
||||||
|
dig @127.0.0.1 -p $$DOT_PORT +tls +tls-ca=certs/cert.pem \
|
||||||
|
+tls-hostname=localhost acrazy.org SOA +short && \
|
||||||
|
echo "" && \
|
||||||
|
echo "=== DoH @ https://localhost:$$DOH_PORT/dns-query ===" && \
|
||||||
|
dig @localhost -p $$DOH_PORT +https +tls-ca=certs/cert.pem \
|
||||||
|
acrazy.org A +short && \
|
||||||
|
echo "" && \
|
||||||
|
echo "=== DoH via curl (raw wire-format) ===" && \
|
||||||
|
curl -sk --cacert certs/cert.pem \
|
||||||
|
-H 'accept: application/dns-message' \
|
||||||
|
--data-binary @<(printf '\x00\x00\x01\x20\x00\x01\x00\x00\x00\x00\x00\x00\x06acrazy\x03org\x00\x00\x01\x00\x01') \
|
||||||
|
-H 'content-type: application/dns-message' \
|
||||||
|
"https://localhost:$$DOH_PORT/dns-query" | xxd | head -5
|
||||||
|
|
||||||
|
clean: down ## Remove containers + prepared zones + certs
|
||||||
|
rm -rf zones-prepared/*.zone certs/*.pem
|
||||||
|
|||||||
@ -7,11 +7,14 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${DNS_PORT}:53/udp"
|
- "${DNS_PORT}:53/udp"
|
||||||
- "${DNS_PORT}:53/tcp"
|
- "${DNS_PORT}:53/tcp"
|
||||||
|
- "${DOT_PORT}:853/tcp"
|
||||||
|
- "${DOH_PORT}:443/tcp"
|
||||||
- "${METRICS_PORT}:9153/tcp"
|
- "${METRICS_PORT}:9153/tcp"
|
||||||
- "${HEALTH_PORT}:8080/tcp"
|
- "${HEALTH_PORT}:8080/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- ./Corefile:/etc/coredns/Corefile:ro
|
- ./Corefile:/etc/coredns/Corefile:ro
|
||||||
- ./zones-prepared:/zones:ro
|
- ./zones-prepared:/zones:ro
|
||||||
|
- ./certs:/etc/coredns/certs:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
47
scripts/generate-certs.sh
Executable file
47
scripts/generate-certs.sh
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate a self-signed RSA cert for CoreDNS DoT/DoH (local dev only).
|
||||||
|
# Production deployments should mount real Let's Encrypt certs (e.g. from
|
||||||
|
# Caddy's shared volume) into certs/ instead.
|
||||||
|
#
|
||||||
|
# SANs cover the names a local resolver client is likely to use:
|
||||||
|
# - localhost (loopback by name)
|
||||||
|
# - 127.0.0.1 / ::1 (loopback by IP)
|
||||||
|
# - coredns (docker container DNS name)
|
||||||
|
# - dns.local (convenient pinning hostname)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CERT_DIR="${CERT_DIR:-certs}"
|
||||||
|
DAYS="${DAYS:-825}" # 825 is the historical macOS/Apple cap; safe ceiling
|
||||||
|
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
if [[ -f "$CERT_DIR/cert.pem" && -f "$CERT_DIR/key.pem" ]]; then
|
||||||
|
# Skip regeneration if not expired and re-prep was not forced.
|
||||||
|
if [[ "${FORCE:-0}" != "1" ]]; then
|
||||||
|
# openssl returns 0 if cert is valid beyond the given window (24h here).
|
||||||
|
if openssl x509 -checkend 86400 -noout -in "$CERT_DIR/cert.pem" >/dev/null 2>&1; then
|
||||||
|
echo "Cert at $CERT_DIR/cert.pem still valid (>24h). Set FORCE=1 to regenerate."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Single-shot self-signed cert with SANs. -addext requires openssl >= 1.1.1.
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes -days "$DAYS" \
|
||||||
|
-keyout "$CERT_DIR/key.pem" \
|
||||||
|
-out "$CERT_DIR/cert.pem" \
|
||||||
|
-subj "/CN=coredns-local" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,DNS:coredns,DNS:dns.local,IP:127.0.0.1,IP:::1" \
|
||||||
|
>/dev/null 2>&1
|
||||||
|
|
||||||
|
# 0644 (not 0600) on the key so the CoreDNS container's `nonroot` user
|
||||||
|
# can read it via the bind mount. Acceptable for a local-dev self-signed
|
||||||
|
# cert whose private key never leaves this directory. For production
|
||||||
|
# certs, mount with explicit UID/GID via :ro,uid=65532 or use a tmpfs/
|
||||||
|
# secret instead.
|
||||||
|
chmod 644 "$CERT_DIR/key.pem"
|
||||||
|
chmod 644 "$CERT_DIR/cert.pem"
|
||||||
|
|
||||||
|
echo "Generated $CERT_DIR/{cert,key}.pem (valid ${DAYS} days)"
|
||||||
|
openssl x509 -in "$CERT_DIR/cert.pem" -noout -subject -issuer -dates \
|
||||||
|
-ext subjectAltName 2>&1 | sed 's/^/ /'
|
||||||
Loading…
x
Reference in New Issue
Block a user