[TUTORIAL] Sender domain reputation in PMG with SpamAssassin AskDNS (and greylisting to catch new domains)

Jul 1, 2022
34
22
28
Saarbrücken
www.bsnsoft.de

Why this post​

A while ago I posted how to plug rspamd into PMG as a custom check (tutorial). This one is simpler: it only uses what PMG already ships, SpamAssassin's AskDNS plugin, and adds a reputation score for the domain in the From: header.

Full disclosure: the list is my own project. I run it with my company (BSNSoft Solutions GmbH) at dnsbl.bsnsoft-hostings.de. It is free for your own mail operations: 100 queries per hour per resolver IP without an account, 1,000 per hour (5,000 per day) with a free registration. I'm posting it here because PMG had a few quirks that cost me an evening, and the greylisting trick in step 3 works with other domain lists too.

What it does​

The zone scores domains, not IPs, from 0 (throwaway) to 9 (established company), based on what the domain itself shows: website, legal notice, certificate, DNS and mail setup, history. The answer is graded as 127.0.0.(9 − score), so 127.0.0.0 is the best and 127.0.0.9 the worst. A domain the zone has never seen gets NXDOMAIN and is scored in the background within seconds.

The rules below give good domains a small bonus only if the mail carries a valid DKIM signature of the From domain, so nobody profits from forging a known brand. Penalties for low and bad domains always apply. PMG's default rule set quarantines from score 3, so the weights are scaled to that: a bad sender domain adds +1.5, together with a bad link +1.8. The list alone never sends a mail to quarantine.

1. Check your resolver​

This was my evening. Like every DNSBL, the zone answers with 127.0.0.x. Router and firewall resolvers with DNS rebinding protection (Unbound on pfSense and OPNsense, dnsmasq with stop-dns-rebind) drop those answers: you get NOERROR without an address, and every DNSBL rule silently never fires. Check on PMG:

Code:
getent hosts gmail.com.dnsbl.bsnsoft-hostings.de

You should see 127.0.0.0. If nothing comes back, give PMG its own resolver. That's a good idea for any DNSBL anyway, Spamhaus and others block large public resolvers:

Code:
apt install unbound
# /etc/resolv.conf: nameserver 127.0.0.1
# PMG as LXC container: on the PVE host run  pct set <CTID> --nameserver 127.0.0.1
# With DHCP, dhclient overwrites /etc/resolv.conf. Add to /etc/dhcp/dhclient.conf:
#   supersede domain-name-servers 127.0.0.1;

2. The rules​

Append to /etc/mail/spamassassin/custom.cf. PMG keeps that file across updates and syncs it from the master to all cluster nodes, so edit it on the master only.

Code:
# BSNSoft Domain Reputation for Proxmox Mail Gateway, version 2026-11-13
# Sender domain (From: header). Bonus only with a valid DKIM signature of that
# domain; penalties always apply. Weights fit PMG's default rule, which
# quarantines from score 3: the sender penalty stays at half of it (+1.5).
ifplugin Mail::SpamAssassin::Plugin::AskDNS

askdns   __BSN_REP_TOP  _AUTHORDOMAIN_.dnsbl.bsnsoft-hostings.de A /^127\.0\.0\.0$/
askdns   __BSN_REP_HIGH _AUTHORDOMAIN_.dnsbl.bsnsoft-hostings.de A /^127\.0\.0\.[12]$/

meta     BSN_REP_TOP    __BSN_REP_TOP && DKIM_VALID_AU
describe BSN_REP_TOP    Sender domain: top reputation, DKIM-authenticated (BSNSoft)
tflags   BSN_REP_TOP    net nice
score    BSN_REP_TOP    -0.5

meta     BSN_REP_HIGH   __BSN_REP_HIGH && DKIM_VALID_AU
describe BSN_REP_HIGH   Sender domain: high reputation, DKIM-authenticated (BSNSoft)
tflags   BSN_REP_HIGH   net nice
score    BSN_REP_HIGH   -0.1

meta     BSN_REP_NOAUTH (__BSN_REP_TOP || __BSN_REP_HIGH) && !DKIM_VALID_AU
describe BSN_REP_NOAUTH Sender domain: good reputation, From not DKIM-authenticated: no bonus (BSNSoft)
tflags   BSN_REP_NOAUTH net
score    BSN_REP_NOAUTH 0.001

askdns   BSN_REP_MEDIUM _AUTHORDOMAIN_.dnsbl.bsnsoft-hostings.de A /^127\.0\.0\.[345]$/
describe BSN_REP_MEDIUM Sender domain: medium reputation (BSNSoft)
tflags   BSN_REP_MEDIUM net
score    BSN_REP_MEDIUM 0.25

askdns   BSN_REP_LOW    _AUTHORDOMAIN_.dnsbl.bsnsoft-hostings.de A /^127\.0\.0\.[67]$/
describe BSN_REP_LOW    Sender domain: low reputation (BSNSoft)
tflags   BSN_REP_LOW    net
score    BSN_REP_LOW    1.0

askdns   BSN_REP_BAD    _AUTHORDOMAIN_.dnsbl.bsnsoft-hostings.de A /^127\.0\.0\.[89]$/
describe BSN_REP_BAD    Sender domain: bad reputation (BSNSoft)
tflags   BSN_REP_BAD    net
score    BSN_REP_BAD    1.5

endif

# Optional: domains of links in the body, BAD range only, small weight.
ifplugin Mail::SpamAssassin::Plugin::URIDNSBL

urirhssub BSN_URI_BAD uri.dnsbl.bsnsoft-hostings.de. A 127.0.0.8-127.0.0.9
body      BSN_URI_BAD eval:check_uridnsbl('BSN_URI_BAD')
describe  BSN_URI_BAD URI domain: bad reputation (BSNSoft)
tflags    BSN_URI_BAD net domains_only
score     BSN_URI_BAD 0.3

endif

Then check and load:

Code:
spamassassin --siteconfigpath=/etc/mail/spamassassin --lint
systemctl restart pmg-smtp-filter

pmgconfig sync does not reload the rules, the filter needs the restart. You can tune the weights later under Configuration → Spam Detector → Custom Scores.

3. Optional: let greylisting buy the time​

SpamAssassin only asks the zone after the mail has been accepted. So the very first mail from a brand-new throwaway domain gets no score, and that's exactly the mail you want to catch. PMG greylists unknown IPv4 senders by default and lets the retry in after three minutes at the earliest. If Postfix asks the zone already during the first, greylisted attempt, the domain is scored when the retry arrives:

Code:
cp /var/lib/pmg/templates/main.cf.in /etc/pmg/templates/main.cf.in
# in smtpd_sender_restrictions, right after permit_mynetworks, add:
        warn_if_reject reject_rhsbl_sender dnsbl.bsnsoft-hostings.de=127.0.0.[8..9]
pmgconfig sync --restart 1

The line rejects nothing. It triggers the lookup and logs a reject_warning for bad sender domains. In my test the first mail from a new domain got no label without the line, and with it the retry got BSN_REP_BAD. Two caveats: it looks up the envelope sender, which for newsletters is the bounce domain of the mailing service, and PMG doesn't update a custom template on upgrades, so diff it against /var/lib/pmg/templates/main.cf.in afterwards.

Verify​

Code:
spamassassin --siteconfigpath=/etc/mail/spamassassin -t < mail.eml | grep BSN_

Or in the tracking center / journalctl -u pmg-smtp-filter:

Code:
SA score=0/5 ... hits=BSN_REP_TOP(-0.5),DKIM_SIGNED(0.1),DKIM_VALID(-0.1),DKIM_VALID_AU(-0.1),...

Two things I learned on the way​

  • PMG cuts mails above Max Spam Size (default 256 KiB) before SpamAssassin sees them. The DKIM signature of such a mail then fails, so DKIM_VALID_AU and every DKIM-based bonus never hit on large mails. That's not specific to this list.
  • SpamAssassin 4 skips AskDNS lookups for domains in uridnsbl_skip_domains (gmail.com, t-online.de and others). Those get no rule from this list at all.

If you run rspamd as custom check​

There is an rspamd plugin as well (link). With the custom-check setup from my older tutorial, set DEFER_UNKNOWN = false at the top of the plugin: PMG only takes rspamd's score, so a soft reject would get lost. Steps 1 and 3 apply there too.

Tested with PMG 9.1 (SpamAssassin 4.0.2). The full guide is at https://dnsbl.bsnsoft-hostings.de/integration#pmg.

Feedback welcome, especially from cluster setups or if you see false positives.