diff --git a/README.md b/README.md index 0d8402f5..0fd737b6 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index f8346bb1..8188270c 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -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/") -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 [ diff --git a/core/admin/mailu/internal/views/rspamd.py b/core/admin/mailu/internal/views/rspamd.py index 458dbb81..b6ead86b 100644 --- a/core/admin/mailu/internal/views/rspamd.py +++ b/core/admin/mailu/internal/views/rspamd.py @@ -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()) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index a892430c..f3b789f9 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -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, diff --git a/core/postfix/start.py b/core/postfix/start.py index b12d0b54..509f961a 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -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/§") ]) diff --git a/core/rspamd/conf/force_actions.conf b/core/rspamd/conf/force_actions.conf new file mode 100644 index 00000000..9f803405 --- /dev/null +++ b/core/rspamd/conf/force_actions.conf @@ -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)"; + } +} diff --git a/core/rspamd/conf/multimap.conf b/core/rspamd/conf/multimap.conf new file mode 100644 index 00000000..dd25c08e --- /dev/null +++ b/core/rspamd/conf/multimap.conf @@ -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"; +} diff --git a/core/rspamd/conf/whitelist.conf b/core/rspamd/conf/whitelist.conf new file mode 100644 index 00000000..56f8a83d --- /dev/null +++ b/core/rspamd/conf/whitelist.conf @@ -0,0 +1,8 @@ +rules { + BLACKLIST_ANTISPOOF = { + valid_dmarc = true; + blacklist = true; + domains = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/local_domains"; + score = 0.0; + } +} diff --git a/core/rspamd/start.py b/core/rspamd/start.py index 58ec89ca..537d996d 100755 --- a/core/rspamd/start.py +++ b/core/rspamd/start.py @@ -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"]) diff --git a/docs/index.rst b/docs/index.rst index 4e4bb416..5c004dc1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 diff --git a/towncrier/newsfragments/2475.feature b/towncrier/newsfragments/2475.feature new file mode 100644 index 00000000..d5340380 --- /dev/null +++ b/towncrier/newsfragments/2475.feature @@ -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.