wip: mail hardening: stalwart PROXY /32 + MTA-STS/TLS-RPT #679
Draft
lytedev
wants to merge 1 commit from
sec-mail into main
pull from: sec-mail
merge into: lytedev:main
lytedev:main
lytedev:propose/collie-voice
lytedev:propose/education-account
lytedev:propose/element-x-personal-releases
lytedev:propose/module-directory-scanner
lytedev:propose/special-args-cleanup
lytedev:propose/limine-fallback
lytedev:propose/household-kiosk
lytedev:propose/xwayland-ei-display-from-client
lytedev:propose/steamdeck-mouse-steps-aside-for-games
lytedev:propose/assistant-watch
lytedev:propose/lpcamm-watch
lytedev:propose/agents-where-information-lives
lytedev:propose/tuwunel-1.9.2
lytedev:propose/pin-flake-registry
lytedev:propose/headscale-0-29-3
lytedev:propose/canary-freshness-per-repo
lytedev:propose/boot-guard-efivars-fix
lytedev:propose/deck-dock-panel-under-niri
lytedev:propose/admindevice-ssh-anything
lytedev:propose/kdeconnect-announce
lytedev:steamdeck-mgba
lytedev:propose/doorbell-no-internet
lytedev:propose/ha-list-tasks
lytedev:propose/ha-music-not-found
lytedev:propose/bc250-40cu-measured
lytedev:propose/multiboot-foxtrot-dragon
lytedev:propose/pitft-stmpe-drive
lytedev:propose/foxtrot-multiboot-prep
lytedev:propose/pitft-no-smartd
lytedev:propose/kexec-luks-reboot
lytedev:propose/foxtrot-multiboot
lytedev:propose/model-eval
lytedev:propose/foxtrot-vm-seats
lytedev:iamb-sender
lytedev:propose/calendar-alarms
lytedev:pinephone-niri
lytedev:push-tsntxvxynzps
lytedev:propose/spoof-reject-cleanups
lytedev:propose/finance-pipeline
lytedev:steamframe-plan
lytedev:pinephone-buildable
lytedev:deck-pose-wip
lytedev:aerc-open-html
lytedev:local-auth-unification
lytedev:iamb-dotfiles
lytedev:esphome-music-panel
lytedev:wifi-system-profile
lytedev:gauge-history
lytedev:validate-gate-fix2
lytedev:validate-slot-no-hugepages
lytedev:pinephone-reenable
lytedev:cyberdeck-research
lytedev:git-anon-ratelimit
lytedev:headscale-basedomain
lytedev:ssd-fast-tier
lytedev:hold-impermanence-flip
lytedev:gate-dummy-eval
lytedev:zen-browser
lytedev:initrd-remote-unlock
lytedev:beefcake-rpool-backup
lytedev:sec-sso-plan
lytedev:flab-lid-wake
lytedev:hearth-k8s
lytedev:zulip
lytedev:router-guest-vlan
lytedev:headscale-extra-records
lytedev:beefcake-advertise-lan
lytedev:push-qrvuuqmxtznv
lytedev:voxtype-per-host-model
lytedev:cleanup
lytedev:push-kanidm-opt-in
lytedev:push-voxtype-flake-fix
lytedev:push-uvqqknxvqlvy
lytedev:push-qrqsxkmlrevert
lytedev:fix-kanidm-uid-attr-map
lytedev:nix-on-droid
lytedev:self-hosted-dns
lytedev:fix-runner-tmpfs-revert
lytedev:beefcake-email
lytedev:beefcake-immich
lytedev:firefox-nightly-devedition
lytedev:fix-bun-baseline
lytedev:flab-stuff
lytedev:printer-sftp
lytedev:dragon-vr-monado
lytedev:clever-vr-game-clamshell-mode-detection
lytedev:wireguard-mesh
lytedev:rescue-beefcake-unpushed-nix
No reviewers
Labels
No labels
bug
enhancement
question
scary
upstream
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
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
lytedev/nix!679
Loading…
Reference in a new issue
No description provided.
Delete branch "sec-mail"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Two inbound-mail hardening changes on beefcake's stalwart / the
lyte.devzone. Not deployed — build-checked only, for review + deploy by you.packages/hosts/beefcake/stalwart.nix)lib/modules/nixos/dns-zones.nix,packages/hosts/beefcake/caddy.nix,packages/hosts/beefcake.nix)Build-checked:
nix build .#nixosConfigurations.beefcake.config.system.build.toplevelsucceeds. The caddyadapt/fmtvalidation and the zone render both run as part of that build and pass. The rendered policy body and TXT records were inspected in the store (below).1. Stalwart PROXY trust:
100.64.0.0/10→100.64.0.15/32overrideProxyTrustedNetworkspreviously trusted the entire tailnet CGNAT range (100.64.0.0/10) to send PROXY-protocol headers on inbound:25. A PROXY header claims an arbitrary client IP, so anything in that range that reached:25could forge the sender IP and defeat SPF/DNSBL/reputation. The only host that legitimately sends PROXY headers is pebble, the MX front:Narrowed to pebble's tailnet /32. Your three questions:
(a) Is pebble's tailnet IP stable? Yes. headscale assigns node IPs persistently — a node keeps its address across reboots/reconnects; it only changes if the node is deleted and re-registered. Confirmed current value
100.64.0.15two ways:ssh root@pebble 'tailscale ip -4'→100.64.0.15headscale nodes list→pebble … 100.64.0.15, fd7a:115c:a1e0::fThe code comment calls out that if headscale ever does reassign it, this /32 must be updated in lockstep or PROXY-passed mail gets rejected as untrusted.
(b) Is a secret required instead? No — there's nothing to configure. PROXY protocol has no authentication or shared secret (HAProxy PROXY protocol spec — "The receiver … may choose to accept the connection … based on the sender's address"; there is no signature/token field). Trust is purely by source IP. So the correct control is a tight source-IP allowlist: the
/32here + the headscale ACL restricting who can even open:25. There is no stronger knob available at this layer.(c) Why was this rated LOW? Because the headscale ACL already restricts
:25to pebble (src: [pebble], dst: [beefcake:25,2526]inheadscale-acl.json), the broad/10trust was only reachable by a host that is both (i) an already-compromised tailnet node and (ii) permitted by the ACL to reach:25— i.e. it required a prior compromise to exploit at all. The/32removes that residual defense-in-depth gap so the two controls agree, rather than fixing an open hole. Hence LOW, not higher.2. MTA-STS + TLS-RPT (M-low)
Today inbound mail TLS is opportunistic STARTTLS with no published policy and no reporting: an on-path attacker can strip the STARTTLS advertisement (downgrade to cleartext) and we'd neither prevent nor detect it.
dig TXT _mta-sts.lyte.dev/dig TXT _smtp._tls.lyte.devboth currently return empty.What these are:
_mta-sts) and fetched over HTTPS (so the policy itself can't be forged by a DNS attacker — it's cert-validated).What this PR adds:
_mta-sts.lyte.devTXT →v=STSv1; id=20260701000000(theidis an opaque version tag; bump it whenever the policy file body changes so senders re-fetch).mta-sts.lyte.devserving the policy athttps://mta-sts.lyte.dev/.well-known/mta-sts.txt(auto-provisioned cert, same asfiles.lyte.dev). Rendered body (verified in the built,caddy fmt'd Caddyfile — no leading whitespace):_smtp._tls.lyte.devTXT →v=TLSRPTv1; rua=mailto:postmaster@lyte.dev.mta-stsadded to beefcake'slyte.dns-updater.recordsso the policy host resolves to this box.Recommendation — start in
testing, thenenforce. I setmode: testingdeliberately: in testing mode senders evaluate the policy and report failures via TLS-RPT but do not reject mail on a TLS failure. So we get visibility with zero delivery risk. Once TLS-RPT reports come in clean for a while (pebble's cert forpebble.lyte.devis valid and matches the MX), flip the Caddyfile tomode: enforceand bump theidindns-zones.nixin the same change so senders pick up the new policy.Deploy notes (for when you deploy)
--hostname 192.168.0.9), not the VPN.mta-sts.lyte.devA record is published by the dns-updater run, and caddy provisions the cert on first hit — the well-known URL won't serve valid TLS until both happen.🤖 Generated with Claude Code
Two inbound-mail hardening changes on beefcake's stalwart / lyte.dev: - Narrow overrideProxyTrustedNetworks from 100.64.0.0/10 (the whole tailnet CGNAT range) to pebble's tailnet /32 (100.64.0.15). Only pebble, the MX front, ever sends PROXY v2 headers (MX -> pebble HAProxy -> beefcake:25). PROXY protocol has no auth; source-IP trust is the only control, so a /32 plus the existing headscale ACL (src: [pebble] -> beefcake:25) are the layered controls. Removes the residual where a compromised tailnet node that could also reach :25 could forge the client IP and defeat SPF/DNSBL/reputation. - Add MTA-STS + TLS-RPT for lyte.dev inbound mail (previously opportunistic STARTTLS only, strippable by an on-path attacker with no policy or reporting): - _mta-sts.lyte.dev TXT (v=STSv1) + a caddy vhost mta-sts.lyte.dev serving the policy at /.well-known/mta-sts.txt (mode: testing, mx: pebble.lyte.dev, max_age 7d). testing is report-only; flip to enforce once TLS-RPT is clean. - _smtp._tls.lyte.dev TXT (v=TLSRPTv1; rua=mailto:postmaster@lyte.dev) for aggregate TLS-failure reports. - mta-sts added to the beefcake dns-updater dynamic records so the policy host resolves here. Build-checked (nixosConfigurations.beefcake toplevel); caddy adapt + zone render validated. Not deployed.mail hardening: stalwart PROXY /32 + MTA-STS/TLS-RPTto wip: mail hardening: stalwart PROXY /32 + MTA-STS/TLS-RPTView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.