2479: Rework the anti-spoofing rule r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

We shouldn't assume that Mailu is the only MTA allowed to send emails on behalf of the domains it hosts.
We should also ensure that it's non-trivial for email-spoofing of hosted domains to happen

Previously we were preventing any spoofing of the envelope from; Now we are preventing spoofing of both the envelope from and the header from unless some form of authentication passes (is a RELAYHOST, SPF, DKIM, ARC)

### Related issue(s)
- close #2475

## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
main
bors[bot] 2 years ago committed by GitHub
commit 0839490beb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,7 +23,7 @@ Main features include:
- **User features**, aliases, auto-reply, auto-forward, fetched accounts - **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas - **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
- **Freedom**, all FOSS components, no tracker included - **Freedom**, all FOSS components, no tracker included
![Domains](docs/assets/screenshots/domains.png) ![Domains](docs/assets/screenshots/domains.png)

@ -158,21 +158,6 @@ def postfix_sender_rate(sender):
user = models.User.get(sender) or flask.abort(404) user = models.User.get(sender) or flask.abort(404)
return flask.abort(404) if user.sender_limiter.hit() else flask.jsonify("450 4.2.1 You are sending too many emails too fast.") return flask.abort(404) if user.sender_limiter.hit() else flask.jsonify("450 4.2.1 You are sending too many emails too fast.")
@internal.route("/postfix/sender/access/<path:sender>")
def postfix_sender_access(sender):
""" Simply reject any sender that pretends to be from a local domain
"""
if '@' in sender:
if sender.startswith('<') and sender.endswith('>'):
sender = sender[1:-1]
try:
localpart, domain_name = models.Email.resolve_domain(sender)
if models.Domain.query.get(domain_name):
return flask.jsonify("REJECT")
except sqlalchemy.exc.StatementError:
pass
return flask.abort(404)
# idna encode domain part of each address in list of addresses # idna encode domain part of each address in list of addresses
def idna_encode(addresses): def idna_encode(addresses):
return [ return [

@ -25,3 +25,7 @@ def rspamd_dkim_key(domain_name):
} }
) )
return flask.jsonify({'data': {'selectors': selectors}}) return flask.jsonify({'data': {'selectors': selectors}})
@internal.route("/rspamd/local_domains", methods=['GET'])
def rspamd_local_domains():
return '\n'.join(domain[0] for domain in models.Domain.query.with_entities(models.Domain.name).all() + models.Alternative.query.with_entities(models.Alternative.name).all())

@ -110,7 +110,6 @@ check_ratelimit = check_sasl_access ${podop}senderrate
smtpd_client_restrictions = smtpd_client_restrictions =
permit_mynetworks, permit_mynetworks,
check_sender_access ${podop}senderaccess,
reject_non_fqdn_sender, reject_non_fqdn_sender,
reject_unknown_sender_domain, reject_unknown_sender_domain,
reject_unknown_recipient_domain, reject_unknown_recipient_domain,

@ -27,7 +27,6 @@ def start_podop():
("mailbox", "url", url + "mailbox/§"), ("mailbox", "url", url + "mailbox/§"),
("recipientmap", "url", url + "recipient/map/§"), ("recipientmap", "url", url + "recipient/map/§"),
("sendermap", "url", url + "sender/map/§"), ("sendermap", "url", url + "sender/map/§"),
("senderaccess", "url", url + "sender/access/§"),
("senderlogin", "url", url + "sender/login/§"), ("senderlogin", "url", url + "sender/login/§"),
("senderrate", "url", url + "sender/rate/§") ("senderrate", "url", url + "sender/rate/§")
]) ])

@ -0,0 +1,17 @@
rules {
ANTISPOOF_NOAUTH {
action = "reject";
expression = "!MAILLIST & ((IS_LOCAL_DOMAIN_E & MISSING_FROM) | (IS_LOCAL_DOMAIN_H & (R_DKIM_NA & R_SPF_NA & DMARC_NA & ARC_NA)))";
message = "Rejected (anti-spoofing: noauth). Please setup DMARC with DKIM or SPF if you want to send emails from your domain from other servers.";
}
ANTISPOOF_DMARC_ENFORCE_LOCAL {
action = "reject";
expression = "!MAILLIST & (IS_LOCAL_DOMAIN_H | IS_LOCAL_DOMAIN_E) & (DMARC_POLICY_SOFTFAIL | DMARC_POLICY_REJECT | DMARC_POLICY_QUARANTINE | DMARC_NA)";
message = "Rejected (anti-spoofing: DMARC compliance is enforced for local domains, regardless of the policy setting)";
}
ANTISPOOF_AUTH_FAILED {
action = "reject";
expression = "!MAILLIST & BLACKLIST_ANTISPOOF";
message = "Rejected (anti-spoofing: auth-failed)";
}
}

@ -0,0 +1,11 @@
IS_LOCAL_DOMAIN_H {
type = "selector"
selector = "from('mime'):domain";
map = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/local_domains";
}
IS_LOCAL_DOMAIN_E {
type = "selector"
selector = "from('smtp'):domain";
map = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/local_domains";
}

@ -0,0 +1,8 @@
rules {
BLACKLIST_ANTISPOOF = {
valid_dmarc = true;
blacklist = true;
domains = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/local_domains";
score = 0.0;
}
}

@ -3,7 +3,9 @@
import os import os
import glob import glob
import logging as log import logging as log
import requests
import sys import sys
import time
from socrate import system, conf from socrate import system, conf
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
@ -19,5 +21,16 @@ if os.environ.get("ANTIVIRUS") == 'clamav':
for rspamd_file in glob.glob("/conf/*"): for rspamd_file in glob.glob("/conf/*"):
conf.jinja(rspamd_file, os.environ, os.path.join("/etc/rspamd/local.d", os.path.basename(rspamd_file))) conf.jinja(rspamd_file, os.environ, os.path.join("/etc/rspamd/local.d", os.path.basename(rspamd_file)))
# Admin may not be up just yet
healthcheck = f'http://{os.environ["ADMIN_ADDRESS"]}/internal/rspamd/local_domains'
while True:
time.sleep(1)
try:
if requests.get(healthcheck,timeout=2).ok:
break
except:
pass
log.warning("Admin is not up just yet, retrying in 1 second")
# Run rspamd # Run rspamd
os.execv("/usr/sbin/rspamd", ["rspamd", "-i", "-f"]) os.execv("/usr/sbin/rspamd", ["rspamd", "-i", "-f"])

@ -28,8 +28,8 @@ Main features include:
- **Web access**, multiple Webmails and administration interface - **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts - **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas - **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF - **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
- **Freedom**, all FOSS components, no tracker included - **Freedom**, all FOSS components, no tracker included
.. image:: assets/screenshots/create.png .. image:: assets/screenshots/create.png

@ -0,0 +1 @@
Upgrade the anti-spoofing rule. We shouldn't assume that Mailu is the only MTA allowed to send emails on behalf of the domains it hosts... but we should also ensure that both the envelope from and header from are checked.
Loading…
Cancel
Save