Home / Alt manpages / header_checks(5)

  • header_checks(5)
  • File format
  • linux

Build a Safe Postfix header_checks Rule and Test It

One badly anchored regex in header_checks can reject every message on the server, so test it before you reload. This guide builds a small Postfix header_checks rule that rejects a matching header, gives you a repeatable test for the pattern using postmap, and leaves you with a clear rollback path. The examples use Postfix 3.8.6, installed here as the postfix package. Allow about 20 minutes if you already administer the server, plus time to send a controlled test message.

You need a shell account that can read Postfix configuration. Editing main.cf, the rules file and reloading Postfix all need elevated privileges; the postmap checks below are ordinary commands once the temporary file is readable. This changes mail policy, so do it in a maintenance window and keep a configuration backup.

1. Confirm the installation and supported map type

Check the version and whether this Postfix build supports PCRE tables first. These commands only read local configuration:

$ postconf mail_version
3.8.6
$ postconf -m | grep -x pcre
pcre

If pcre is absent, use a supported type such as regexp and adapt the pattern using regexp_table(5). Do not put pcre: into main.cf until this command has confirmed it is available.

Checkpoint

Record the version and the map type you will use. This guide continues with PCRE.

2. Create a narrow rule

Back up the existing files before touching them. This is an elevated, state-changing step:

# cp -p /etc/postfix/main.cf /etc/postfix/main.cf.before-header-checks
# cp -p /etc/postfix/header_checks.pcre /etc/postfix/header_checks.pcre.before-header-checks 2>/dev/null || true

Now edit /etc/postfix/header_checks.pcre and add this rule:

/^X-Example-Demo:[[:space:]]*block[[:space:]]*$/i    REJECT 5.7.1 Demonstration header rejected
  • Both ends are anchored. The pattern accepts whitespace around the value but will not reject every message that merely contains the word block.
  • The i flag is case-insensitive. It covers both the header name and its value.
  • Order matters. Rules run in file order, and the first matching action wins for the current line.
  • Keep rules separated. Leave a blank line or a comment between them: a line that starts with whitespace is treated as a continuation of the previous rule, not a new one.
  • Narrow before broad. Put specific rules ahead of general ones so a catch-all does not shadow a more useful action.

3. Point Postfix at the table

Inspect the current setting before replacing it:

$ postconf header_checks
header_checks =

Set the table with an elevated command. If your server already shows a value here, preserve it and edit the referenced table instead of overwriting it blindly:

# postconf -e 'header_checks = pcre:/etc/postfix/header_checks.pcre'

This changes main.cf, but running Postfix does not pick it up yet. Check the rendered setting:

$ postconf -h header_checks
pcre:/etc/postfix/header_checks.pcre

4. Test the pattern without sending mail

Use postmap to query a PCRE table directly. A matching lookup returns the action text; a non-matching lookup returns nothing and a successful status. This first query is a read-only test:

$ postmap -q 'X-Example-Demo: block' pcre:/etc/postfix/header_checks.pcre
REJECT 5.7.1 Demonstration header rejected
$ postmap -q 'X-Example-Demo: permit' pcre:/etc/postfix/header_checks.pcre
$ printf 'postmap status: %s\n' "$?"
postmap status: 0

Query the exact form that will reach the cleanup service. Header checks see one logical header at a time, including a folded header as one value; body checks are different, they see one physical body line at a time.

You can test several inputs from a file without touching the table:

$ printf '%s\n' 'X-Example-Demo: block' 'X-Example-Demo: permit' > /tmp/header-checks-input
$ postmap -q - pcre:/etc/postfix/header_checks.pcre < /tmp/header-checks-input
REJECT 5.7.1 Demonstration header rejected

The first result corresponds to the matching input; the second produces nothing. Remove the temporary input once you are done:

$ rm -f /tmp/header-checks-input

5. Validate and reload carefully

Ask Postfix to validate its configuration before disturbing a running service:

# postfix check
$ printf 'configuration status: %s\n' "$?"
configuration status: 0

No output and status 0 means the validation passed. It does not prove your intended messages match the rule, which is exactly why the postmap checks above matter.

Warning

Reloading changes the policy used by new mail. It can reject messages for every recipient, and a DISCARD or HOLD action has broader consequences than this example. Reload only after you have checked the pattern and the action:

# postfix reload
$ postconf -h header_checks
pcre:/etc/postfix/header_checks.pcre

Send a test message carrying the demonstration header from a controlled account, then check the Postfix log. Do not use a real customer message as the first test. Expect a rejection for the matching header and normal delivery for a message without it.

6. Understand the boundaries

header_checks applies to initial message headers, except MIME-related headers handled by mime_header_checks. The MIME and nested-header settings default to $header_checks, while header_checks itself defaults to empty. Headers added by the cleanup daemon, such as From:, To:, Message-ID: and Date:, are excluded from inspection: do not assume a rule can see every header a later delivery step displays.

These checks do not decode attachments or unzip archives. Encoded bodies and encoded non-ASCII headers must be matched in their encoded form, so a rule that looks correct in a mail client can miss the actual input Postfix sees.

Prefer WARN while developing a new pattern if rejection would be risky: it logs and moves on to the next input line. Once real log results look right, swap in the intended action and repeat the validation and postmap tests.

7. Undo the example

If the rule causes an unwanted rejection, restore the saved files and reload. This is an elevated rollback:

# cp -p /etc/postfix/main.cf.before-header-checks /etc/postfix/main.cf
# cp -p /etc/postfix/header_checks.pcre.before-header-checks /etc/postfix/header_checks.pcre
# postfix check
# postfix reload

Recovery

If no earlier rules file existed, remove only the demonstration rule and leave the rest intact; do not delete the whole table to fix one bad pattern. If messages were held or discarded by a different action, that recovery is a separate queue-management task, so check the relevant Postfix queue before removing anything.

Done means

  • Version and map type confirmed. Postfix 3.8.6 supports PCRE.
  • Rule is narrow. It is anchored and tested against both matching and non-matching input.
  • Table wired up. postconf -h header_checks points at the intended table.
  • Config validated. postfix check returned status 0 before reload.
  • Boundaries understood. You know the MIME, folding and encoded-content limits.
  • Rollback ready. You kept a backup and know how to restore it.