Build a Safe Postfix header_checks Rule and Test It
You will finish with a small Postfix header_checks rule that rejects a matching header, a repeatable test for the pattern, and 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.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need a shell account that can read Postfix configuration. Editing main.cf, the rules file and reloading Postfix requires elevated privileges. The postmap checks below are ordinary commands when the temporary file is readable. This guide changes mail policy, so do it during a maintenance window and keep an existing configuration backup.
1. Confirm the installation and supported map type
First confirm the version and whether this Postfix build supports PCRE tables. 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 the first command has confirmed it.
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 changing them. This is an elevated, state-changing operation:
# 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-Dixon-Demo:[[:space:]]*block[[:space:]]*$/i REJECT 5.7.1 Demonstration header rejected
The pattern anchors both ends of the logical header. It accepts whitespace around the value but does not reject every message containing the word block. The i flag makes the header name and value case-insensitive. Rules are tested in file order, and the first matching action applies to the current input line.
Leave a blank line or a comment between rules. A rule starts with non-whitespace text; leading whitespace would make it a continuation of the previous logical line. Put broad rules after narrow rules so a general match does not hide 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 the output from your server shows an existing value, preserve it and edit the referenced table instead of blindly replacing it:
# postconf -e 'header_checks = pcre:/etc/postfix/header_checks.pcre'
This changes main.cf but does not yet make running Postfix use the new value. 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. A matching lookup returns the action text; a non-matching lookup returns no text and a successful status. The first query is an ordinary read-only test:
$ postmap -q 'X-Dixon-Demo: block' pcre:/etc/postfix/header_checks.pcre
REJECT 5.7.1 Demonstration header rejected
$ postmap -q 'X-Dixon-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 modifying the table:
$ printf '%s\n' 'X-Dixon-Demo: block' 'X-Dixon-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 input produces no result. Remove the temporary input after testing:
$ 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 this validation passed. It does not prove that your intended messages match the rule, which is why the earlier postmap checks 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 checking the pattern and the action:
# postfix reload
$ postconf -h header_checks
pcre:/etc/postfix/header_checks.pcre
Send a test message with the demonstration header from a controlled account, then inspect the Postfix log. Do not use a real customer message as the first test. A rejection is expected for the matching header; a normal message without it should continue through the usual delivery path.
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 displayed by a later delivery step.
These checks do not decode attachments or unzip archives. Encoded bodies and encoded non-ASCII headers must be matched in their encoded form. A rule that looks correct in a mail client can therefore miss the actual input seen by Postfix.
Prefer WARN while developing a new pattern if rejection would be risky. It logs a warning and inspects the next input line. After reviewing real log results, replace it with 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
If no earlier rules file existed, remove only the demonstration rule and leave other rules intact. Do not delete the whole table to fix one bad pattern. If messages were put on hold or discarded by a different action, recovery is a separate queue-management task; check the relevant Postfix queue before removing anything.
Done means
- You confirmed Postfix 3.8.6 and a supported PCRE map type.
- The rule is anchored, narrow and tested against both matching and non-matching input.
postconf -h header_checkspoints at the intended table.postfix checkreturned status 0 before reload.- You understand MIME, folding and encoded-content boundaries.
- You kept a rollback copy and know how to restore it.