mirror of
https://github.com/Mailu/Mailu.git
synced 2025-01-26 03:52:50 +02:00
Merge #2479
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>
This commit is contained in:
commit
0839490beb
@ -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)
|
||||
|
@ -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 [
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
|
@ -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/§")
|
||||
])
|
||||
|
17
core/rspamd/conf/force_actions.conf
Normal file
17
core/rspamd/conf/force_actions.conf
Normal file
@ -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
core/rspamd/conf/multimap.conf
Normal file
11
core/rspamd/conf/multimap.conf
Normal file
@ -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
core/rspamd/conf/whitelist.conf
Normal file
8
core/rspamd/conf/whitelist.conf
Normal file
@ -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 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"))
|
||||
@ -19,5 +21,16 @@ if os.environ.get("ANTIVIRUS") == 'clamav':
|
||||
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"])
|
||||
|
@ -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
|
||||
|
1
towncrier/newsfragments/2475.feature
Normal file
1
towncrier/newsfragments/2475.feature
Normal file
@ -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…
x
Reference in New Issue
Block a user