diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 4de3e580..f9ca2466 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).cleanup_sessions(app) utils.limiter.init_app(app) utils.babel.init_app(app) utils.login.init_app(app) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 429e778c..6f65d17d 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 = { @@ -31,6 +32,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, @@ -53,6 +55,7 @@ DEFAULT_CONFIG = { 'RECAPTCHA_PRIVATE_KEY': '', # Advanced settings 'LOG_LEVEL': 'WARNING', + 'SESSION_LIFETIME': 24, 'SESSION_COOKIE_SECURE': True, 'CREDENTIAL_ROUNDS': 12, # Host settings @@ -135,6 +138,8 @@ 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 + self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) # update the app config itself app.config = self diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index f9ebbf13..600e438b 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" + }), } def check_credentials(user, password, ip, protocol=None): @@ -42,12 +45,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/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')) 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) diff --git a/core/admin/requirements-prod.txt b/core/admin/requirements-prod.txt index 8968c38c..7620fd95 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-marshmallow==0.14.0 diff --git a/core/admin/requirements.txt b/core/admin/requirements.txt index 59383a07..f9d175b3 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 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 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"]; } diff --git a/docs/configuration.rst b/docs/configuration.rst index 26bdb024..e08675a8 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 + .. _fetchmail: The ``FETCHMAIL_DELAY`` is a delay (in seconds) for the fetchmail service to @@ -142,6 +149,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. 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 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 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) 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.