feat(web-arm): static IPv6 address, route, and Postfix v4-pin #81

Closed
opened 2026-06-03 15:33:53 +02:00 by dominik.polakovics · 1 comment

What to build

Give web-arm its own outbound IPv6 by statically configuring the Hetzner Cloud /64, without regressing outbound mail. This is the foundation both directions build on.

web-arm is a Hetzner Cloud CAX (ARM) host on enp1s0. It currently has no global IPv6 address and no default v6 route: Hetzner offers no DHCPv6, and SLAAC is unreliable here (the box is a router via docker/podman/wireguard, with accept_ra=0/autoconf=0). So we declare the address statically — exactly what cloud-init does on a stock image.

Apply to hosts/web-arm/:

networking.interfaces.enp1s0.ipv6.addresses = [
  { address = "2a01:4f8:c012:43b::1"; prefixLength = 64; }
];
networking.defaultGateway6 = { address = "fe80::1"; interface = "enp1s0"; };
services.postfix.config.smtp_address_preference = "ipv4";

The Postfix v4-pin must land in the same change as the route. Merging the route alone makes Postfix start preferring an IPv6 source with no rDNS/SPF to dual-stack MXes (Gmail etc.), silently regressing mail that works today.

Also add docs/adr/0010-static-ipv6-on-web-arm.md capturing the decision: static over DHCPv6 (not offered by Hetzner) and over SLAAC (unreliable on a forwarding host; MAC-derived address is a poor inbound AAAA target), plus why mail is pinned to v4.

Acceptance criteria

  • hosts/web-arm declares static 2a01:4f8:c012:43b::1/64 on enp1s0 + defaultGateway6 via fe80::1
  • Postfix pinned to IPv4 (smtp_address_preference = ipv4) in the same change
  • docs/adr/0010-static-ipv6-on-web-arm.md added with the static-vs-SLAAC/DHCPv6 + mail-pin rationale
  • web-arm dry-build passes (pre-commit hook)
  • Post-deploy: ip -6 route shows default via fe80::1; curl -6 from the host reaches the v6 internet
  • Post-deploy: nginx listening on :::80 and :::443 (ss -tlnp)
  • Post-deploy: outbound mail still leaves over IPv4 (deliverability unchanged)

Blocked by

None - can start immediately

## What to build Give web-arm its own **outbound IPv6** by statically configuring the Hetzner Cloud /64, without regressing outbound mail. This is the foundation both directions build on. web-arm is a Hetzner Cloud CAX (ARM) host on `enp1s0`. It currently has no global IPv6 address and no default v6 route: Hetzner offers no DHCPv6, and SLAAC is unreliable here (the box is a router via docker/podman/wireguard, with `accept_ra=0`/`autoconf=0`). So we declare the address statically — exactly what cloud-init does on a stock image. Apply to `hosts/web-arm/`: ```nix networking.interfaces.enp1s0.ipv6.addresses = [ { address = "2a01:4f8:c012:43b::1"; prefixLength = 64; } ]; networking.defaultGateway6 = { address = "fe80::1"; interface = "enp1s0"; }; services.postfix.config.smtp_address_preference = "ipv4"; ``` The Postfix v4-pin **must** land in the same change as the route. Merging the route alone makes Postfix start preferring an IPv6 source with no rDNS/SPF to dual-stack MXes (Gmail etc.), silently regressing mail that works today. Also add `docs/adr/0010-static-ipv6-on-web-arm.md` capturing the decision: static over DHCPv6 (not offered by Hetzner) and over SLAAC (unreliable on a forwarding host; MAC-derived address is a poor inbound AAAA target), plus why mail is pinned to v4. ## Acceptance criteria - [ ] `hosts/web-arm` declares static `2a01:4f8:c012:43b::1/64` on `enp1s0` + `defaultGateway6` via `fe80::1` - [ ] Postfix pinned to IPv4 (`smtp_address_preference = ipv4`) in the same change - [ ] `docs/adr/0010-static-ipv6-on-web-arm.md` added with the static-vs-SLAAC/DHCPv6 + mail-pin rationale - [ ] web-arm dry-build passes (pre-commit hook) - [ ] Post-deploy: `ip -6 route` shows `default via fe80::1`; `curl -6` from the host reaches the v6 internet - [ ] Post-deploy: nginx listening on `:::80` and `:::443` (`ss -tlnp`) - [ ] Post-deploy: outbound mail still leaves over IPv4 (deliverability unchanged) ## Blocked by None - can start immediately
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Give web-arm a statically-configured global IPv6 address + default route on its WAN interface enp1s0, and pin Postfix outbound to IPv4 in the same change so mail deliverability does not regress.

Current behavior:
web-arm (Hetzner Cloud CAX / ARM) has no global IPv6 and no default IPv6 route. Confirmed live via the read-only diag channel: enp1s0 is the WAN NIC, carrying the public IPv4 188.34.191.144/32 (DHCP) and only a link-local fe80:: address; ip -6 route shows only on-link fe80::/64 entries — no default route, so outbound IPv6 is currently impossible. Hetzner offers no DHCPv6, and SLAAC is unsuitable: the host forwards for docker/podman/wireguard (RA-acceptance/autoconf off), and a MAC-derived SLAAC address would be an unstable, poor inbound AAAA target. Postfix sets no smtp_address_preference, so it runs on the Postfix default (any) — meaning the instant a default IPv6 route exists, Postfix will prefer an IPv6 source (which has no rDNS / SPF) when delivering to dual-stack MXes such as Gmail, silently regressing outbound mail that works today.

Desired behavior:
web-arm declares its Hetzner-routed /64 statically on enp1s0 plus a default IPv6 route via the Hetzner link-local gateway, giving it working outbound IPv6 — while outbound mail continues to leave over IPv4. The address, the route, and the Postfix v4-pin must ship in one change; the route without the pin is the regression.

Key interfaces / contracts:

  • web-arm's host configuration declares, via the standard NixOS networking options:
    • a static global IPv6 address 2a01:4f8:c012:43b::1, prefix length 64, on interface enp1s0
    • a default IPv6 gateway: address fe80::1 with interface enp1s0 (a link-local gateway must name its interface)
  • Postfix's main.cf parameter smtp_address_preference is set to ipv4, via whatever attribute the host's Postfix configuration exposes for main.cf settings (the repo configures Postfix through services.postfix). Behavioral contract, verifiable on the deployed host: postconf smtp_address_preference reports ipv4. The dry-build validates the exact Nix attribute.
  • A new ADR docs/adr/0010-static-ipv6-on-web-arm.md records the decision: static chosen over DHCPv6 (not offered by Hetzner) and over SLAAC (unreliable on a forwarding host; MAC-derived address is a poor inbound AAAA target), and why outbound mail is pinned to IPv4. (0010 is the next free ADR number as of triage; if another ADR has claimed it by implementation time, use the next free number.)

Literal values — use verbatim (maintainer-supplied from the Hetzner Cloud console):

  • routed /64 host address: 2a01:4f8:c012:43b::1/64
  • gateway: fe80::1 (Hetzner Cloud standard link-local gateway)
  • WAN interface: enp1s0 (confirmed live during triage)

These are opaque strings the dry-build cannot check for correctness — a wrong value passes eval and only surfaces post-deploy. Re-confirm the /64 and gateway against the Hetzner console before merge.

Acceptance criteria (agent's gate):

  • web-arm declares static 2a01:4f8:c012:43b::1/64 on enp1s0 and a default IPv6 route via fe80::1 on enp1s0
  • Postfix pinned to IPv4 (smtp_address_preference = ipv4) in the same change
  • docs/adr/0010-static-ipv6-on-web-arm.md added with the static-vs-SLAAC/DHCPv6 + mail-pin rationale
  • web-arm dry-build passes (pre-commit hook)

Acceptance criteria (post-deploy — human-verified after merge, not part of the PR):

  • ip -6 route on web-arm shows default via fe80::1 dev enp1s0; curl -6 from the host reaches the v6 internet
  • nginx still listening on :::80 and :::443 (ss -tlnp)
  • outbound mail still leaves over IPv4 — postconf smtp_address_preference = ipv4, deliverability unchanged

Out of scope:

  • Any DNS changes / AAAA records. Inbound IPv6 (publishing web-arm over v6) is #82 (pilot) and #83 (bulk rollout), which are manual Hetzner DNS / Cloudflare console changes — not this repo, not this issue.
  • Dual-stacking any production site or nginx vhost.
  • Any other host.
  • IPv4 addressing or the firewall port list. (Reviewer note, not a task: the existing allowedTCPPorts = [ 22 80 443 ] apply to IPv6 too once the address is up, so those three ports become reachable over v6 — same ports as v4, expected and acceptable.)
  • Explicitly setting accept_ra / autoconf / tempAddress — keep scope to address + route + mail-pin, matching the issue's own scoping, unless the dry-build or reviewer shows a stray SLAAC address would otherwise coexist with the static one.
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Give web-arm a statically-configured global IPv6 address + default route on its WAN interface `enp1s0`, and pin Postfix outbound to IPv4 in the **same change** so mail deliverability does not regress. **Current behavior:** web-arm (Hetzner Cloud CAX / ARM) has no global IPv6 and no default IPv6 route. Confirmed live via the read-only diag channel: `enp1s0` is the WAN NIC, carrying the public IPv4 `188.34.191.144/32` (DHCP) and only a link-local `fe80::` address; `ip -6 route` shows only on-link `fe80::/64` entries — no default route, so outbound IPv6 is currently impossible. Hetzner offers no DHCPv6, and SLAAC is unsuitable: the host forwards for docker/podman/wireguard (RA-acceptance/autoconf off), and a MAC-derived SLAAC address would be an unstable, poor inbound AAAA target. Postfix sets no `smtp_address_preference`, so it runs on the Postfix default (`any`) — meaning the instant a default IPv6 route exists, Postfix will prefer an IPv6 source (which has no rDNS / SPF) when delivering to dual-stack MXes such as Gmail, silently regressing outbound mail that works today. **Desired behavior:** web-arm declares its Hetzner-routed /64 statically on `enp1s0` plus a default IPv6 route via the Hetzner link-local gateway, giving it working outbound IPv6 — while outbound mail continues to leave over IPv4. The address, the route, and the Postfix v4-pin must ship in one change; the route without the pin is the regression. **Key interfaces / contracts:** - web-arm's host configuration declares, via the standard NixOS networking options: - a static global IPv6 address `2a01:4f8:c012:43b::1`, prefix length `64`, on interface `enp1s0` - a default IPv6 gateway: address `fe80::1` **with** interface `enp1s0` (a link-local gateway must name its interface) - Postfix's main.cf parameter `smtp_address_preference` is set to `ipv4`, via whatever attribute the host's Postfix configuration exposes for main.cf settings (the repo configures Postfix through `services.postfix`). Behavioral contract, verifiable on the deployed host: `postconf smtp_address_preference` reports `ipv4`. The dry-build validates the exact Nix attribute. - A new ADR `docs/adr/0010-static-ipv6-on-web-arm.md` records the decision: static chosen over DHCPv6 (not offered by Hetzner) and over SLAAC (unreliable on a forwarding host; MAC-derived address is a poor inbound AAAA target), and why outbound mail is pinned to IPv4. (0010 is the next free ADR number as of triage; if another ADR has claimed it by implementation time, use the next free number.) **Literal values — use verbatim (maintainer-supplied from the Hetzner Cloud console):** - routed /64 host address: `2a01:4f8:c012:43b::1/64` - gateway: `fe80::1` (Hetzner Cloud standard link-local gateway) - WAN interface: `enp1s0` (confirmed live during triage) These are opaque strings the dry-build cannot check for correctness — a wrong value passes `eval` and only surfaces post-deploy. Re-confirm the /64 and gateway against the Hetzner console before merge. **Acceptance criteria (agent's gate):** - [ ] web-arm declares static `2a01:4f8:c012:43b::1/64` on `enp1s0` and a default IPv6 route via `fe80::1` on `enp1s0` - [ ] Postfix pinned to IPv4 (`smtp_address_preference = ipv4`) in the same change - [ ] `docs/adr/0010-static-ipv6-on-web-arm.md` added with the static-vs-SLAAC/DHCPv6 + mail-pin rationale - [ ] web-arm dry-build passes (pre-commit hook) **Acceptance criteria (post-deploy — human-verified after merge, not part of the PR):** - [ ] `ip -6 route` on web-arm shows `default via fe80::1 dev enp1s0`; `curl -6` from the host reaches the v6 internet - [ ] nginx still listening on `:::80` and `:::443` (`ss -tlnp`) - [ ] outbound mail still leaves over IPv4 — `postconf smtp_address_preference` = `ipv4`, deliverability unchanged **Out of scope:** - Any DNS changes / AAAA records. Inbound IPv6 (publishing web-arm over v6) is #82 (pilot) and #83 (bulk rollout), which are manual Hetzner DNS / Cloudflare console changes — not this repo, not this issue. - Dual-stacking any production site or nginx vhost. - Any other host. - IPv4 addressing or the firewall port list. (Reviewer note, not a task: the existing `allowedTCPPorts = [ 22 80 443 ]` apply to IPv6 too once the address is up, so those three ports become reachable over v6 — same ports as v4, expected and acceptable.) - Explicitly setting `accept_ra` / `autoconf` / `tempAddress` — keep scope to address + route + mail-pin, matching the issue's own scoping, unless the dry-build or reviewer shows a stray SLAAC address would otherwise coexist with the static one.
Sign in to join this conversation.
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#81
No description provided.