Block email forwarding spam with Rspamd
At Onlime GmbH we have a mail infrastructure that consists of 3 mail servers: mx1
acts as primary MX server and provides SMTP as outgoing mail gateway for our customers. mx2
acts as secondary MX and fallback incoming mailserver. Incoming email from mx2
is forwarded to mx1
which does spam/antivirus filtering with Rspamd. Finally, the 3rd mailserver which is simply called mail
acts as IMAP server and outgoing mailserver.
We allow customers to set up email forwardings on their domains. A forwarding address could have another customer email address as destination, but could also directly or indirectly (through another forwarding) point to an external email address. Spam filtering is done on mx1
, but Rspamd actually just flags the email as ham/spam with a spam score and adds the X-Spamd-Result
header with all symbols. The actual action is done on the final mailserver mail
in the recipients mailbox via Sieve rule. The reason for this is that we want to let the customer define his own spam score (going from "minimal" to "radical" which maps to a spam score treshold) and what should happen with an email that got classified as spam (store it in Spam
box or discard it directly).
So, how can we block an incoming spam email that would get forwarded to some external address, without introducing any Backscattering? We need to block incoming email already radically on the first/incoming mail server mx1
if it will be forwarded to some external email address.
Mail Infrastructure Recap
If you didn't quite get the description of our mailserver infrastructure from my intro above, I try to explain in some more visual way:
So, again, Rspamd on mx1
just flags the messages with a spam score, but the actual blocking of spam is done on mail
through a Sieve rule in the final mailbox (on Dovecot IMAP server). If we would block an outgoing forwarding spam mail there, that would result in backscattering, as the email was not rejected with a 5xx
status code on the first mailserver mx1
. How can we block it already on mx1
without introducing any added complexity in our mail infrastructure? See the problem?
The Idea
Proposed solution:
- Deploy a list of forwarding emails (that point directly or indirectly to some external address) to a Rspamd map
forwardings.inc.local
- Use Rspamd mutimap module to tag all emails with
rcpt
(map type) inforwardings.inc.local
with symbolIS_FORWARDING
- Use Rspamd force actions to override default action with
reject
if theIS_FORWARDING
symbol is set andX-Spam: Yes
header was set
Like this, we would reject directly on mx1/mx2/mailman as frontend mailservers, not generating any bounces / backscattering!
Rspamd force action
That solution was quite easy to implement. The only tricky part was to generate a list of forwarding email addresses that pointed directly or indirectly (potentially recursively over multiple forwardings) to some external address. This is your job on application level. I put this list into Rspamd's local.d/maps.d/forwardings.inc.local
and keep it updated through our controlpanel, deploying it via an agent/microservice on the frontend mailservers. Whenever a customer creates a new forwarding that points to some external email address, this list is regenerated and deployed to the frontend mailservers mx1/mx2/mailman
. It contains one address per line:
forwarding1@example.com
forwarding2@example.com
...
Rspamd multimap module now adds symbol IS_FORWARDING
(which should be unterstood as "is forwarding to some external recipient") if the recipient was found in forwardings.inc.local
:
IS_FORWARDING {
type = "rcpt";
map = "/etc/rspamd/local.d/maps.d/forwardings.inc.local";
}
In case you were wondering: No, it is not required to restart/reload Rspamd when this map content changes!
Finally, we configure the Rspamd force actions module in force_actions.conf
as follows:
rules {
SPAMMY_FORWARDING {
action = "reject";
expression = "IS_FORWARDING";
# require_action setting defines actions that will be overridden
require_action = ["add header"];
# default message: "Spam message rejected"
#message = "This message cannot be sent because it looks like spam."
}
}
You could also override the default message "Spam message rejected" there, see my comment.
Further explanation: The
X-Spam: Yes
header is set by Rspamd if the calculated spam score was above the defaultadd_header = 6.0
action configuration. Above force action only comes into play, if that header was set AND theIS_FORWARDING
symol is there. And because we didn't define any score forIS_FORWARDING
symbol, it will just be0.00
, not influencing the whole spam score calculation. Think of this symbol just as a flag, not as any spam/ham symbol.
That's the whole trick!