From 662741e7adb92e33ccc913ce5487e45c8836c976 Mon Sep 17 00:00:00 2001 From: MajorLinux Date: Sat, 6 Jun 2026 10:38:04 -0400 Subject: [PATCH] troubleshooting: Postfix header_checks can't act on milter-added headers Document the majormail spam-routing failure (2026-06-06): a cleanup header_checks REDIRECT keyed on the milter-added X-Spam-Flag never fired for real inbound mail (only locally-injected), so spam kept reaching the inbox. Fix is to route in Sieve at delivery (after the milter), with a redirect + loop guard. Includes the 'local-injection tests lie' warning. --- 05-troubleshooting/index.md | 1 + ...postfix-header-checks-vs-milter-headers.md | 72 +++++++++++++++++++ SUMMARY.md | 1 + 3 files changed, 74 insertions(+) create mode 100644 05-troubleshooting/networking/postfix-header-checks-vs-milter-headers.md diff --git a/05-troubleshooting/index.md b/05-troubleshooting/index.md index 970f13a..41e16a2 100644 --- a/05-troubleshooting/index.md +++ b/05-troubleshooting/index.md @@ -15,6 +15,7 @@ Practical fixes for common Linux, networking, and application problems. - [Mail Client Stops Receiving: Fail2ban IMAP Self-Ban](networking/fail2ban-imap-self-ban-mail-client.md) - [firewalld: Mail Ports Wiped After Reload](networking/firewalld-mail-ports-reset.md) - [Dovecot IMAP Clients Fail to Sync: vsz_limit OOM from a Bloated Index Log](networking/dovecot-imap-oom-vsz-limit-bloated-index.md) +- [Postfix header_checks Can't Act on Milter-Added Headers (Use Sieve)](networking/postfix-header-checks-vs-milter-headers.md) - [Tailscale SSH: Unexpected Re-Authentication Prompt](networking/tailscale-ssh-reauth-prompt.md) - [iOS Tailscale Clients Report HostName="localhost" — Breaks /etc/hosts Generators](networking/tailscale-status-json-hostname-localhost-ios.md) - [rsync over Tailscale: Hung in TCP Teardown After Transfer Completes](networking/rsync-tailscale-teardown-stall.md) diff --git a/05-troubleshooting/networking/postfix-header-checks-vs-milter-headers.md b/05-troubleshooting/networking/postfix-header-checks-vs-milter-headers.md new file mode 100644 index 0000000..b9e2a6d --- /dev/null +++ b/05-troubleshooting/networking/postfix-header-checks-vs-milter-headers.md @@ -0,0 +1,72 @@ +--- +title: "Postfix header_checks Can't Act on Milter-Added Headers (Use Sieve)" +domain: troubleshooting +category: networking +tags: [postfix, milter, header_checks, spamassassin, spamass-milter, dovecot, sieve, spam] +status: published +created: 2026-06-06 +updated: 2026-06-06 +--- +# Postfix header_checks Can't Act on Milter-Added Headers (Use Sieve) + +A Postfix `header_checks` rule that keys on a header added by a **milter** (e.g. `X-Spam-Flag: YES` from `spamass-milter`/`rspamd`/`opendkim`) appears correct, is wired up, and even fires for test mail — yet silently does nothing for real inbound mail. The cause: `header_checks` run in the `cleanup` daemon and **do not reliably see headers a milter adds**, so a rule like: + +``` +/^X-Spam-Flag:[[:space:]]+YES/ REDIRECT junk@example.com +``` + +never matches genuine inbound spam, even though the delivered message clearly contains `X-Spam-Flag: YES`. + +> Hit on **majormail** (2026-06-06): spam-routing REDIRECT had been dead since it was deployed — spam kept reaching the inbox. + +## Why + +Milter header modifications and `header_checks` happen at different stages of `cleanup`, and `header_checks` evaluate the message as received from the network, **before** the milter's header additions are folded in. So for an `smtpd_milter`-tagged message, the flag header is not visible to `header_checks` at the time they run. + +Confusingly, **locally-injected test mail can fire the rule** (timing/origin differences) — so a quick `swaks`/`smtplib` test to `localhost:25` "passes" while real inbound mail silently slips through. Don't trust a local-injection test for this; verify against real inbound mail (or with the method below). + +## How to confirm + +```bash +# A delivered message that SHOULD have matched — but wasn't acted on: +grep -iE '^(X-Spam-Flag|Delivered-To|Subject):' /var/vmail///.Junk/cur/ +# X-Spam-Flag: YES Delivered-To: @… (i.e. NOT redirected) + +# Is the spam scanner an smtpd milter? (then header_checks can't see its headers) +postconf smtpd_milters +# smtpd_milters = … unix:/run/spamass-milter/spamass-milter.sock + +# maillog: the header_checks REDIRECT never appears for real inbound spam, +# only (if at all) for locally-submitted mail ("redirect: … from local"). +grep -i 'redirect:' /var/log/maillog +``` + +## Fix — act at delivery time, in Sieve + +Dovecot **Sieve** runs at LMTP delivery, *after* the milter, so it reliably sees milter-added headers. Do the routing there instead of in `header_checks`. To keep spam out of the real mailbox entirely (so a push client like Spark never sees it), `redirect` to a dedicated account rather than `fileinto Junk`: + +```sieve +require ["envelope"]; +if header :contains "X-Spam-Flag" "YES" { + # Loop guard: a global before-script also runs for junk@'s own delivery. + if envelope :is "to" "junk@example.com" { + keep; + stop; + } + redirect "junk@example.com"; + stop; +} +``` + +On majormail this is the global before-script `roles/majormail/templates/spam-to-junk.sieve.j2` (MajorAnsible `07dab90`); `redirect` cancels the implicit keep so the real mailbox stays clean (INBOX *and* Junk). Verify deterministically with `sieve-test -u -r