1
0
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:
Dimitri Huisman 2023-04-04 08:00:25 +00:00
commit e88fa6a1f5
No known key found for this signature in database
19 changed files with 83 additions and 34 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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'))

View File

@ -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>

View File

@ -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'])

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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' }}

View File

@ -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>

View 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.

View File

@ -0,0 +1 @@
Ensure we ask for the existing password before processing a password change request.

View 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