mirror of
https://github.com/Mailu/Mailu.git
synced 2025-01-30 04:30:49 +02:00
Merge branch 'master' into new-release-mailu
This commit is contained in:
commit
e88fa6a1f5
@ -44,7 +44,7 @@ DEFAULT_CONFIG = {
|
||||
'AUTH_RATELIMIT_IP': '5/hour',
|
||||
'AUTH_RATELIMIT_IP_V4_MASK': 24,
|
||||
'AUTH_RATELIMIT_IP_V6_MASK': 48,
|
||||
'AUTH_RATELIMIT_USER': '100/day',
|
||||
'AUTH_RATELIMIT_USER': '50/day',
|
||||
'AUTH_RATELIMIT_EXEMPTION': '',
|
||||
'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400,
|
||||
'DISABLE_STATISTICS': False,
|
||||
|
@ -85,6 +85,7 @@ def handle_authentication(headers):
|
||||
raw_user_email = urllib.parse.unquote(headers["Auth-User"])
|
||||
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
|
||||
user_email = 'invalid'
|
||||
password = 'invalid'
|
||||
try:
|
||||
user_email = raw_user_email.encode("iso8859-1").decode("utf8")
|
||||
password = raw_password.encode("iso8859-1").decode("utf8")
|
||||
@ -107,6 +108,7 @@ def handle_authentication(headers):
|
||||
"Auth-Server": server,
|
||||
"Auth-User": user_email,
|
||||
"Auth-User-Exists": is_valid_user,
|
||||
"Auth-Password": password,
|
||||
"Auth-Port": port
|
||||
}
|
||||
status, code = get_status(protocol, "authentication")
|
||||
@ -115,6 +117,7 @@ def handle_authentication(headers):
|
||||
"Auth-Error-Code": code,
|
||||
"Auth-User": user_email,
|
||||
"Auth-User-Exists": is_valid_user,
|
||||
"Auth-Password": password,
|
||||
"Auth-Wait": 0
|
||||
}
|
||||
# Unexpected
|
||||
|
@ -48,7 +48,7 @@ def nginx_authentication():
|
||||
if headers.get("Auth-Status") == "OK":
|
||||
utils.limiter.exempt_ip_from_ratelimits(client_ip)
|
||||
elif is_valid_user:
|
||||
utils.limiter.rate_limit_user(username, client_ip)
|
||||
utils.limiter.rate_limit_user(username, client_ip, password=response.headers.get('Auth-Password', None))
|
||||
elif not is_from_webmail:
|
||||
utils.limiter.rate_limit_ip(client_ip, username)
|
||||
return response
|
||||
|
@ -68,9 +68,13 @@ class LimitWraperFactory(object):
|
||||
app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.')
|
||||
return is_rate_limited
|
||||
|
||||
def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None):
|
||||
def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None, password=''):
|
||||
limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user')
|
||||
if self.is_subject_to_rate_limits(ip):
|
||||
truncated_password = hmac.new(bytearray(username, 'utf-8'), bytearray(password, 'utf-8'), 'sha256').hexdigest()[-6:]
|
||||
if password and (self.storage.get(f'dedup2-{username}-{truncated_password}') > 0):
|
||||
return
|
||||
self.storage.incr(f'dedup2-{username}-{truncated_password}', limits.parse(app.config['AUTH_RATELIMIT_USER']).GRANULARITY.seconds, True)
|
||||
limiter.hit(device_cookie if device_cookie_name == username else username)
|
||||
|
||||
""" Device cookies as described on:
|
||||
|
@ -59,7 +59,7 @@ def login():
|
||||
flask.flash(msg, "error")
|
||||
return response
|
||||
else:
|
||||
utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip, username)
|
||||
utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username, form.pw.data) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip, username)
|
||||
flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.')
|
||||
flask.flash('Wrong e-mail or password', 'error')
|
||||
return flask.render_template('login.html', form=form, fields=fields)
|
||||
|
@ -128,6 +128,12 @@ class UserPasswordForm(flask_wtf.FlaskForm):
|
||||
pwned = fields.HiddenField(label='', default=-1)
|
||||
submit = fields.SubmitField(_('Update password'))
|
||||
|
||||
class UserPasswordChangeForm(flask_wtf.FlaskForm):
|
||||
current_pw = fields.PasswordField(_('Current password'), [validators.DataRequired()])
|
||||
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
||||
pw2 = fields.PasswordField(_('Password check'), [validators.DataRequired()])
|
||||
pwned = fields.HiddenField(label='', default=-1)
|
||||
submit = fields.SubmitField(_('Update password'))
|
||||
|
||||
class UserReplyForm(flask_wtf.FlaskForm):
|
||||
reply_enabled = fields.BooleanField(_('Enable automatic reply'))
|
||||
|
@ -20,7 +20,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('.user_password') }}" class="nav-link" role="menuitem">
|
||||
<a href="{{ url_for('.user_password_change') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-lock"></i>
|
||||
<p>{% trans %}Update password{% endtrans %}</p>
|
||||
</a>
|
||||
|
@ -99,18 +99,13 @@ def user_settings(user_email):
|
||||
flask.url_for('.user_list', domain_name=user.domain.name))
|
||||
return flask.render_template('user/settings.html', form=form, user=user)
|
||||
|
||||
|
||||
@ui.route('/user/password', methods=['GET', 'POST'], defaults={'user_email': None})
|
||||
@ui.route('/user/password/<path:user_email>', methods=['GET', 'POST'])
|
||||
@access.owner(models.User, 'user_email')
|
||||
def user_password(user_email):
|
||||
def _process_password_change(form, user_email):
|
||||
user_email_or_current = user_email or flask_login.current_user.email
|
||||
user = models.User.query.get(user_email_or_current) or flask.abort(404)
|
||||
form = forms.UserPasswordForm()
|
||||
if form.validate_on_submit():
|
||||
if form.pw.data != form.pw2.data:
|
||||
flask.flash('Passwords do not match', 'error')
|
||||
else:
|
||||
elif user_email or models.User.login(user_email_or_current, form.current_pw.data):
|
||||
if msg := utils.isBadOrPwned(form):
|
||||
flask.flash(msg, "error")
|
||||
return flask.render_template('user/password.html', form=form, user=user)
|
||||
@ -121,8 +116,19 @@ def user_password(user_email):
|
||||
if user_email:
|
||||
return flask.redirect(flask.url_for('.user_list',
|
||||
domain_name=user.domain.name))
|
||||
else:
|
||||
flask.flash('Wrong current password', 'error')
|
||||
return flask.render_template('user/password.html', form=form, user=user)
|
||||
|
||||
@ui.route('/user/password', methods=['GET', 'POST'], defaults={'user_email': None})
|
||||
@access.owner(models.User, 'user_email')
|
||||
def user_password_change(user_email):
|
||||
return _process_password_change(forms.UserPasswordChangeForm(), user_email)
|
||||
|
||||
@ui.route('/user/password/<path:user_email>', methods=['GET', 'POST'])
|
||||
@access.domain_admin(models.User, 'user_email')
|
||||
def user_password(user_email):
|
||||
return _process_password_change(forms.UserPasswordForm(), user_email)
|
||||
|
||||
@ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None})
|
||||
@ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST'])
|
||||
|
@ -135,12 +135,6 @@ WEBSITE=https://mailu.io
|
||||
# Advanced settings
|
||||
###################################
|
||||
|
||||
# Log driver for front service. Possible values:
|
||||
# json-file (default)
|
||||
# journald (On systemd platforms, useful for Fail2Ban integration)
|
||||
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker compose log` for front!)
|
||||
LOG_DRIVER=json-file
|
||||
|
||||
# Docker-compose project name, this will prepended to containers names.
|
||||
COMPOSE_PROJECT_NAME=mailu
|
||||
|
||||
|
@ -9,7 +9,9 @@ services:
|
||||
restart: always
|
||||
env_file: .env
|
||||
logging:
|
||||
driver: $LOG_DRIVER
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-front
|
||||
ports:
|
||||
- "$BIND_ADDRESS4:80:80"
|
||||
- "$BIND_ADDRESS4:443:443"
|
||||
@ -43,6 +45,10 @@ services:
|
||||
image: mailu/dovecot:$VERSION
|
||||
restart: always
|
||||
env_file: .env
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-imap
|
||||
volumes:
|
||||
- "$ROOT/mail:/mail"
|
||||
- "$ROOT/overrides/dovecot:/overrides:ro"
|
||||
@ -53,6 +59,10 @@ services:
|
||||
image: mailu/postfix:$VERSION
|
||||
restart: always
|
||||
env_file: .env
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-smtp
|
||||
volumes:
|
||||
- "$ROOT/mailqueue:/queue"
|
||||
- "$ROOT/overrides/postfix:/overrides:ro"
|
||||
@ -63,6 +73,10 @@ services:
|
||||
image: mailu/rspamd:$VERSION
|
||||
restart: always
|
||||
env_file: .env
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-antispam
|
||||
volumes:
|
||||
- "$ROOT/filter:/var/lib/rspamd"
|
||||
- "$ROOT/dkim:/dkim:ro"
|
||||
@ -88,6 +102,10 @@ services:
|
||||
image: mailu/admin:$VERSION
|
||||
restart: always
|
||||
env_file: .env
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-admin
|
||||
volumes:
|
||||
- "$ROOT/data:/data"
|
||||
- "$ROOT/dkim:/dkim"
|
||||
|
@ -35,8 +35,6 @@ services:
|
||||
image: mailu/nginx:$VERSION
|
||||
restart: always
|
||||
env_file: .env
|
||||
logging:
|
||||
driver: $LOG_DRIVER
|
||||
labels: # Traefik labels for simple reverse-proxying
|
||||
- "traefik.enable=true"
|
||||
- "traefik.port=80"
|
||||
|
@ -47,10 +47,11 @@ accounts for a specific IP subnet as defined in
|
||||
``AUTH_RATELIMIT_IP_V4_MASK`` (default: /24) and
|
||||
``AUTH_RATELIMIT_IP_V6_MASK`` (default: /48).
|
||||
|
||||
The ``AUTH_RATELIMIT_USER`` (default: 100/day) holds a security setting for fighting
|
||||
The ``AUTH_RATELIMIT_USER`` (default: 50/day) holds a security setting for fighting
|
||||
attackers that attempt to guess a user's password (typically using a password
|
||||
bruteforce attack). The value defines the limit of authentication attempts allowed
|
||||
for any given account within a specific timeframe.
|
||||
bruteforce attack). The value defines the limit of distinct authentication attempts
|
||||
allowed for any given account within a specific timeframe. Multiple attempts for the
|
||||
same account with the same password only counts for one.
|
||||
|
||||
The ``AUTH_RATELIMIT_EXEMPTION_LENGTH`` (default: 86400) is the number of seconds
|
||||
after a successful login for which a specific IP address is exempted from rate limits.
|
||||
|
@ -579,8 +579,7 @@ down brute force attacks. The same applies to login attempts via the single sign
|
||||
We *do* provide a possibility to export the logs from the ``front`` service and ``Admin`` service to the host.
|
||||
The ``front`` container logs failed logon attempts on SMTP, IMAP and POP3.
|
||||
The ``Admin`` container logs failed logon attempt on the single sign on page.
|
||||
For this you need to set ``LOG_DRIVER=journald`` or ``syslog``, depending on the log
|
||||
manager of the host. You will need to setup the proper Regex in the Fail2Ban configuration.
|
||||
You will need to setup the proper Regex in the Fail2Ban configuration.
|
||||
Below an example how to do so.
|
||||
|
||||
If you use a reverse proxy in front of Mailu, it is vital to set the environment variables REAL_IP_HEADER and REAL_IP_FROM.
|
||||
|
@ -26,7 +26,9 @@ services:
|
||||
restart: always
|
||||
env_file: {{ env }}
|
||||
logging:
|
||||
driver: {{ log_driver or 'json-file' }}
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-front
|
||||
ports:
|
||||
{% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %}
|
||||
{% if bind4 %}
|
||||
@ -38,8 +40,12 @@ services:
|
||||
{% endfor %}
|
||||
networks:
|
||||
- default
|
||||
{% if webmail_type != 'none' %}
|
||||
- webmail
|
||||
{% endif %}
|
||||
{% if webdav_enabled %}
|
||||
- radicale
|
||||
{% endif %}
|
||||
volumes:
|
||||
- "{{ root }}/certs:/certs"
|
||||
- "{{ root }}/overrides/nginx:/overrides:ro"
|
||||
@ -62,6 +68,10 @@ services:
|
||||
image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}admin:${MAILU_VERSION:-{{ version }}}
|
||||
restart: always
|
||||
env_file: {{ env }}
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-admin
|
||||
{% if not admin_enabled %}
|
||||
ports:
|
||||
- 127.0.0.1:8080:80
|
||||
@ -81,6 +91,10 @@ services:
|
||||
image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}dovecot:${MAILU_VERSION:-{{ version }}}
|
||||
restart: always
|
||||
env_file: {{ env }}
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-imap
|
||||
volumes:
|
||||
- "{{ root }}/mail:/mail"
|
||||
- "{{ root }}/overrides/dovecot:/overrides:ro"
|
||||
@ -96,6 +110,10 @@ services:
|
||||
image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}postfix:${MAILU_VERSION:-{{ version }}}
|
||||
restart: always
|
||||
env_file: {{ env }}
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-smtp
|
||||
volumes:
|
||||
- "{{ root }}/mailqueue:/queue"
|
||||
- "{{ root }}/overrides/postfix:/overrides:ro"
|
||||
@ -127,6 +145,10 @@ services:
|
||||
hostname: antispam
|
||||
restart: always
|
||||
env_file: {{ env }}
|
||||
logging:
|
||||
driver: journald
|
||||
options:
|
||||
tag: mailu-antispam
|
||||
{% if oletools_enabled %}
|
||||
networks:
|
||||
- default
|
||||
|
@ -158,12 +158,6 @@ DOMAIN_REGISTRATION=true
|
||||
# Advanced settings
|
||||
###################################
|
||||
|
||||
# Log driver for front service. Possible values:
|
||||
# json-file (default)
|
||||
# journald (On systemd platforms, useful for Fail2Ban integration)
|
||||
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker compose log` for front!)
|
||||
# LOG_DRIVER={{ log_driver or 'json-file' }}
|
||||
|
||||
# Docker-compose project name, this will prepended to containers names.
|
||||
COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }}
|
||||
|
||||
|
@ -47,7 +47,7 @@ Or in plain english: if receivers start to classify your mail as spam, this post
|
||||
<label>Authentication rate limit per user</label>
|
||||
<!-- Validates number input only -->
|
||||
<p><input class="form-control" style="width: 9%; display: inline;" type="number" name="auth_ratelimit_user"
|
||||
value="100" required > / day
|
||||
value="50" required > / day
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
1
towncrier/newsfragments/2726.misc
Normal file
1
towncrier/newsfragments/2726.misc
Normal file
@ -0,0 +1 @@
|
||||
Change the behaviour of AUTH_RATELIMIT_USER and only account for distinct attempts. Same username and same password is now a only accounted once per period.
|
1
towncrier/newsfragments/2733.misc
Normal file
1
towncrier/newsfragments/2733.misc
Normal file
@ -0,0 +1 @@
|
||||
Ensure we ask for the existing password before processing a password change request.
|
2
towncrier/newsfragments/2734.misc
Normal file
2
towncrier/newsfragments/2734.misc
Normal file
@ -0,0 +1,2 @@
|
||||
Remove LOG_DRIVER which never worked and replace it with journald by default
|
||||
Fix a bug where front may get attached to networks that don't exist
|
Loading…
x
Reference in New Issue
Block a user