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.
This commit is contained in:
parent
d8f07e8e2e
commit
662741e7ad
3 changed files with 74 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/<dom>/<user>/.Junk/cur/<msg>
|
||||
# X-Spam-Flag: YES Delivered-To: <user>@… (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 <user> -r <recipient> <script> <msg.eml>` — it prints the resulting actions.
|
||||
|
||||
## Key Notes
|
||||
|
||||
- **Don't use `header_checks` for milter-added headers.** Options that *do* see them: Sieve at delivery (simplest), or running the scanner as a re-injecting `content_filter` (the re-injected message has the flag as a real header). spamass-milter cannot rewrite the envelope recipient itself.
|
||||
- **`redirect` re-injects via the MTA** — if a *global* before-script does the redirect, it also runs for the destination mailbox's delivery; guard with an `envelope :is "to"` check or you get a mail loop.
|
||||
- **Local-injection tests lie here.** A `localhost:25` test may fire a header_checks rule that real inbound mail never triggers.
|
||||
|
||||
## Related
|
||||
|
||||
- [Mail Client Stops Receiving: Fail2ban IMAP Self-Ban](fail2ban-imap-self-ban-mail-client.md)
|
||||
- [Dovecot IMAP Clients Fail to Sync: vsz_limit OOM from a Bloated Index Log](dovecot-imap-oom-vsz-limit-bloated-index.md)
|
||||
|
|
@ -81,6 +81,7 @@ updated: 2026-05-15T09:00
|
|||
* [Mail Client Stops Receiving: Fail2ban IMAP Self-Ban](05-troubleshooting/networking/fail2ban-imap-self-ban-mail-client.md)
|
||||
* [firewalld: Mail Ports Wiped After Reload](05-troubleshooting/networking/firewalld-mail-ports-reset.md)
|
||||
* [Dovecot IMAP Clients Fail to Sync: vsz_limit OOM from a Bloated Index Log](05-troubleshooting/networking/dovecot-imap-oom-vsz-limit-bloated-index.md)
|
||||
* [Postfix header_checks Can't Act on Milter-Added Headers (Use Sieve)](05-troubleshooting/networking/postfix-header-checks-vs-milter-headers.md)
|
||||
* [Tailscale SSH: Unexpected Re-Authentication Prompt](05-troubleshooting/networking/tailscale-ssh-reauth-prompt.md)
|
||||
* [ssh.socket Unreachable After Reboot (Tailscale Race Condition)](05-troubleshooting/networking/ssh-socket-tailscale-race-condition.md)
|
||||
* [Fail2ban & UFW Rule Bloat Cleanup](05-troubleshooting/networking/fail2ban-ufw-rule-bloat-cleanup.md)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue