Skip to content

Commit

Permalink
Merge #2479
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
bors[bot] and nextgens committed Nov 9, 2022
2 parents cf6da14 + ec42241 commit 0839490
Show file tree
Hide file tree
Showing 11 changed files with 57 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Main features include:
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas
- **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

![Domains](docs/assets/screenshots/domains.png)
Expand Down
15 changes: 0 additions & 15 deletions core/admin/mailu/internal/views/postfix.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,21 +158,6 @@ def postfix_sender_rate(sender):
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.")

@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
def idna_encode(addresses):
return [
Expand Down
4 changes: 4 additions & 0 deletions core/admin/mailu/internal/views/rspamd.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ def rspamd_dkim_key(domain_name):
}
)
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())
1 change: 0 additions & 1 deletion core/postfix/conf/main.cf
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ check_ratelimit = check_sasl_access ${podop}senderrate

smtpd_client_restrictions =
permit_mynetworks,
check_sender_access ${podop}senderaccess,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_unknown_recipient_domain,
Expand Down
1 change: 0 additions & 1 deletion core/postfix/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def start_podop():
("mailbox", "url", url + "mailbox/§"),
("recipientmap", "url", url + "recipient/map/§"),
("sendermap", "url", url + "sender/map/§"),
("senderaccess", "url", url + "sender/access/§"),
("senderlogin", "url", url + "sender/login/§"),
("senderrate", "url", url + "sender/rate/§")
])
Expand Down
17 changes: 17 additions & 0 deletions core/rspamd/conf/force_actions.conf
Original file line number Diff line number Diff line change
@@ -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)";
}
}
11 changes: 11 additions & 0 deletions core/rspamd/conf/multimap.conf
Original file line number Diff line number Diff line change
@@ -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";
}
8 changes: 8 additions & 0 deletions core/rspamd/conf/whitelist.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rules {
BLACKLIST_ANTISPOOF = {
valid_dmarc = true;
blacklist = true;
domains = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/local_domains";
score = 0.0;
}
}
13 changes: 13 additions & 0 deletions core/rspamd/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import os
import glob
import logging as log
import requests
import sys
import time
from socrate import system, conf

log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
Expand All @@ -19,5 +21,16 @@
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)))

# 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
os.execv("/usr/sbin/rspamd", ["rspamd", "-i", "-f"])
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Main features include:
- **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts
- **Admin features**, global admins, announcements, per-domain delegation, quotas
- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF
- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner
- **Antispam**, auto-learn, greylisting, DMARC and SPF, anti-spoofing
- **Freedom**, all FOSS components, no tracker included

.. image:: assets/screenshots/create.png
Expand Down
1 change: 1 addition & 0 deletions towncrier/newsfragments/2475.feature
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 0839490

Please sign in to comment.