Hermes Stopped Believing the Sender's Own dmarc=pass

You put Hermes on your inbox: the daily digest arrives, it reads, summarises and remembers. After an upgrade, nothing arrives at all — not even from senders on your allowlist. The only clue is one WARNING line in the log, naming a setting you have never configured.
The problem is the question “who actually sent this?”. Hermes checks the Authentication-Results header to establish sender identity, and that header is plain text a sender can write themselves. When your receiving server does not stamp its own version, an injected line sits on top and Hermes used to treat it as authoritative. The fix is blunt: no pin, no trust.
How the forgery worked
Authentication-Results is the header that vouches for “this message passed SPF / DKIM / DMARC”. Normally your receiving server adds it after the message arrives — precisely because the server has done the verification, its verdict is the one worth reading.
But it is a normal header. A sender can include their own line: attacker.self; dmarc=pass header.from=allowed.example. If you run a self-hosted MTA that does not stamp an authserv-id of its own (OpenDKIM / OpenDMARC style), that line stays on top. Hermes’ old behaviour read the topmost header regardless of who claimed to write it, so:
- the attacker forges
From:to match an address on your allowlist; - they add their own
dmarc=passline; - Hermes considers the sender authenticated and delivers.
The rewritten _verify_sender_authentication accepts two outcomes only — it reads the topmost Authentication-Results and requires its authserv-id to match yours exactly:
if not authserv_id: # without a pin the topmost header may be one the sender wrote
return False, _MISSING_AUTHSERV_REASON
if clauses[0].strip().lower() != authserv_id:
return False, _UNTRUSTED_AUTHSERV_REASON
Two neighbours were tightened as well: two dmarc results in one header now produce ambiguous dmarc result instead of a guess, and a dmarc=pass must have every header.from aligned with the From: domain Hermes parsed.
What actually changes after the upgrade
This is a behaviour change, so the blast radius matters:
- Who is affected: deployments with sender authentication on and no pin set. Sender auth is on by default, including open-access setups such as
EMAIL_ALLOW_ALL_USERS/GATEWAY_ALLOW_ALL_USERS. - What happens: every message needing an authenticated
From:is dropped, allowlisted senders included, until the operator does one of two things — setEMAIL_AUTHSERV_ID(or theplatforms.email.authserv_idconfig key), or explicitly opt out of sender authentication. - What you see: each unpinned account logs one WARNING when it connects, saying the authserv-id is not configured and that Hermes refuses to trust
Authentication-Results, plus both escape hatches (set the authserv-id, or setrequire_authenticated_sender: false/EMAIL_TRUST_FROM_HEADER=true). The warning also fires when an account’s first connect fails and the reconnect watcher brings it up later. - Who is unaffected: pinned deployments, and deployments that had already opted out. They behave exactly as before.
Finding the value to pin
Do not guess it. Open a message your server delivered, view the raw source (“Show original” in Gmail), find the topmost Authentication-Results header and take the token before the first semicolon.
- For Gmail that is
mx.google.com; - if the token changes between messages, it is a per-relay hostname. The right move then is to make your MTA stamp a stable authserv-id (OpenDKIM’s
AuthservID, the equivalent in OpenDMARC) and pin that.
Either configuration works; the environment variable is friendlier for containers:
# .env or container environment
EMAIL_AUTHSERV_ID=mx.google.com
# config.yaml
platforms:
email:
authserv_id: mx.google.com
# keep the default strict mode; only disable if you accept the forgery risk
require_authenticated_sender: true
The value is normalised to lowercase, so case does not matter, but the domain must match — do not widen it to a parent domain to save typing.
If you genuinely need mail flowing again today
Setting platforms.email.require_authenticated_sender: false (or EMAIL_TRUST_FROM_HEADER=true) makes Hermes accept unauthenticated senders again. That is a deliberate downgrade: on a self-hosted MTA it means anyone can forge an allowlisted sender. The better alternative is a stable authserv-id on your MTA — configure it once and stop thinking about it.
For other identity-forgery fixes in the same family, see the three sender-identity spoofing fixes; the rest of the email platform configuration lives in the Hermes command reference.
The short version
Authentication-Resultsis a header the sender can write; on a self-hosted MTA without a stamp it can sort to the top;- Hermes now requires an exact authserv-id match and drops mail needing an authenticated sender until you set one, logging a WARNING;
- copy the token before the first semicolon of the topmost
Authentication-Results(Gmail:mx.google.com) intoEMAIL_AUTHSERV_ID/platforms.email.authserv_id; - the opt-out is
require_authenticated_sender: falseorEMAIL_TRUST_FROM_HEADER=true, and it means accepting the forgery risk.