SPF Settings
SPF Settings
Admin path: Content Checks > SPF Settings
(view_spf_settings.cfm, inc/get_spf_settings.cfm,
inc/spf_save_settings.cfm, inc/spf_generate_config_file.cfm,
inc/spf_add_whitelist.cfm, inc/spf_edit_whitelist.cfm,
inc/spf_delete_whitelist.cfm,
inc/generate_postfix_configuration.cfm).
This page controls inbound SPF policy enforcement. SPF
(RFC 7208) lets the
owner of a domain publish, in DNS, the list of IP addresses authorized
to send mail using that domain in the envelope MAIL FROM (and
optionally the SMTP HELO). When Postfix accepts a connection,
Hermes consults the published record for the connecting client and
decides whether to accept, defer, or reject the message based on the
result.
Hermes is responsible only for the verification side. Publishing
your own organization's SPF record (the v=spf1 ... TXT record at
your sending domain) is a one-time DNS operation done at your
authoritative DNS host — it is not managed from this page.
Where SPF sits in the flow
+----------------------+
| Remote SMTP peer |
+----------+-----------+
|
v
+----------+--------------------------------+
| smtpd :25 (hermes_postfix_dkim) |
| smtpd_recipient_restrictions = ..., |
| check_policy_service unix:private/ |
| policy-spf |
| | |
| v |
| Postfix spawns policyd-spf (python) |
| from master.cf "policy-spf unix" entry |
| - reads /etc/postfix-policyd-spf-python/ |
| policyd-spf.conf |
| - queries DNS for the sender's SPF TXT |
| - returns Pass / Fail / Softfail / |
| Neutral / None / TempError / PermError |
| - returns Postfix action verb |
| (DUNNO / REJECT / DEFER_IF_REJECT) |
+----------+--------------------------------+
|
v
+----------+--------------------------------+
| OpenDKIM milter :8891 (DKIM verify) |
| OpenDMARC milter :54321 (DMARC eval) |
+----------+--------------------------------+
|
v
Amavis / SpamAssassin / ClamAV
The policy daemon is a Postfix policy delegate — a separate
process that Postfix spawns from master.cf:
policy-spf unix - n n - - spawn
user=nobody argv=/usr/bin/policyd-spf
smtpd_recipient_restrictions invokes it via
check_policy_service unix:private/policy-spf. The daemon's
configuration file at
/etc/postfix-policyd-spf-python/policyd-spf.conf is what this admin
page writes; the entire file is regenerated on every save from the
template at /opt/hermes/templates/policyd-spf.conf.HERMES.
SPF result classes and their typical meaning
| Result | Meaning | Default Hermes behavior |
|---|---|---|
Pass |
Connecting IP is in the published v=spf1 record |
Accept |
Fail |
Sender has published -all; this IP is explicitly disallowed |
Reject |
SoftFail |
Sender has published ~all; this IP is not authorized but the owner is in monitoring mode |
Reject (Hermes recommended) — see Operational consequence below |
Neutral |
Sender published ?all; owner expresses no opinion |
Accept (treated as None) |
None |
No SPF record exists for the sender | Accept |
TempError |
DNS timeout / SERVFAIL during the lookup | Accept (treat as no record) — operator can switch to defer |
PermError |
SPF record is malformed or exceeds the 10-DNS-lookup limit | Accept (treat as no record) — operator can switch to reject |
SPF is checked twice per message by the daemon: once against the
SMTP HELO identity (before MAIL FROM), and once against the
envelope sender domain after MAIL FROM. Each check has its own
rejection policy on this page.
The two cards on the page
1. SPF Settings (master toggle + policy daemon controls)
The master SPF Enabled dropdown flips a single child row in the
parameters table — the row whose parameter value is
check_policy_service unix:private/policy-spf under the
smtpd_recipient_restrictions parent. When SPF is disabled the page
also forces DMARC off (DMARC requires both an SPF and a DKIM result;
without SPF the DMARC milter has nothing to align against). The
in-page callout warns about this dependency.
When SPF is enabled, the policy section exposes six controls, each
written to a parameters2 row in the dkim/spf module rows:
| Control | policyd-spf.conf directive |
Effect |
|---|---|---|
| Logging Level | debugLevel |
0–4 verbosity; -1 disables logging. Higher levels log every DNS lookup and the full SMTP envelope data — useful for diagnosing federal / M365 GOV / Proofpoint Government chain issues |
| Test Mode | TestOnly |
1 adds the SPF result to message headers but never rejects, regardless of the rejection policies below. Use to evaluate impact before enforcing |
| HELO Check Rejection Policy | HELO_reject |
What to do with the SPF result for the SMTP HELO/EHLO identity. Options: Fail, SPF_Not_Pass (Reject All), Softfail (Recommended), Null (reject HELO of null-sender bounces only), False (header only), No_Check |
| Mail From Check Rejection Policy | Mail_From_reject |
Same option set, but applied to the envelope MAIL FROM domain |
| Permanent Error Policy | PermError_reject |
True rejects when the published SPF record is broken; False (recommended) treats it as no record |
| Temporary Error Policy | TempError_Defer |
True issues a 4xx defer on DNS timeout; False (recommended) accepts and continues |
Operational consequence — single point of SPF truth. The Hermes baseline disables SpamAssassin's redundant SPF re-check. SA's in-process SPF scoring runs after Amavis has reinjected the message over a local hop, so SA sees an IP path that does not include the original sender — on government/M365 GOV/Proofpoint Government mail the wrong IP gets scored, producing false-positive
SPF_SOFTFAILhits. The policy daemon on this page is the single authoritative SPF verifier; it sees the real connecting client IP. To preserve the spam-coverage SA'sSPF_SOFTFAILrule provided, set both HELO and Mail From Check Rejection Policy to Reject SoftFail. This is the in-page recommendation and the shipped baseline.
2. SPF Whitelist Entries
Per-row bypass list written to four Whitelist directives in
policyd-spf.conf:
| Entry type | policyd-spf.conf directive |
What it matches | Typical use |
|---|---|---|---|
| IP / Network Address | Whitelist |
The connecting client IP (single address or CIDR) | Trusted secondary MX, known forwarders, partner relays |
| HELO/EHLO Host Name | HELO_Whitelist |
The hostname announced in HELO/EHLO. Daemon DNS-checks the connecting IP against an A/AAAA for that name to prevent forgery |
Mailing-list providers that consistently HELO with their own domain |
| Domain Name | Domain_Whitelist |
The envelope MAIL FROM domain |
Senders with broken ~all records whose mail you still need to receive |
| PTR Domain | Domain_Whitelist_PTR |
The reverse-DNS (PTR) domain of the connecting IP | Hosts whose forward DNS is unstable but whose reverse DNS is well-controlled |
Entries are stored in the spf_bypass table
(entry, entry_type, entry_note). The save handler joins all
enabled rows of each type with commas and substitutes them into the
template at IP-NETWORK-WHITELIST, HELO-WHITELIST,
DOMAIN-WHITELIST, PTR-WHITELIST placeholders.
A whitelist hit completely skips SPF evaluation for that
connection — the daemon returns Pass without consulting DNS. Use
IP-based whitelisting when possible; HELO / Domain / PTR entries
incur extra DNS lookups per message.
The DataTable supports add (textarea — one entry per line, validated and deduplicated), inline edit modal, single delete, and bulk delete via checkbox selection.
What this page does NOT control
- Per-sender allow/block. Address-level rules live on Sender/Recipient Rules and apply later in the pipeline.
- The SPF record for your own sending domain. That is a DNS TXT
record you publish at your authoritative DNS host. A correct
outbound SPF for a Hermes-served sending domain typically looks
like
v=spf1 mx ip4:<hermes-egress-ip> include:<isp-relay> ~all— see Domains (Email Relay) and Domains (Email Server) for the egress IP your record needs to authorize. - Network-level allow. Trusted SMTP source ranges (
mynetworks) short-circuit before any policy check via Relay Networks.
Save flow
1. Validate form fields exist when SPF is being enabled
- Missing fields -> session.m = 20, redirect, no DB write
2. UPDATE parameters child row for SPF on/off
3. UPDATE parameters2 rows for the six policy daemon directives
4. cfinclude spf_generate_config_file.cfm
a. Read /opt/hermes/templates/policyd-spf.conf.HERMES
b. REReplace placeholders (DEBUG-LEVEL, TEST-ONLY, HELO-REJECT,
MAIL-FROM-REJECT, PERMERROR-REJECT, TEMPERROR-REJECT)
c. SELECT all enabled spf_bypass rows by entry_type, comma-join,
substitute *-WHITELIST placeholders
d. Backup current /etc/postfix-policyd-spf-python/policyd-spf.conf
as policyd-spf.conf.HERMES
e. Move generated tmp file into place
5. cfinclude generate_postfix_configuration.cfm
- Regenerates main.cf so smtpd_recipient_restrictions reflects
SPF on/off
- Reloads Postfix inside hermes_postfix_dkim
6. If SPF was DISABLED: also disable the OpenDMARC milter rows,
clear FailureReports, deactivate the DMARC report Ofelia job,
regenerate opendmarc.conf, restart OpenDMARC
7. session.m = 9 -> green "SPF settings saved" alert on redirect
Files and containers touched
| Path | Owner | Role |
|---|---|---|
config/hermes/var/www/html/admin/2/view_spf_settings.cfm |
hermes_commandbox |
The page |
config/hermes/var/www/html/admin/2/inc/get_spf_settings.cfm |
hermes_commandbox |
Loads current parameters / parameters2 / spf_bypass values |
config/hermes/var/www/html/admin/2/inc/spf_save_settings.cfm |
hermes_commandbox |
Validates form, updates rows, calls config + Postfix regen; disables DMARC if SPF off |
config/hermes/var/www/html/admin/2/inc/spf_generate_config_file.cfm |
hermes_commandbox |
Renders policyd-spf.conf from the template + DB |
config/hermes/opt/hermes/templates/policyd-spf.conf.HERMES |
hermes_commandbox (read) → hermes_postfix_dkim (live /etc/postfix-policyd-spf-python/policyd-spf.conf) |
Canonical template with DEBUG-LEVEL, TEST-ONLY, etc. placeholders |
parameters table (check_policy_service unix:private/policy-spf row) |
hermes_db_server (hermes DB) |
SPF on/off |
parameters2 table (rows where module='spf') |
hermes_db_server (hermes DB) |
The six daemon settings |
spf_bypass table |
hermes_db_server (hermes DB) |
Whitelist entries |
hermes_postfix_dkim container |
— | Runs smtpd, spawns policyd-spf, hosts the live policyd-spf.conf |
hermes_unbound container |
— | Resolves every SPF DNS query the daemon makes |
Failure semantics
| Failure | Behavior |
|---|---|
| Missing form fields when enabling SPF | session.m = 20, redirect, no DB write |
spf_generate_config_file.cfm throws (template missing, write fails, etc.) |
Surfaces as a cfcatch from the inline include — the save aborts |
| Empty whitelist entry on Add | session.m = 13, redirect, no DB write |
| Whitelist entry fails IP / hostname syntax check | session.m = 17, redirect, no DB write |
| Duplicate whitelist entry | session.m = 14, redirect, no DB write |
postfix reload fails inside the container |
Standard generate_postfix_configuration.cfm failure path |
Related
- DKIM Settings — the second authentication service whose result is consumed by DMARC; paired conceptually with SPF as a "DNS-based outbound sender authentication" mechanism
- DMARC Settings — the policy layer that consumes SPF and DKIM results; disabling SPF here automatically disables DMARC
- ARC Settings — chain-of-custody for authentication results across forwarders; participates only after SPF / DKIM / DMARC have produced their verdicts
- Trusted ARC Sealers (M365) — for M365 customers whose downstream verifiers escalate when SPF fails on forwarded mail
- Perimeter Checks — the rest of the
smtpd_recipient_restrictionschain; the SPF / DKIM / DMARC status badges on its fourth card link back to the dedicated pages - Sender/Recipient Rules — per-address bypass applied after the SPF verdict
- DNS Resolver — every SPF lookup
flows through
hermes_unbound; resolver mode (recursive vs. forwarding through a public provider) directly affects SPF reliability and the 10-DNS-lookup limit timing - Domains (Email Relay), Domains (Email Server) — where the egress IP that your authoritative SPF record needs to authorize is documented