coredns/Corefile
Ryan Malloy 57c8366b7f coredns: document why HE-IP restriction lives at firewall, not CoreDNS
Goal was to restrict AXFR to Hurricane Electric's five secondary
nameserver IPs. Tried several CoreDNS Corefile syntaxes:

  transfer { to 216.218.130.2 ... 216.66.1.2 }       # space-separated
  transfer { to 216.218.130.2 \n to 216.218.131.2 }  # multi-line
  transfer { to 216.218.130.2 }                       # single IP
  transfer { to * 216.218.130.2 ... }                 # mixed

Every form with a specific IPv4 address silently breaks server-block
startup — the auto plugin still loads zones into memory but the
:53/:443/:853 listeners never bind. Reproducible on coredns/coredns
1.11.3 AND 1.12.2 with the (common) snippet + auto + forward shape.
Only `to *` results in healthy listener startup.

Even if we got CoreDNS-side filtering to work, Docker's default
userland-proxy rewrites source IPs to the bridge gateway, which would
break IP-based filtering anyway short of `network_mode: host`.

Decision: keep `to *` in CoreDNS, push HE-only filtering to the
FortiWiFi firewall (source-IP-restricted VIP/DNAT for WAN:53/tcp).
This is correct-layered defense — the perimeter does the IP work
before packets ever reach dell01.
2026-05-16 16:04:44 -06:00

60 lines
1.9 KiB
Plaintext

# Shared zone-loading + recursive-forwarding config.
# CoreDNS snippets are textually expanded by `import`, so we keep anything
# that's not transport-specific (TLS) in here.
(common) {
auto {
directory /zones (.*)\.zone {1}
reload 30s
}
# AXFR authorization is `to *` at this layer, with HE-only filtering
# done by the FortiWiFi firewall (source IP restriction on the
# TCP/53 DNAT rule). Reasons we don't filter at CoreDNS:
#
# 1. CoreDNS plugin quirk: `to <specific-IP>` (any form — single,
# multi-line, space-separated) silently fails to start server
# blocks. Reproduced on 1.11.3 and 1.12.2. Only `to *` works.
# 2. Docker port publishing with userland-proxy rewrites source
# IPs to the bridge gateway, so IP filtering wouldn't see HE's
# real address anyway (without network_mode: host).
# 3. Filtering at the perimeter (FortiWiFi) is correct-layered
# defense: bad packets don't reach the host at all.
#
# Required FortiWiFi rule:
# VIP "coredns-tcp" — src in {216.218.130.2, 216.218.131.2,
# 216.218.132.2, 216.218.133.2, 216.66.1.2} —
# dst WAN:53/tcp → 172.16.1.15:5353/tcp
transfer {
to *
}
forward . 1.1.1.1 1.0.0.1 9.9.9.9 {
max_concurrent 1000
}
cache 30
errors
log
loop
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
}