Defer Unbound; manage DNS via systemd-resolved (Hetzner + Quad9)

closed
#38de33d opened by agent Sep 16

Decision

v1 simplicity: no local Unbound recursive resolver. Outbound mail goes via SMTP relay (smarthost), which removes direct-MX lookups from the cell.

References:

  • docs/deployment.md:67 lists Unbound as a cell service, but docs/deployment.md:135 shows dns/ unbound resolver (planned) — the role does not exist yet.
  • deployment/playbooks/site.yml has no dns/unbound role in the converge order; deployment/roles/recovery-bootstrap/tasks/main.yml:238 notes “No unbound/DNS unit exists yet (TBD)”.
  • Outbound relay decided in docs/adr/0009-cell-architecture.md:101-108 (D28) and docs/deployment.md:71-74; internal/relay/stalwart.go ApplyOutbound routes all non-local mail via the sovrn-relay MtaRoute, and router.go:115-127 fails startup when relay config fails so a cell never silently falls back to direct-MX.

Remaining cell DNS with relay (low volume, any recursion works):

  • smarthost A/AAAA for mail-eu.smtp2go.com:2525 (sovrn.yml:125-138),
  • sovrnd dnsprober MX/TXT/CNAME via net.DefaultResolver (internal/domain/resolver.go, internal/dnsprober/net.go),
  • healthz dns leg via net.DefaultResolver (healthz.go:291-309),
  • ACME Http01 HTTPS to the LE directory + inbound challenge (deployment/roles/stalwart/files/bootstrap-stalwart.sh:285-412).

Plan — DNS via systemd-resolved (Hetzner + unfiltered Quad9)

No new dns/ role. Manage the host resolver in the common/ role:

  • deployment/inventory/group_vars/all/sovrn.yml: add sovrn_dns_primary (185.12.64.1 185.12.64.2 + 2a01:4ff:ff00::add:1 2a01:4ff:ff00::add:2) and sovrn_dns_fallback (9.9.9.10 149.112.112.10, unfiltered Quad9 — no threat-block, so dnsprober/verifier sees ground truth and inbound senders never vanish; DNSSEC validation stays on).
  • deployment/roles/common/: write /etc/systemd/resolved.conf.d/sovrn-dns.conf (DNS=, FallbackDNS=, LLMNR=no, MulticastDNS=no, DNSSEC=allow-trust-anchor), enable systemd-resolved, symlink /etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf so all Go net.DefaultResolver users and Stalwart hickory-resolver in system mode hit the 127.0.0.53 cache. Do NOT write a static /etc/resolv.conf — Hetzner cloud-init/DHCP would clobber it on reboot.
  • Docs cleanup: docs/deployment.md, ADR-0009 D24, sovrn.yml/healthz comments off the “local Unbound must answer” assumption.
  • Verify on staging: just update, resolvectl status shows Hetzner+Quad9, dig @127.0.0.53 example.com, GET /healthz green, relay outbound e2e, reboot-persistence check.

Revisit triggers

Enable Stalwart inbound RBL/DNSBL or SPF/DKIM-verify, MTA-STS/DANE needs, observed provider-resolver incidents/latency, or a hard DNSSEC requirement. Tracked in the child issue (inbound spam architecture + Unbound revisit).

1 Comment

agent 388adbe Sep 16

Implemented (v1 simplicity path, unfiltered Quad9).

Changes:

  • deployment/inventory/group_vars/all/sovrn.yml: new sovrn_dns_primary (Hetzner 185.12.64.1 185.12.64.2 + 2a01:4ff:ff00::add:1 2a01:4ff:ff00::add:2), sovrn_dns_fallback (9.9.9.10 149.112.112.10, unfiltered), sovrn_dns_dnssec (allow-trust-anchor); health comment reworded off Unbound.
  • deployment/roles/common/: new templates/sovrn-dns.conf.j2 drop-in + handlers/main.yml (restart systemd-resolved); tasks/main.yml installs systemd-resolved, writes /etc/systemd/resolved.conf.d/sovrn-dns.conf, symlinks /etc/resolv.conf -> stub, enables the service. No static resolv.conf (Hetzner cloud-init would clobber it).
  • Docs/comments: docs/deployment.md (service list, health paragraph, layout dns/ entry), healthz.go resolver comment, recovery-bootstrap start-order comment. ADR-0009 D24 left untouched (accepted record; deferral documented here instead).

Verification:

  • go build ./... + go test -run 'TestHealth|TestDns|TestDNS' . — ok.
  • ansible-playbook --syntax-check playbooks/site.yml — ok.
  • Jinja render of the drop-in with prod values — ok.

Staging still to do: just update, resolvectl status, dig @127.0.0.53, /healthz green, relay e2e, reboot persistence.