1
0
mirror of https://github.com/Mailu/Mailu.git synced 2025-01-30 04:30:49 +02:00

Improve auth-related logging

This commit is contained in:
Florent Daigniere 2023-05-04 00:14:44 +02:00
parent f2435f6964
commit 6ee913502e
11 changed files with 52 additions and 22 deletions

View File

@ -12,8 +12,11 @@ import hmac
class NoPingFilter(logging.Filter):
def filter(self, record):
if not (record.args['{host}i'] == 'localhost' and record.args['r'] == 'GET /ping HTTP/1.1'):
return True
if (record.args['{host}i'] == 'localhost' and record.args['r'] == 'GET /ping HTTP/1.1'):
return False
if record.args['r'].endswith(' /internal/rspamd/local_domains HTTP/1.1'):
return False
return True
class Logger(glogging.Logger):
def setup(self, cfg):
@ -46,6 +49,7 @@ def create_app_from_config(config):
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()
app.truncated_pw_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('TRUNCATED_PW_KEY', 'utf-8'), 'sha256').digest()
# Initialize list of translations
app.config.translations = {
@ -63,6 +67,7 @@ def create_app_from_config(config):
debug.profiler.init_app(app)
if assets := app.config.get('DEBUG_ASSETS'):
app.static_folder = assets
app.logger.setLevel(app.config.get('LOG_LEVEL'))
# Inject the default variables in the Jinja parser
# TODO: move this to blueprints when needed

View File

@ -75,7 +75,7 @@ DEFAULT_CONFIG = {
'API': False,
'WEB_API': '/api',
'API_TOKEN': None,
'LOG_LEVEL': 'WARNING',
'LOG_LEVEL': 'INFO',
'SESSION_KEY_BITS': 128,
'SESSION_TIMEOUT': 3600,
'PERMANENT_SESSION_LIFETIME': 30*24*3600,

View File

@ -27,23 +27,26 @@ STATUSES = {
WEBMAIL_PORTS = ['10143', '10025']
def check_credentials(user, password, ip, protocol=None, auth_port=None):
def check_credentials(user, password, ip, protocol=None, auth_port=None, source_port=None):
if not user or not user.enabled or (protocol == "imap" and not user.enable_imap and not auth_port in WEBMAIL_PORTS) or (protocol == "pop3" and not user.enable_pop):
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: accound disabled')
return False
is_ok = False
# webmails
if auth_port in WEBMAIL_PORTS and password.startswith('token-'):
if utils.verify_temp_token(user.get_id(), password):
is_ok = True
if not is_ok and utils.is_app_token(password):
app.logger.debug(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: webmail-token')
return True
if utils.is_app_token(password):
for token in user.tokens:
if (token.check_password(password) and
(not token.ip or token.ip == ip)):
is_ok = True
break
if not is_ok and user.check_password(password):
is_ok = True
return is_ok
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: token-{token.id}')
return True
if user.check_password(password):
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: success: password')
return True
app.logger.info(f'Login attempt for: {user}/{protocol}/{auth_port} from: {ip}/{source_port}: failed: badauth: {utils.truncated_pw_hash(password)}')
return False
def handle_authentication(headers):
""" Handle an HTTP nginx authentication request
@ -100,7 +103,7 @@ def handle_authentication(headers):
else:
is_valid_user = user is not None
ip = urllib.parse.unquote(headers["Client-Ip"])
if check_credentials(user, password, ip, protocol, headers["Auth-Port"]):
if check_credentials(user, password, ip, protocol, headers["Auth-Port"], headers['Client-Port']):
server, port = get_server(headers["Auth-Protocol"], True)
return {
"Auth-Status": "OK",

View File

@ -107,7 +107,7 @@ def basic_authentication():
exc = str(exc).split('\n', 1)[0]
app.logger.warn(f'Invalid user {user_email!r}: {exc}')
else:
if user is not None and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
if user is not None and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web", flask.request.headers.get('X-Real-Port', None)):
response = flask.Response()
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
utils.limiter.exempt_ip_from_ratelimits(client_ip)

View File

@ -17,6 +17,7 @@ def login():
return _proxy()
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
client_port = flask.request.headers.get('X-Real-Port', None)
form = forms.LoginForm()
fields = []
@ -55,13 +56,13 @@ def login():
flask_login.login_user(user)
response = flask.redirect(destination)
response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True)
flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip} pwned={form.pwned.data}.')
flask.current_app.logger.info(f'Login attempt for: {username}/sso/{flask.request.headers.get("X-Forwarded-Proto")} from: {client_ip}/{client_port}: success: password: {form.pwned.data}')
if msg := utils.isBadOrPwned(form):
flask.flash(msg, "error")
return response
else:
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.current_app.logger.info(f'Login attempt for: {username}/sso/{flask.request.headers.get("X-Forwarded-Proto")} from: {client_ip}/{client_port}: failed: badauth: {utils.truncated_pw_hash(form.pw.data)}')
flask.flash('Wrong e-mail or password', 'error')
return flask.render_template('login.html', form=form, fields=fields)
@ -96,10 +97,12 @@ def _proxy():
proxy_ip = flask.request.headers.get('X-Forwarded-By', flask.request.remote_addr)
ip = ipaddress.ip_address(proxy_ip)
if not any(ip in cidr for cidr in app.config['PROXY_AUTH_WHITELIST']):
flask.current_app.logger.error('Login failed by proxy - not on whitelist: from {client_ip} through {flask.request.remote_addr}.')
return flask.abort(500, '%s is not on PROXY_AUTH_WHITELIST' % proxy_ip)
email = flask.request.headers.get(app.config['PROXY_AUTH_HEADER'])
if not email:
flask.current_app.logger.error('Login failed by proxy - no header: from {client_ip} through {flask.request.remote_addr}.')
return flask.abort(500, 'No %s header' % app.config['PROXY_AUTH_HEADER'])
url = _has_usable_redirect(True) or app.config['WEB_ADMIN']
@ -108,9 +111,11 @@ def _proxy():
if user:
flask.session.regenerate()
flask_login.login_user(user)
flask.current_app.logger.info(f'Login succeeded by proxy created user: {user} from {client_ip} through {flask.request.remote_addr}.')
return flask.redirect(url)
if not app.config['PROXY_AUTH_CREATE']:
flask.current_app.logger.warning('Login failed by proxy - does not exist: {user} from {client_ip} through {flask.request.remote_addr}.')
return flask.abort(500, 'You don\'t exist. Go away! (%s)' % email)
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)

View File

@ -16,6 +16,7 @@
{%- call macros.table(order='[[1,"asc"]]') %}
<thead>
<tr>
<th>{% trans %}ID{% endtrans %}</th>
<th data-orderable="false">{% trans %}Actions{% endtrans %}</th>
<th>{% trans %}Comment{% endtrans %}</th>
<th>{% trans %}Authorized IP{% endtrans %}</th>
@ -26,6 +27,7 @@
<tbody>
{%- for token in user.tokens %}
<tr>
<td>{{ token.id }}</td>
<td>
<a href="{{ url_for('.token_delete', token_id=token.id) }}" title="{% trans %}Delete{% endtrans %}"><i class="fa fa-trash"></i></a>
</td>

View File

@ -532,3 +532,6 @@ def is_app_token(candidate):
if len(candidate) == 32 and all(c in string.hexdigits[:-6] for c in candidate):
return True
return False
def truncated_pw_hash(pw):
return hmac.new(app.truncated_pw_key, bytearray(pw, 'utf-8'), 'sha256').hexdigest()[:6]

View File

@ -52,14 +52,15 @@ def test_DNS():
test_DNS()
cmdline = [
"gunicorn",
"--threads", f"{os.cpu_count()}",
# If SUBNET6 is defined, gunicorn must listen on IPv6 as well as IPv4
"-b", f"{'[::]' if os.environ.get('SUBNET6') else ''}:80",
"gunicorn",
"--threads", f"{os.cpu_count()}",
# If SUBNET6 is defined, gunicorn must listen on IPv6 as well as IPv4
"-b", f"{'[::]' if os.environ.get('SUBNET6') else ''}:80",
"--logger-class mailu.Logger",
f"--log-level {os.environ.get('LOG_LEVEL', 'INFO')}",
"--worker-tmp-dir /dev/shm",
"--error-logfile", "-",
"--preload"
"--error-logfile", "-",
"--preload"
]
# logging

View File

@ -348,6 +348,7 @@ mail {
protocol smtp;
smtp_auth plain;
auth_http_header Auth-Port 10025;
auth_http_header Client-Port $remote_port;
}
# Default IMAP server for the webmail (no encryption, but authentication)
@ -356,6 +357,7 @@ mail {
protocol imap;
smtp_auth plain;
auth_http_header Auth-Port 10143;
auth_http_header Client-Port $remote_port;
# ensure we talk HAPROXY protocol to the backends
proxy_protocol on;
}
@ -381,6 +383,7 @@ mail {
protocol smtp;
smtp_auth none;
auth_http_header Auth-Port 25;
auth_http_header Client-Port $remote_port;
}
# All other protocols are disabled if TLS is failing
@ -396,6 +399,7 @@ mail {
protocol imap;
imap_auth plain;
auth_http_header Auth-Port 143;
auth_http_header Client-Port $remote_port;
# ensure we talk HAPROXY protocol to the backends
proxy_protocol on;
}
@ -411,6 +415,7 @@ mail {
protocol pop3;
pop3_auth plain;
auth_http_header Auth-Port 110;
auth_http_header Client-Port $remote_port;
# ensure we talk HAPROXY protocol to the backends
proxy_protocol on;
}
@ -426,6 +431,7 @@ mail {
protocol smtp;
smtp_auth plain login;
auth_http_header Auth-Port 587;
auth_http_header Client-Port $remote_port;
}
{% if TLS %}
@ -437,6 +443,7 @@ mail {
protocol smtp;
smtp_auth plain login;
auth_http_header Auth-Port 465;
auth_http_header Client-Port $remote_port;
}
server {
@ -447,6 +454,7 @@ mail {
protocol imap;
imap_auth plain;
auth_http_header Auth-Port 993;
auth_http_header Client-Port $remote_port;
# ensure we talk HAPROXY protocol to the backends
proxy_protocol on;
}
@ -459,6 +467,7 @@ mail {
protocol pop3;
pop3_auth plain;
auth_http_header Auth-Port 995;
auth_http_header Client-Port $remote_port;
# ensure we talk HAPROXY protocol to the backends
proxy_protocol on;
}

View File

@ -1,6 +1,7 @@
# Default proxy setup
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_hide_header True-Client-IP;
proxy_hide_header CF-Connecting-IP;

View File

@ -0,0 +1 @@
Improve auth-related logging