feat: rewrite forwarded mail sender to pass SPF checks - #298
feat: rewrite forwarded mail sender to pass SPF checks#298DavidePrincipi wants to merge 3 commits into
Conversation
eb606be to
f8c0952
Compare
| # | ||
| sender_canonical_maps = socketmap:unix:/var/lib/postsrsd/srs.sock:forward | ||
| sender_canonical_classes = envelope_sender | ||
| recipient_canonical_maps = socketmap:unix:/var/lib/postsrsd/srs.sock:reverse |
There was a problem hiding this comment.
Nice work -- the outbound side looks right, and the test covers it.
I have a doubt about the return path.
Bounces come back to SRS0=...@<first mail domain>. But recipient_canonical_maps runs in cleanup, while smtpd accepts or refuses the recipient at RCPT TO -- before that.
The SRS domain is in virtual_alias_domains, and smtpd_reject_unlisted_recipient defaults to yes. I could not find anything in virtual_alias_maps that matches an SRS0= local part.
On a running instance, two alias domains, no catchall:
# swaks --server 127.0.0.1 --helo test.local --from postmaster@example.net \
--to 'SRS0=abcd=TT=inbound.test=u1@example.com' --quit-after RCPT
<** 550 5.1.1 <SRS0=abcd=TT=inbound.test=u1@example.com>: Recipient address
rejected: User unknown in virtual alias table
No PR applied there, but the PR does not touch smtpd_recipient_restrictions, virtual_alias_maps or smtpd_reject_unlisted_recipient. Am I missing a path where the reverse map fires earlier?
If I'm right, DSNs and vacation replies from the forward destination get refused at our edge. Today a failed forward bounces to the original sender. After this change it would fail silently.
Would a generated regexp: entry in virtual_alias_maps be enough, just to satisfy the unlisted check? The image has regexp, not pcre.
Happy to be wrong. A robot test doing a RCPT TO on an SRS address would settle it.
There was a problem hiding this comment.
Right, nice catch! there's a missing piece of Postfix configuration.
| ldap_schema=${POSTFIX_LDAP_SCHEMA:?} | ||
|
|
||
| start_postsrsd() { | ||
| postsrsd -D -C /etc/postsrsd/postsrsd.conf -p /run/postsrsd.pid || : |
There was a problem hiding this comment.
Separate from the above: start_postsrsd ends with || :, and postsrsd.cf is already in main.cf by then.
If postsrsd fails to start, or dies later with nothing supervising it, sender_canonical_maps points at a missing socket and cleanup defers every message -- not just forwards.
With syslog = off and postsrsd launched from a transient podman exec, I don't think anything would show up in journald either.
Would failing the reload be safer?
There was a problem hiding this comment.
Yes this is ugly. I'd rebase this PR on Alpine 3.24, which provides a modern version of postsrsd supporting reload and inotify.
Forwarded mail keeps the original external sender in the envelope, so destination servers reject it on SPF grounds. Wire Postfix's sender/recipient canonical maps to postsrsd's socketmap so forwards are rewritten to pass SPF at the receiving end, and add a check_recipient_access pipemap rule so a genuine SRS0 bounce is verified against postsrsd's own secret and accepted at RCPT TO, instead of being rejected as an unlisted recipient before it ever reaches that rewrite. postsrsd has no configuration reload capability of its own, so reload-config manages its whole lifecycle directly: it starts it (in its own daemon mode) once a mail domain is configured, stops it when SRS is disabled, and kills and restarts it whenever the domains list actually changes, waiting out the old process first. The container itself now relies on podman's --init for signal handling and reaping, so entrypoint.sh just execs Postfix in the foreground as before this change. SRS can be turned off with POSTFIX_SRS=0. The signing secret is bootstrapped once into a new postsrsd/ subdir of the existing postfix-queue volume, so no new volume is needed; it is intentionally left out of the module backup, since restore-time regeneration only risks losing bounce notifications for a narrow window around the restore. Assisted-by: Claude:claude-sonnet-5
f8c0952 to
608adf2
Compare
Extend the existing external-forward test case with a check that the envelope sender was rewritten to a postsrsd SRS0 address, using postfix/qmgr's own journal as ground truth like the sibling delivery/relay assertions already do. Add a dedicated suite covering the reverse path: a genuine SRS0 recipient (its signature verified against postsrsd's own secret) is accepted and routed back to the real original sender, while an address that only looks like one is still rejected as unlisted. Assisted-by: Claude:claude-sonnet-5
Expose whether postsrsd (NethServer/dev#7741) is currently active, mirroring reload-config's own POSTFIX_SRS parsing. Assisted-by: Claude:claude-sonnet-5
608adf2 to
fabf7cb
Compare
Summary
When a mailbox forwards a message to an external address, the message still shows the original sender in its envelope. Many mail servers reject or discard it, because the forwarding server is not allowed to send mail for the original sender's domain (SPF check).
This change adds postsrsd to the Postfix container:
check_recipient_accesscombined withpipemap), then decoded back to the real original sender — otherwise it would be silently rejected as an unknown recipient before ever reaching the reverse rewrite.reload-configowns its whole lifecycle: it starts postsrsd once a mail domain is configured, stops it when SRS is disabled, and restarts it whenever the domains list changes. The container now relies on podman's--initfor signal handling and process reaping.POSTFIX_SRS=0, and its current state is reported throughget-facts.Related issue
NethServer/dev#7741
How to test
SRS0=...address), instead of the original sender.SRS0=...address (simulating a bounce) and check it is accepted, not rejected as an unknown recipient.POSTFIX_SRS=0in the module'sstate/environmentfile, reload Postfix, and check the rewrite no longer happens.Automated coverage:
tests/50__smtp/00__userforwards.robot,tests/50__smtp/00__srs_reverse.robot.🤖 Generated with Claude Code