diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 51532968..7fb04380 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -28,6 +28,7 @@ def create_app_from_config(config): utils.proxy.init_app(app) utils.migrate.init_app(app, models.db) + app.device_cookie_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('DEVICE_COOKIE_KEY', 'utf-8'), 'sha256').digest() app.temp_token_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('WEBMAIL_TEMP_TOKEN_KEY', 'utf-8'), 'sha256').digest() app.srs_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('SRS_KEY', 'utf-8'), 'sha256').digest() diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 1f2a9239..d33efa12 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -2,6 +2,7 @@ import os from datetime import timedelta from socrate import system +import ipaddress DEFAULT_CONFIG = { # Specific to the admin UI @@ -36,8 +37,12 @@ DEFAULT_CONFIG = { 'TLS_FLAVOR': 'cert', 'INBOUND_TLS_ENFORCE': False, 'DEFER_ON_TLS_ERROR': True, - 'AUTH_RATELIMIT': '1000/minute;10000/hour', - 'AUTH_RATELIMIT_SUBNET': False, + 'AUTH_RATELIMIT_IP': '60/hour', + 'AUTH_RATELIMIT_IP_V4_MASK': 24, + 'AUTH_RATELIMIT_IP_V6_MASK': 56, + 'AUTH_RATELIMIT_USER': '100/day', + 'AUTH_RATELIMIT_EXEMPTION': '', + 'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400, 'DISABLE_STATISTICS': False, # Mail settings 'DMARC_RUA': None, @@ -148,6 +153,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')] + self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr) self.config['HOSTNAMES'] = ','.join(hostnames) self.config['HOSTNAME'] = hostnames[0] # update the app config itself diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index f011bf5a..04d6d94e 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -20,6 +20,11 @@ STATUSES = { "encryption": ("Must issue a STARTTLS command first", { "smtp": "530 5.7.0" }), + "ratelimit": ("Temporary authentication failure (rate-limit)", { + "imap": "LIMIT", + "smtp": "451 4.3.2", + "pop3": "-ERR [LOGIN-DELAY] Retry later" + }), } def check_credentials(user, password, ip, protocol=None): @@ -72,8 +77,8 @@ def handle_authentication(headers): } # Authenticated user elif method == "plain": - service_port = int(urllib.parse.unquote(headers["Auth-Port"])) - if service_port == 25: + is_valid_user = False + if headers["Auth-Port"] == '25': return { "Auth-Status": "AUTH not supported", "Auth-Error-Code": "502 5.5.1", @@ -85,29 +90,37 @@ def handle_authentication(headers): # we need to manually decode. raw_user_email = urllib.parse.unquote(headers["Auth-User"]) raw_password = urllib.parse.unquote(headers["Auth-Pass"]) + user_email = 'invalid' try: user_email = raw_user_email.encode("iso8859-1").decode("utf8") password = raw_password.encode("iso8859-1").decode("utf8") + ip = urllib.parse.unquote(headers["Client-Ip"]) except: app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}') else: try: user = models.User.query.get(user_email) + is_valid_user = True except sqlalchemy.exc.StatementError as exc: exc = str(exc).split('\n', 1)[0] app.logger.warn(f'Invalid user {user_email!r}: {exc}') else: - if check_credentials(user, password, urllib.parse.unquote(headers["Client-Ip"]), protocol): + ip = urllib.parse.unquote(headers["Client-Ip"]) + if check_credentials(user, password, ip, protocol): server, port = get_server(headers["Auth-Protocol"], True) return { "Auth-Status": "OK", "Auth-Server": server, + "Auth-User": user_email, + "Auth-User-Exists": is_valid_user, "Auth-Port": port } status, code = get_status(protocol, "authentication") return { "Auth-Status": status, "Auth-Error-Code": code, + "Auth-User": user_email, + "Auth-User-Exists": is_valid_user, "Auth-Wait": 0 } # Unexpected diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 1686e1cb..c5cd9e28 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -5,19 +5,17 @@ from flask import current_app as app import flask import flask_login import base64 -import ipaddress - @internal.route("/auth/email") def nginx_authentication(): """ Main authentication endpoint for Nginx email server """ - limiter = utils.limiter.get_limiter(app.config["AUTH_RATELIMIT"], "auth-ip") client_ip = flask.request.headers["Client-Ip"] - if not limiter.test(client_ip): + if utils.limiter.should_rate_limit_ip(client_ip): + status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit') response = flask.Response() - response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded' - response.headers['Auth-Error-Code'] = '451 4.3.2' + response.headers['Auth-Status'] = status + response.headers['Auth-Error-Code'] = code if int(flask.request.headers['Auth-Login-Attempt']) < 10: response.headers['Auth-Wait'] = '3' return response @@ -25,14 +23,27 @@ def nginx_authentication(): response = flask.Response() for key, value in headers.items(): response.headers[key] = str(value) - if ("Auth-Status" not in headers) or (headers["Auth-Status"] != "OK"): - limit_subnet = str(app.config["AUTH_RATELIMIT_SUBNET"]) != 'False' - subnet = ipaddress.ip_network(app.config["SUBNET"]) - if limit_subnet or ipaddress.ip_address(client_ip) not in subnet: - limiter.hit(flask.request.headers["Client-Ip"]) + is_valid_user = False + if response.headers.get("Auth-User-Exists"): + username = response.headers["Auth-User"] + if utils.limiter.should_rate_limit_user(username, client_ip): + # FIXME could be done before handle_authentication() + status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit') + response = flask.Response() + response.headers['Auth-Status'] = status + response.headers['Auth-Error-Code'] = code + if int(flask.request.headers['Auth-Login-Attempt']) < 10: + response.headers['Auth-Wait'] = '3' + return response + is_valid_user = True + 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) + else: + rate_limit_ip(client_ip) return response - @internal.route("/auth/admin") def admin_authentication(): """ Fails if the user is not an authenticated admin. @@ -60,15 +71,29 @@ def user_authentication(): def basic_authentication(): """ Tries to authenticate using the Authorization header. """ + client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) + if utils.limiter.should_rate_limit_ip(client_ip): + response = flask.Response(status=401) + response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"' + response.headers['Retry-After'] = '60' + return response authorization = flask.request.headers.get("Authorization") if authorization and authorization.startswith("Basic "): encoded = authorization.replace("Basic ", "") user_email, password = base64.b64decode(encoded).split(b":", 1) - user = models.User.query.get(user_email.decode("utf8")) - if nginx.check_credentials(user, password.decode('utf-8'), flask.request.remote_addr, "web"): + user_email = user_email.decode("utf8") + if utils.limiter.should_rate_limit_user(user_email, client_ip): + response = flask.Response(status=401) + response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"' + response.headers['Retry-After'] = '60' + return response + user = models.User.query.get(user_email) + if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"): response = flask.Response() response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "") + utils.limiter.exempt_ip_from_ratelimits(client_ip) return response + utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip) response = flask.Response(status=401) response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"' return response diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index b5f99915..3bc65f4f 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -1,7 +1,12 @@ +from mailu import utils +from flask import current_app as app +import base64 import limits import limits.storage import limits.strategies +import hmac +import secrets class LimitWrapper(object): """ Wraps a limit by providing the storage, item and identifiers @@ -31,4 +36,59 @@ class LimitWraperFactory(object): self.limiter = limits.strategies.MovingWindowRateLimiter(self.storage) def get_limiter(self, limit, *args): - return LimitWrapper(self.limiter, limits.parse(limit), *args) \ No newline at end of file + return LimitWrapper(self.limiter, limits.parse(limit), *args) + + def is_subject_to_rate_limits(self, ip): + return False if utils.is_exempt_from_ratelimits(ip) else not (self.storage.get(f'exempt-{ip}') > 0) + + def exempt_ip_from_ratelimits(self, ip): + self.storage.incr(f'exempt-{ip}', app.config["AUTH_RATELIMIT_EXEMPTION_LENGTH"], True) + + def should_rate_limit_ip(self, ip): + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip') + client_network = utils.extract_network_from_ip(ip) + is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(client_network) + if is_rate_limited: + app.logger.warn(f'Authentication attempt from {ip} has been rate-limited.') + return is_rate_limited + + def rate_limit_ip(self, ip): + if ip != app.config['WEBMAIL_ADDRESS']: + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip') + client_network = utils.extract_network_from_ip(ip) + if self.is_subject_to_rate_limits(ip): + limiter.hit(client_network) + + def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') + is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(device_cookie if device_cookie_name == username else username) + if is_rate_limited: + 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): + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') + if self.is_subject_to_rate_limits(ip): + limiter.hit(device_cookie if device_cookie_name == username else username) + + """ Device cookies as described on: + https://owasp.org/www-community/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies + """ + def parse_device_cookie(self, cookie): + try: + login, nonce, _ = cookie.split('$') + if hmac.compare_digest(cookie, self.device_cookie(login, nonce)): + return nonce, login + except: + pass + return None, None + + """ Device cookies don't require strong crypto: + 72bits of nonce, 96bits of signature is more than enough + and these values avoid padding in most cases + """ + def device_cookie(self, username, nonce=None): + if not nonce: + nonce = secrets.token_urlsafe(9) + sig = str(base64.urlsafe_b64encode(hmac.new(app.device_cookie_key, bytearray(f'device_cookie|{username}|{nonce}', 'utf-8'), 'sha256').digest()[20:]), 'utf-8') + return f'{username}${nonce}${sig}' diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index ecd3089a..515d6055 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -1,4 +1,4 @@ -from mailu import models +from mailu import models, utils from mailu.ui import ui, forms, access from flask import current_app as app @@ -14,16 +14,30 @@ def index(): @ui.route('/login', methods=['GET', 'POST']) def login(): + client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) form = forms.LoginForm() if form.validate_on_submit(): - user = models.User.login(form.email.data, form.pw.data) + device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit')) + username = form.email.data + if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip): + flask.flash('Too many attempts from your IP (rate-limit)', 'error') + return flask.render_template('login.html', form=form) + if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username): + flask.flash('Too many attempts for this user (rate-limit)', 'error') + return flask.render_template('login.html', form=form) + user = models.User.login(username, 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) + response = flask.redirect(flask.url_for(endpoint) or flask.url_for('.index')) + response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('ui.login')) + flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.') + 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) + 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) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index a77cef47..dda927b0 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -17,10 +17,12 @@ from multiprocessing import Value from mailu import limiter +from flask import current_app as app import flask import flask_login import flask_migrate import flask_babel +import ipaddress import redis from flask.sessions import SessionMixin, SessionInterface @@ -57,19 +59,30 @@ def has_dane_record(domain, timeout=10): # If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled # we will receive this non-specific exception. The safe behaviour is to # accept to defer the email. - flask.current_app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?') - return flask.current_app.config['DEFER_ON_TLS_ERROR'] + app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?') + return app.config['DEFER_ON_TLS_ERROR'] except dns.exception.Timeout: - flask.current_app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).') + app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).') except dns.resolver.NXDOMAIN: pass # this is expected, not TLSA record is fine except Exception as e: - flask.current_app.logger.error(f'Error while looking up the TLSA record for {domain} {e}') + app.logger.error(f'Error while looking up the TLSA record for {domain} {e}') pass # Rate limiter limiter = limiter.LimitWraperFactory() +def extract_network_from_ip(ip): + n = ipaddress.ip_network(ip) + if n.version == 4: + return str(n.supernet(prefixlen_diff=(32-int(app.config["AUTH_RATELIMIT_IP_V4_MASK"]))).network_address) + else: + return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address) + +def is_exempt_from_ratelimits(ip): + ip = ipaddress.ip_address(ip) + return any(ip in cidr for cidr in app.config['AUTH_RATELIMIT_EXEMPTION']) + # Application translation babel = flask_babel.Babel() @@ -77,8 +90,8 @@ babel = flask_babel.Babel() def get_locale(): """ selects locale for translation """ language = flask.session.get('language') - if not language in flask.current_app.config.translations: - language = flask.request.accept_languages.best_match(flask.current_app.config.translations.keys()) + if not language in app.config.translations: + language = flask.request.accept_languages.best_match(app.config.translations.keys()) flask.session['language'] = language return language @@ -475,7 +488,7 @@ class MailuSessionExtension: with cleaned.get_lock(): if not cleaned.value: cleaned.value = True - flask.current_app.logger.info('cleaning session store') + app.logger.info('cleaning session store') MailuSessionExtension.cleanup_sessions(app) app.before_first_request(cleaner) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index bfd664de..f013176d 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -217,6 +217,7 @@ http { location /internal { internal; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization; proxy_pass http://$admin; diff --git a/docs/configuration.rst b/docs/configuration.rst index 5d8e87b1..f5bd9582 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -39,14 +39,25 @@ address. The ``WILDCARD_SENDERS`` setting is a comma delimited list of user email addresses that are allowed to send emails from any existing address (spoofing the sender). -The ``AUTH_RATELIMIT`` holds a security setting for fighting attackers that -try to guess user passwords. The value is the limit of failed authentication attempts -that a single IP address can perform against IMAP, POP and SMTP authentication endpoints. +The ``AUTH_RATELIMIT_IP`` (default: 60/hour) holds a security setting for fighting +attackers that waste server resources by trying to guess user passwords (typically +using a password spraying attack). The value defines the limit of authentication +attempts that will be processed on non-existing accounts for a specific IP subnet +(as defined in ``AUTH_RATELIMIT_IP_V4_MASK`` and ``AUTH_RATELIMIT_IP_V6_MASK`` below). -If ``AUTH_RATELIMIT_SUBNET`` is ``True`` (default: False), the ``AUTH_RATELIMIT`` -rules does also apply to auth requests coming from ``SUBNET``, especially for the webmail. -If you disable this, ensure that the rate limit on the webmail is enforced in a different -way (e.g. roundcube plug-in), otherwise an attacker can simply bypass the limit using webmail. +The ``AUTH_RATELIMIT_USER`` (default: 100/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. + +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. +This ensures that users behind a NAT don't get locked out when a single client is +misconfigured... but also potentially allow for users to attack each-other. + +The ``AUTH_RATELIMIT_EXEMPTION`` (default: '') is a comma separated list of network +CIDRs that won't be subject to any form of rate limiting. Specifying ``0.0.0.0/0, ::/0`` +there is a good way to disable rate limiting altogether. The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to ``notls`` will cause Mailu not to server any web content! More on :ref:`tls_flavor`. diff --git a/docs/swarm/master/README.md b/docs/swarm/master/README.md index 42e742da..0e933308 100644 --- a/docs/swarm/master/README.md +++ b/docs/swarm/master/README.md @@ -100,6 +100,9 @@ https://github.com/moby/moby/issues/25526#issuecomment-336363408 ### Don't create an open relay ! As a side effect of this ingress mode "feature", make sure that the ingress subnet is not in your RELAYHOST, otherwise you would create an smtp open relay :-( +### Ratelimits + +When using ingress mode you probably want to disable rate limits, because all requests originate from the same ip address. Otherwise automatic login attempts can easily DoS the legitimate users. ## Scalability - smtp and imap are scalable diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index 52f4ee04..3e53e7d2 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -29,9 +29,14 @@ POSTMASTER={{ postmaster }} # Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt) TLS_FLAVOR={{ tls_flavor }} -# Authentication rate limit (per source IP address) -{% if auth_ratelimit_pm > '0' %} -AUTH_RATELIMIT={{ auth_ratelimit_pm }}/minute +# Authentication rate limit per IP (per /24 on ipv4 and /56 on ipv6) +{% if auth_ratelimit_ip > '0' %} +AUTH_RATELIMIT_IP={{ auth_ratelimit_ip }}/hour +{% endif %} + +# Authentication rate limit per user (regardless of the source-IP) +{% if auth_ratelimit_user > '0' %} +AUTH_RATELIMIT_USER={{ auth_ratelimit_user }}/day {% endif %} # Opt-out of statistics, replace with "True" to opt out @@ -150,9 +155,8 @@ DOMAIN_REGISTRATION=true # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }} -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME={{ password_scheme or 'PBKDF2' }} +# Number of rounds used by the password hashing scheme +CREDENTIAL_ROUNDS=12 # Header to take the real ip from REAL_IP_HEADER={{ real_ip_header }} diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html index f532f757..74a45800 100644 --- a/setup/templates/steps/config.html +++ b/setup/templates/steps/config.html @@ -48,10 +48,18 @@ Or in plain english: if receivers start to classify your mail as spam, this post
- + -

/ minute +

/ hour +

+
+ +
+ + +

/ day

diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env index a78515b8..254c3c7d 100644 --- a/tests/compose/core/mailu.env +++ b/tests/compose/core/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/fetchmail/mailu.env b/tests/compose/fetchmail/mailu.env index afb57751..a015eaa8 100644 --- a/tests/compose/fetchmail/mailu.env +++ b/tests/compose/fetchmail/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/filters/mailu.env b/tests/compose/filters/mailu.env index 4c8c219d..1b4fb93d 100644 --- a/tests/compose/filters/mailu.env +++ b/tests/compose/filters/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/rainloop/mailu.env b/tests/compose/rainloop/mailu.env index 1f5fce0c..944dd376 100644 --- a/tests/compose/rainloop/mailu.env +++ b/tests/compose/rainloop/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/roundcube/mailu.env b/tests/compose/roundcube/mailu.env index ba153ac2..9db7fcbe 100644 --- a/tests/compose/roundcube/mailu.env +++ b/tests/compose/roundcube/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/webdav/mailu.env b/tests/compose/webdav/mailu.env index 939f453b..f1de013d 100644 --- a/tests/compose/webdav/mailu.env +++ b/tests/compose/webdav/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/towncrier/newsfragments/116.feature b/towncrier/newsfragments/116.feature new file mode 100644 index 00000000..4f73e7a8 --- /dev/null +++ b/towncrier/newsfragments/116.feature @@ -0,0 +1 @@ + Make the rate limit apply to a subnet rather than a specific IP (/24 for v4 and /56 for v6) diff --git a/towncrier/newsfragments/1194.bugfix b/towncrier/newsfragments/1194.bugfix new file mode 100644 index 00000000..866c6c3b --- /dev/null +++ b/towncrier/newsfragments/1194.bugfix @@ -0,0 +1 @@ +Fix rate-limiting on /webdav/ diff --git a/towncrier/newsfragments/1612.feature b/towncrier/newsfragments/1612.feature new file mode 100644 index 00000000..04d8d508 --- /dev/null +++ b/towncrier/newsfragments/1612.feature @@ -0,0 +1 @@ +Refactor the rate limiter to ensure that it performs as intented. diff --git a/towncrier/newsfragments/1926.feature b/towncrier/newsfragments/1926.feature new file mode 100644 index 00000000..fdd4ae87 --- /dev/null +++ b/towncrier/newsfragments/1926.feature @@ -0,0 +1 @@ +Log authentication attempts on the admin portal