Block email forwarding spam with Rspamd

April 16th, 2021 by Philip Iezzi 4 min read
cover image

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:

2021-mail-infra-overview

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) in forwardings.inc.local with symbol IS_FORWARDING
  • Use Rspamd force actions to override default action with reject if the IS_FORWARDING symbol is set and X-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:

local.d/maps.d/forwardings.inc.local
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:

local.d/multimap.conf
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:

local.d/force_actions.conf
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 default add_header = 6.0 action configuration. Above force action only comes into play, if that header was set AND the IS_FORWARDING symol is there. And because we didn't define any score for IS_FORWARDING symbol, it will just be 0.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!