From f0f873ffe7f176d480dc9a80cb68242f1fe8a69d Mon Sep 17 00:00:00 2001 From: lub Date: Tue, 1 Sep 2020 21:48:09 +0200 Subject: [PATCH 01/14] add option to enforce inbound starttls --- core/admin/mailu/configuration.py | 1 + core/admin/mailu/internal/nginx.py | 30 ++++++++++++++++++++++++------ docs/configuration.rst | 7 +++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 66b0b832..7fcd1ee7 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -31,6 +31,7 @@ DEFAULT_CONFIG = { 'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io', 'POSTMASTER': 'postmaster', 'TLS_FLAVOR': 'cert', + 'INBOUND_TLS_ENFORCE': False, 'AUTH_RATELIMIT': '10/minute;1000/hour', 'AUTH_RATELIMIT_SUBNET': True, 'DISABLE_STATISTICS': False, diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index fa127584..a7771f91 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -17,6 +17,9 @@ STATUSES = { "smtp": "535 5.7.8", "pop3": "-ERR Authentication failed" }), + "encryption": ("Must issue a STARTTLS command first", { + "smtp": "530 5.7.0" + }), } @@ -28,12 +31,27 @@ def handle_authentication(headers): protocol = headers["Auth-Protocol"] # Incoming mail, no authentication if method == "none" and protocol == "smtp": - server, port = get_server(headers["Auth-Protocol"], False) - return { - "Auth-Status": "OK", - "Auth-Server": server, - "Auth-Port": port - } + server, port = get_server(protocol, False) + if app.config["INBOUND_TLS_ENFORCE"]: + if "Auth-SSl" in headers and headers["Auth-SSL"] == "on": + return { + "Auth-Status": "OK", + "Auth-Server": server, + "Auth-Port": port + } + else: + status, code = get_status(protocol, "encryption") + return { + "Auth-Status": status, + "Auth-Error-Code" : code, + "Auth-Wait": 0 + } + else: + return { + "Auth-Status": "OK", + "Auth-Server": server, + "Auth-Port": port + } # Authenticated user elif method == "plain": server, port = get_server(headers["Auth-Protocol"], True) diff --git a/docs/configuration.rst b/docs/configuration.rst index 4b211925..3438b7fe 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -73,6 +73,13 @@ By default postfix uses "opportunistic TLS" for outbound mail. This can be chang by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt``. This setting is highly recommended if you are a relayhost that supports TLS. +Similarily by default nginx uses "opportunistic TLS" for inbound mail. This can be changed +by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidden for +internet facing hosts according to e.g. `RFC 3207`_ , because this prevents MTAs without STARTTLS +support or e.g. mismatching TLS versions to deliver emails to Mailu. + +.. _`RFC 3207`: https://tools.ietf.org/html/rfc3207 + The ``FETCHMAIL_DELAY`` is a delay (in seconds) for the fetchmail service to go and fetch new email if available. Do not use too short delays if you do not want to be blacklisted by external services, but not too long delays if you From d348477efc933e5f2d1fa85c027b7d7fb2ccf623 Mon Sep 17 00:00:00 2001 From: lub Date: Tue, 1 Sep 2020 21:50:21 +0200 Subject: [PATCH 02/14] add towncrier for 1610 --- towncrier/newsfragments/1610.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/1610.feature diff --git a/towncrier/newsfragments/1610.feature b/towncrier/newsfragments/1610.feature new file mode 100644 index 00000000..b56ac332 --- /dev/null +++ b/towncrier/newsfragments/1610.feature @@ -0,0 +1 @@ +Add possibility to enforce inbound STARTTLS via INBOUND_TLS_LEVEL=true From 05e2af180256b6a9387cd437d023c8d3f968a651 Mon Sep 17 00:00:00 2001 From: lub Date: Wed, 2 Sep 2020 15:16:10 +0200 Subject: [PATCH 03/14] fix small typo in Auth-SSL --- core/admin/mailu/internal/nginx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index a7771f91..e1f8cb7a 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -33,7 +33,7 @@ def handle_authentication(headers): if method == "none" and protocol == "smtp": server, port = get_server(protocol, False) if app.config["INBOUND_TLS_ENFORCE"]: - if "Auth-SSl" in headers and headers["Auth-SSL"] == "on": + if "Auth-SSL" in headers and headers["Auth-SSL"] == "on": return { "Auth-Status": "OK", "Auth-Server": server, From 59bc4f7aea9da3f3ccf9ceca9a07af825de9e4af Mon Sep 17 00:00:00 2001 From: anrc <15327800+githtz@users.noreply.github.com> Date: Thu, 24 Sep 2020 13:16:25 +0200 Subject: [PATCH 04/14] Remove the username from the milter_headers Rspamd adds the name of the authenticated user by default. Setting add_smtp_user to false prevents the login to be leaked. --- core/rspamd/conf/milter_headers.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/rspamd/conf/milter_headers.conf b/core/rspamd/conf/milter_headers.conf index cb680cfa..7da200df 100644 --- a/core/rspamd/conf/milter_headers.conf +++ b/core/rspamd/conf/milter_headers.conf @@ -5,6 +5,9 @@ skip_authenticated = false; use = ["x-spamd-bar", "x-spam-level", "x-virus", "authentication-results"]; routines { + authentication-results { + add_smtp_user = false; + } x-virus { symbols = ["CLAM_VIRUS", "FPROT_VIRUS", "JUST_EICAR"]; } From 3f037e2f08acdf078064e4e03fc1c2b46551b27a Mon Sep 17 00:00:00 2001 From: anrc <15327800+githtz@users.noreply.github.com> Date: Thu, 24 Sep 2020 16:53:42 +0200 Subject: [PATCH 05/14] Add changelog --- towncrier/newsfragments/1638.fix | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/1638.fix diff --git a/towncrier/newsfragments/1638.fix b/towncrier/newsfragments/1638.fix new file mode 100644 index 00000000..9a87e41e --- /dev/null +++ b/towncrier/newsfragments/1638.fix @@ -0,0 +1 @@ +Hide the login of the user in sent emails From 22af5b8432b1f229d8a4a29e5ee46b1fe0c62bba Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 20 Feb 2021 19:10:20 +0100 Subject: [PATCH 06/14] Switch to server-side sessions in redis --- core/admin/mailu/__init__.py | 4 ++++ core/admin/requirements-prod.txt | 1 + core/admin/requirements.txt | 1 + 3 files changed, 6 insertions(+) diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 4de3e580..4d7efba1 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -1,5 +1,8 @@ import flask import flask_bootstrap +import redis +from flask_kvsession import KVSessionExtension +from simplekv.memory.redisstore import RedisStore from mailu import utils, debug, models, manage, configuration @@ -17,6 +20,7 @@ def create_app_from_config(config): # Initialize application extensions config.init_app(app) models.db.init_app(app) + KVSessionExtension(RedisStore(redis.StrictRedis().from_url('redis://{0}/3'.format(config['REDIS_ADDRESS']))), app) utils.limiter.init_app(app) utils.babel.init_app(app) utils.login.init_app(app) diff --git a/core/admin/requirements-prod.txt b/core/admin/requirements-prod.txt index f767f431..54cf9a14 100644 --- a/core/admin/requirements-prod.txt +++ b/core/admin/requirements-prod.txt @@ -13,6 +13,7 @@ Flask==1.0.2 Flask-Babel==0.12.2 Flask-Bootstrap==3.3.7.1 Flask-DebugToolbar==0.10.1 +Flask-KVSession==0.6.2 Flask-Limiter==1.0.1 Flask-Login==0.4.1 Flask-Migrate==2.4.0 diff --git a/core/admin/requirements.txt b/core/admin/requirements.txt index 9739ed3f..abb37234 100644 --- a/core/admin/requirements.txt +++ b/core/admin/requirements.txt @@ -3,6 +3,7 @@ Flask-Login Flask-SQLAlchemy Flask-bootstrap Flask-Babel +Flask-KVSession Flask-migrate Flask-script Flask-wtf From d459c374322f219ab12801d17817c12c628f1fdc Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 22 Feb 2021 20:34:06 +0100 Subject: [PATCH 07/14] make session IDs 128bits --- core/admin/mailu/configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 429e778c..a9ab937f 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -135,6 +135,7 @@ class ConfigManager(dict): self.config['QUOTA_STORAGE_URL'] = 'redis://{0}/1'.format(self.config['REDIS_ADDRESS']) self.config['SESSION_COOKIE_SAMESITE'] = 'Strict' self.config['SESSION_COOKIE_HTTPONLY'] = True + self.config['SESSION_KEY_BITS'] = 128 # update the app config itself app.config = self From a1d32568d6aceec8b0a2a0fd0514714585020edc Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 22 Feb 2021 20:43:52 +0100 Subject: [PATCH 08/14] Regenerate session-ids to prevent session fixation --- core/admin/mailu/ui/views/base.py | 2 ++ core/admin/mailu/ui/views/users.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index 7501a883..625c02e1 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -17,6 +17,7 @@ def login(): if form.validate_on_submit(): user = models.User.login(form.email.data, form.pw.data) if user: + flask.session.regenerate() flask_login.login_user(user) endpoint = flask.request.args.get('next', '.index') return flask.redirect(flask.url_for(endpoint) @@ -30,6 +31,7 @@ def login(): @access.authenticated def logout(): flask_login.logout_user() + flask.session.destroy() return flask.redirect(flask.url_for('.index')) diff --git a/core/admin/mailu/ui/views/users.py b/core/admin/mailu/ui/views/users.py index 8830ff5b..2784fe53 100644 --- a/core/admin/mailu/ui/views/users.py +++ b/core/admin/mailu/ui/views/users.py @@ -119,6 +119,7 @@ def user_password(user_email): if form.pw.data != form.pw2.data: flask.flash('Passwords do not match', 'error') else: + flask.session.regenerate() user.set_password(form.pw.data) models.db.session.commit() flask.flash('Password updated for %s' % user) @@ -186,6 +187,7 @@ def user_signup(domain_name=None): if domain.has_email(form.localpart.data): flask.flash('Email is already used', 'error') else: + flask.session.regenerate() user = models.User(domain=domain) form.populate_obj(user) user.set_password(form.pw.data) From b9becd86497fa685e80cca2ccbe20d54405e6d24 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 22 Feb 2021 21:15:25 +0100 Subject: [PATCH 09/14] make sessions expire --- core/admin/mailu/configuration.py | 3 +++ docs/configuration.rst | 2 ++ 2 files changed, 5 insertions(+) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index a9ab937f..0f50bc95 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -1,5 +1,6 @@ import os +from datetime import timedelta from socrate import system DEFAULT_CONFIG = { @@ -53,6 +54,7 @@ DEFAULT_CONFIG = { 'RECAPTCHA_PRIVATE_KEY': '', # Advanced settings 'LOG_LEVEL': 'WARNING', + 'SESSION_LIFETIME': 24, 'SESSION_COOKIE_SECURE': True, 'CREDENTIAL_ROUNDS': 12, # Host settings @@ -136,6 +138,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_SAMESITE'] = 'Strict' self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['SESSION_KEY_BITS'] = 128 + self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) # update the app config itself app.config = self diff --git a/docs/configuration.rst b/docs/configuration.rst index 26bdb024..7cb53d13 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -142,6 +142,8 @@ The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by The ``SESSION_COOKIE_SECURE`` (default: True) setting controls the secure flag on the cookies of the administrative interface. It should only be turned off if you intend to access it over plain HTTP. +``SESSION_LIFETIME`` (default: 24) is the length in hours a session is valid for on the administrative interface. + The ``LOG_LEVEL`` setting is used by the python start-up scripts as a logging threshold. Log messages equal or higher than this priority will be printed. Can be one of: CRITICAL, ERROR, WARNING, INFO, DEBUG or NOTSET. From 481cb6739216d168e6c439852a7db2e441b13f68 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 22 Feb 2021 21:18:06 +0100 Subject: [PATCH 10/14] cleanup old sessions on startup --- core/admin/mailu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 4d7efba1..f9ca2466 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -20,7 +20,7 @@ def create_app_from_config(config): # Initialize application extensions config.init_app(app) models.db.init_app(app) - KVSessionExtension(RedisStore(redis.StrictRedis().from_url('redis://{0}/3'.format(config['REDIS_ADDRESS']))), app) + KVSessionExtension(RedisStore(redis.StrictRedis().from_url('redis://{0}/3'.format(config['REDIS_ADDRESS']))), app).cleanup_sessions(app) utils.limiter.init_app(app) utils.babel.init_app(app) utils.login.init_app(app) From 64d757582d2f3531604503d3608dd2815a591c72 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 22 Feb 2021 21:59:15 +0100 Subject: [PATCH 11/14] Disable anti-csrf on the login form The rationale is that the attacker doesn't have the password... and that doing it this way we avoid creating useless sessions --- core/admin/mailu/ui/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py index 356137e8..32bb31ab 100644 --- a/core/admin/mailu/ui/forms.py +++ b/core/admin/mailu/ui/forms.py @@ -46,6 +46,8 @@ class ConfirmationForm(flask_wtf.FlaskForm): class LoginForm(flask_wtf.FlaskForm): + class Meta: + csrf = False email = fields.StringField(_('E-mail'), [validators.Email()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) submit = fields.SubmitField(_('Sign in')) From 513d2a4c5ef6930fe4d6f6e7371039233227dfb5 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 9 Mar 2021 19:43:08 +0100 Subject: [PATCH 12/14] Fix bug #1660: nested headers shouldn't be touched --- core/postfix/conf/master.cf | 1 + 1 file changed, 1 insertion(+) diff --git a/core/postfix/conf/master.cf b/core/postfix/conf/master.cf index b43095ee..e45a8ccf 100644 --- a/core/postfix/conf/master.cf +++ b/core/postfix/conf/master.cf @@ -12,6 +12,7 @@ smtp inet n - n - - smtpd -o cleanup_service_name=outclean outclean unix n - n - 0 cleanup -o header_checks=pcre:/etc/postfix/outclean_header_filter.cf + -o nested_header_checks= # Internal postfix services pickup unix n - n 60 1 pickup From 97be7359fe1d2de4fcf9022325dcb66b387999c2 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 9 Mar 2021 19:47:34 +0100 Subject: [PATCH 13/14] towncrier --- towncrier/newsfragments/1660.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/1660.bugfix diff --git a/towncrier/newsfragments/1660.bugfix b/towncrier/newsfragments/1660.bugfix new file mode 100644 index 00000000..a90fb099 --- /dev/null +++ b/towncrier/newsfragments/1660.bugfix @@ -0,0 +1 @@ +Don't replace nested headers (typically in attached emails) From b872b46097f507ae48fbb2a102207530678c36d7 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 9 Mar 2021 20:13:31 +0100 Subject: [PATCH 14/14] towncrier --- towncrier/newsfragments/1783.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/1783.misc diff --git a/towncrier/newsfragments/1783.misc b/towncrier/newsfragments/1783.misc new file mode 100644 index 00000000..2ee4c97f --- /dev/null +++ b/towncrier/newsfragments/1783.misc @@ -0,0 +1 @@ +Switch from client side sessions (cookies) to server-side sessions (Redis). This simplies the security model a lot and allows for an easier recovery should a cookie ever land in the hands of an attacker.