Setting DMARC filter in Spamassassin

October 28, 2020 Roberto Puzzanghera5 comments

  • Thanks to Iulian for the hint. This is a link to his page
  • Take a look here for further DMARC solutions for qmail
  • MXtoolbox: verifying your DMARC record
  • RFC 4789 Domain-based Message Authentication, Reporting, and Conformance (DMARC)

You can use Spamassassin to apply a DMARC filter by means of the AskDNS plugin. Just add the following to your local.cf:

ifplugin Mail::SpamAssassin::Plugin::AskDNS
askdns __DMARC_POLICY_NONE _dmarc._AUTHORDOMAIN_ TXT /^v=DMARC1;.*\bp=none;/
askdns __DMARC_POLICY_QUAR _dmarc._AUTHORDOMAIN_ TXT /^v=DMARC1;.*\bp=quarantine;/
askdns __DMARC_POLICY_REJECT _dmarc._AUTHORDOMAIN_ TXT /^v=DMARC1;.*\bp=reject;/

meta DMARC_REJECT !(DKIM_VALID_AU || SPF_PASS) && __DMARC_POLICY_REJECT
score DMARC_REJECT 10
meta DMARC_QUAR !(DKIM_VALID_AU || SPF_PASS) && __DMARC_POLICY_QUAR
score DMARC_QUAR 5
meta DMARC_NONE !(DKIM_VALID_AU || SPF_PASS) && __DMARC_POLICY_NONE
score DMARC_NONE 0.1
endif # Mail::SpamAssassin::Plugin::AskDNS

This means that a DMARC reject (p=reject in the DNS record) will turn into a +10 spam score, DMARC quarantine (p=quarantine) into a +5 spam score and a p=none into a +0.1 spam score.

This is how you may want to set your own DMARC record into your bind zone:

_dmarc.yourdomain.tld. IN TXT "v=DMARC1;p=reject;sp=none;pct=100;rua=mailto:postmaster@yourdomain.tld"

Of course this requires that you already have both SPF and DKIM working as explained before.

If you decide to set a similar DNS record in your DMZ view, it is important that you have set your allowed localnets in spamassassin, for example:

internal_networks 10.0.0/24

otherwise you will probably ban your system or web application mail messages in case you don't sign them.

Comments

Incorrect rejections

I implemented these rules a couple of weeks ago. I've been noticing some legitimate mail in my spam box, which led me to investigate.
According to the specifications of the relevant standards, it's allowed to implement SPF and DMARC, but not DKIM.

However, your example rule gives these types of emails a score of 10:

meta DMARC_REJECT !(DKIM_VALID_AU && SPF_PASS) && __DMARC_POLICY_REJECT
score DMARC_REJECT 10

To allow mails with only SPF and DMARC to be delivered, I think the expression should be like this instead:

meta DMARC_REJECT !((!DKIM_SIGNED || DKIM_VALID_AU) && SPF_PASS) && __DMARC_POLICY_REJECT

Reply | Permalink

Incorrect rejections

I had a look to RFC7489#section-6.6.2 and it actually suggests that the DMARC test should pass if <<"one or more" of the Authenticated Identifiers align with the From domain>> so I'm going to accept your observation.

Perhaps it's even more correct to leave things as originally suggested by Iulian

meta DMARC_REJECT !(DKIM_VALID_AU || SPF_PASS) && __DMARC_POLICY_REJECT

so that the emails will be accepted if at least one between DKIM and SPF pass.

Reply | Permalink

Incorrect rejections

I think your suggestion is correct. Let me check the rfc in detail before correcting the rule.

Your rule should also prevent that the email will be rejected when for any reason the dkim record was not retrieved. There is a discussion on Iulian's blog (the guy who suggested that rule) on the purpose...

Reply | Permalink

Invalid syntax

meta DMARC_REJECT !(DKIM_VALID_AU || SPF_PASS) && __DMARC_POLICY_REJECT

this check is invalid, this -OR- logic in the () reads:

if not (DKIM_VALID_AU -OR- SPF_PASS) AND theres a policy for the domain then reject which means an email with assuming a policy exists (1):

!DKIM_VALID_AU and !SPF_PASS == if !(0 || 0) && 1 == 1 && 1 == 1 == ACTION (GOOD)
DKIM_VALID_AU and !SPF_PASS == if !(1 || 0) && 1 == 0 && 1 == 0 == NO ACTION (BAD)
!DKIM_VALID_AU and SPF_PASS == if !(0 || 1) && 1 == 0 && 1 == 0 == NO ACTION (BAD)
DKIM_VALID_AU and SPF_PASS == if !(1 || 1) && 1 == 0 && 1 == 0 == NO ACTION (GOOD)

if no policy exists (0) we are always NO ACTION (GOOD)

Basically its not failing out and runing the domain's policy for a failure of individual parts.

Test with a quick perl script (play with the 3 variables up top):

#!/usr/bin/perl
$DKIM_VALID_AU = 1;
$SPF_PASS = 1;
$POLICY = 1;

$PRES = int( !($DKIM_VALID_AU || $SPF_PASS) );
$RES = int ( !($DKIM_VALID_AU || $SPF_PASS) && $POLICY );

print "DKIM_VALID_AU=$DKIM_VALID_AU SPF_PASS=$SPF_PASS PAREN RESULT=$PRES RESULT=$RES AKA: ";

if ($RES) {
print "ACTION\n";
} else {
print "NO ACTION\n";
}
POLICY=1 DKIM_VALID_AU=0 SPF_PASS=0 PAREN RESULT=1 RESULT=1 AKA: ACTION
POLICY=1 DKIM_VALID_AU=0 SPF_PASS=1 PAREN RESULT=0 RESULT=0 AKA: NO ACTION
POLICY=1 DKIM_VALID_AU=1 SPF_PASS=0 PAREN RESULT=0 RESULT=0 AKA: NO ACTION
POLICY=1 DKIM_VALID_AU=1 SPF_PASS=1 PAREN RESULT=0 RESULT=0 AKA: NO ACTION

The following will actually work (tested for all cases):

meta DMARC_REJECT !(DKIM_VALID_AU && SPF_PASS) && __DMARC_POLICY_REJECT

if not (DKIM_VALID_AU -AND- SPF_PASS) AND theres a policy for the domain then REJECT

Validate by changing that perl script logic to match:

#!/usr/bin/perl
$DKIM_VALID_AU = 1;
$SPF_PASS = 1;
$POLICY = 1;

$PRES = int( !($DKIM_VALID_AU && $SPF_PASS) );
$RES = int ( !($DKIM_VALID_AU && $SPF_PASS) && $POLICY );

print "DKIM_VALID_AU=$DKIM_VALID_AU SPF_PASS=$SPF_PASS PAREN RESULT=$PRES RESULT=$RES AKA: ";

if ($RES) {
print "ACTION\n";
} else {
print "NO ACTION\n";
}
POLICY=1 DKIM_VALID_AU=1 SPF_PASS=1 PAREN RESULT=0 RESULT=0 AKA: NO ACTION
POLICY=1 DKIM_VALID_AU=0 SPF_PASS=1 PAREN RESULT=1 RESULT=1 AKA: ACTION
POLICY=1 DKIM_VALID_AU=1 SPF_PASS=0 PAREN RESULT=1 RESULT=1 AKA: ACTION
POLICY=1 DKIM_VALID_AU=0 SPF_PASS=0 PAREN RESULT=1 RESULT=1 AKA: ACTION

POLICY=0 DKIM_VALID_AU=1 SPF_PASS=1 PAREN RESULT=0 RESULT=0 AKA: NO ACTION
POLICY=0 DKIM_VALID_AU=0 SPF_PASS=0 PAREN RESULT=1 RESULT=0 AKA: NO ACTION

If any of the conditions are 0 (fail) then the policy is enforced. If everything checks out its ignored. No policy means no action.

Thanks so much for the information about how to set the DMARC check up via AskDNS. Hopefully this correction helps make this method even better.

Reply | Permalink

Invalid syntax

Thank you, fixed

Reply | Permalink