fix(mail): claim inbound IPv6 so IMAP/MX over v6 work; pin Postfix to v4 #98

Merged
dominik.polakovics merged 1 commit from afk/97 into main 2026-06-05 13:45:25 +02:00

Closes #97

What

mail published an AAAA (imap/mail.cloonar.com2a01:4f8:c012:9d85::2) but never configured the address, so inbound IPv6 SYNs to IMAPS/MX reached the host's link with no owner and were black-holed — connections time out rather than being refused. This stayed latent until web-arm gained v6 egress (ADR 0010): RFC 6724 then made its FreeScout prefer ::2 for imap.cloonar.com, and PHP's IMAP client (stream_socket_client / webklex/php-imap) has no happy-eyeballs / v6→v4 fallback, so it hung.

Changes

  • hosts/mail/configuration.nix — declare 2a01:4f8:c012:9d85::2/64 on enp1s0 and defaultGateway6 = fe80::1, claiming the published AAAA so inbound IMAPS and MX-over-v6 work. Dovecot already listens on [::]:993, so no change there.
  • hosts/mail/modules/postfix.nix — pin smtp_address_preference = ipv4. ::2 has no rDNS and is in no sending domain's SPF, so the default any would prefer the v6 source to dual-stack MXes (Gmail/Microsoft) and get filed as spam. The address and the pin are coupled and ship together.
  • docs/adr/0016-mail-claims-inbound-ipv6.md — records the decision; the mail counterpart of ADR 0010.

Verification

  • Pre-commit dry-build of mail passes (:: mail OK) — the project's eval gate.
  • dig AAAA imap.cloonar.com and mail.cloonar.com both return 2a01:4f8:c012:9d85::2, confirming the claimed address matches the live record (A → 91.107.201.241, matching the diag finding).
  • The dry-build is eval-only and cannot verify the literal address/interface (ADR 0010's known gap). Before merge, re-confirm 2a01:4f8:c012:9d85::2 + enp1s0 against the Hetzner console.

Post-deploy checks

  • ip -6 route shows default via fe80::1 dev enp1s0.
  • External IMAPS to [2a01:4f8:c012:9d85::2]:993 connects and FreeScout fetches mail.
  • postconf smtp_address_preferenceipv4; a test mail to Gmail still leaves over IPv4 (check the Received: header).

Notes

  • Outbound stays on IPv4 exactly as today; real v6 sending (PTR + SPF across all sending domains) is separate future work.
  • Once ::2 is up, the address-family-agnostic firewall makes every open port reachable over v6 too — including LDAP 389/636 — mirroring today's v4 exposure. Explicitly accepted; out of scope here.
  • Admin/SSH stays on IPv4, so a bad v6 route can't lock anyone out.
Closes #97 ## What `mail` published an `AAAA` (`imap`/`mail.cloonar.com` → `2a01:4f8:c012:9d85::2`) but never configured the address, so inbound IPv6 SYNs to IMAPS/MX reached the host's link with no owner and were **black-holed** — connections time out rather than being refused. This stayed latent until web-arm gained v6 egress (ADR 0010): RFC 6724 then made its FreeScout prefer `::2` for `imap.cloonar.com`, and PHP's IMAP client (`stream_socket_client` / `webklex/php-imap`) has no happy-eyeballs / v6→v4 fallback, so it hung. ## Changes - **`hosts/mail/configuration.nix`** — declare `2a01:4f8:c012:9d85::2/64` on `enp1s0` and `defaultGateway6 = fe80::1`, claiming the published `AAAA` so inbound IMAPS and MX-over-v6 work. Dovecot already listens on `[::]:993`, so no change there. - **`hosts/mail/modules/postfix.nix`** — pin `smtp_address_preference = ipv4`. `::2` has no rDNS and is in no sending domain's SPF, so the default `any` would prefer the v6 source to dual-stack MXes (Gmail/Microsoft) and get filed as spam. The address and the pin are coupled and ship together. - **`docs/adr/0016-mail-claims-inbound-ipv6.md`** — records the decision; the `mail` counterpart of ADR 0010. ## Verification - Pre-commit dry-build of `mail` passes (`:: mail OK`) — the project's eval gate. - `dig AAAA imap.cloonar.com` and `mail.cloonar.com` both return `2a01:4f8:c012:9d85::2`, confirming the claimed address matches the live record (A → `91.107.201.241`, matching the diag finding). - The dry-build is **eval-only** and cannot verify the literal address/interface (ADR 0010's known gap). **Before merge, re-confirm `2a01:4f8:c012:9d85::2` + `enp1s0` against the Hetzner console.** ### Post-deploy checks - `ip -6 route` shows `default via fe80::1 dev enp1s0`. - External IMAPS to `[2a01:4f8:c012:9d85::2]:993` connects and FreeScout fetches mail. - `postconf smtp_address_preference` → `ipv4`; a test mail to Gmail still leaves over IPv4 (check the `Received:` header). ## Notes - Outbound stays on IPv4 exactly as today; real v6 *sending* (PTR + SPF across all sending domains) is separate future work. - Once `::2` is up, the address-family-agnostic firewall makes every open port reachable over v6 too — including LDAP 389/636 — mirroring today's v4 exposure. Explicitly accepted; out of scope here. - Admin/SSH stays on IPv4, so a bad v6 route can't lock anyone out.
mail published an AAAA (imap/mail.cloonar.com -> 2a01:4f8:c012:9d85::2) but
never configured the address, so inbound v6 SYNs to IMAPS/MX reached the link
with no owner and were black-holed (timeouts, not refusals). This stayed
latent until web-arm gained v6 egress (ADR 0010): its FreeScout then preferred
::2 for imap and hung, since PHP's IMAP client has no happy-eyeballs / v6->v4
fallback (#97).

- hosts/mail/configuration.nix: declare 2a01:4f8:c012:9d85::2/64 on enp1s0 and
  defaultGateway6 fe80::1, claiming the published AAAA so inbound IMAPS and
  MX-over-v6 work. Dovecot already listens on [::]:993, so no change there.
- hosts/mail/modules/postfix.nix: pin smtp_address_preference = ipv4. ::2 has
  no rDNS and is in no sending domain's SPF, so the default "any" would prefer
  the v6 source to dual-stack MXes (Gmail/Microsoft) and get filed as spam. The
  address and the pin are coupled and ship together.
- docs/adr/0016: record the decision; mail counterpart of ADR 0010.

Outbound stays on IPv4 exactly as today; real v6 sending (PTR + SPF) is
separate future work. The literal address/interface are eval-unverifiable and
were confirmed against the live diag channel and the published AAAA.
Author
Owner

This was generated by AI while landing a PR.

Landed. Validated and merged with tea pr merge --style merge (merge commit). Issue #97 auto-closed via the Closes #97 link.

Verification signal relied on: the repo's pre-commit dry-build (eval gate, :: mail OK) — not re-run. The diff changes no derivation src/*Hash, so eval-only is adequate at the Nix layer; its only blind spot is the literal address/interface (handled below).

Checked:

  • AFK contractafk/97main, working Closes #97; issue auto-closed on merge, releasing the branch claim.
  • Conventions — Conventional-Commits title scoped to host; no secrets.yaml edits; system.stateVersion untouched (22.11); ADR 0016 is the correct next number (main was at 0015, no collision/renumber).
  • Mirrors the live ADR-0010 precedentenp1s0 + interface-qualified defaultGateway6 = { address = "fe80::1"; interface = "enp1s0"; }, the same pattern already deployed on web-arm.
  • The Postfix pin actually renders — mail is the first host to mix the legacy config = {…} block with the new settings.main pin. Confirmed against the nixos-25.11 module that services.postfix.config is a mkRenamedOptionModule onto settings.main, and main.cf is built from settings.main — so the two merge into one attrset, smtp_address_preference is unique (no key collision), and the pin takes effect. The address+pin coupling is correct and documented in both files and the ADR.
  • enp1s0 literal — already live-confirmed on mail via the read-only diag channel during triage (#97 captured 91.107.201.241/32 + fe80::9400:2ff:fe23:b445 on enp1s0).

Post-deploy checks (the deploy action syncs to the SFTP chroot; mail runs nixos-rebuild switch within ~5 min via bento):

  • ip -6 routedefault via fe80::1 dev enp1s0
  • external IMAPS to [2a01:4f8:c012:9d85::2]:993 connects, and FreeScout (web-arm) fetches mail again
  • postconf smtp_address_preferenceipv4; a test mail to a Gmail address still shows an IPv4 Received: source

Failure mode of any wrong literal is bounded — admin/SSH stays on IPv4, so a bad v6 route cannot lock anyone out.

> *This was generated by AI while landing a PR.* **Landed.** Validated and merged with `tea pr merge --style merge` (merge commit). Issue #97 auto-closed via the `Closes #97` link. **Verification signal relied on:** the repo's pre-commit dry-build (eval gate, `:: mail OK`) — not re-run. The diff changes no derivation `src`/`*Hash`, so eval-only is adequate at the Nix layer; its only blind spot is the literal address/interface (handled below). **Checked:** - **AFK contract** — `afk/97` → `main`, working `Closes #97`; issue auto-closed on merge, releasing the branch claim. - **Conventions** — Conventional-Commits title scoped to host; no `secrets.yaml` edits; `system.stateVersion` untouched (22.11); ADR **0016** is the correct next number (main was at 0015, no collision/renumber). - **Mirrors the live ADR-0010 precedent** — `enp1s0` + interface-qualified `defaultGateway6 = { address = "fe80::1"; interface = "enp1s0"; }`, the same pattern already deployed on web-arm. - **The Postfix pin actually renders** — mail is the first host to mix the legacy `config = {…}` block with the new `settings.main` pin. Confirmed against the nixos-25.11 module that `services.postfix.config` is a `mkRenamedOptionModule` onto `settings.main`, and `main.cf` is built from `settings.main` — so the two merge into one attrset, `smtp_address_preference` is unique (no key collision), and the pin takes effect. The address+pin coupling is correct and documented in both files and the ADR. - **`enp1s0` literal** — already live-confirmed on mail via the read-only diag channel during triage (#97 captured `91.107.201.241/32` + `fe80::9400:2ff:fe23:b445` on `enp1s0`). **Post-deploy checks** (the deploy action syncs to the SFTP chroot; mail runs `nixos-rebuild switch` within ~5 min via bento): - `ip -6 route` → `default via fe80::1 dev enp1s0` - external IMAPS to `[2a01:4f8:c012:9d85::2]:993` connects, and FreeScout (web-arm) fetches mail again - `postconf smtp_address_preference` → `ipv4`; a test mail to a Gmail address still shows an IPv4 `Received:` source Failure mode of any wrong literal is bounded — admin/SSH stays on IPv4, so a bad v6 route cannot lock anyone out.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!98
No description provided.