diff --git a/conf/fail2ban/jail.local b/conf/fail2ban/jail.local index b9340e52..f306b59d 100644 --- a/conf/fail2ban/jail.local +++ b/conf/fail2ban/jail.local @@ -24,6 +24,15 @@ filter = dovecotimap findtime = 30 maxretry = 20 +[management-daemon] +enabled = true +filter = miab-management-daemon +port = http,https +logpath = /var/log/mailinabox.log +maxretry = 20 +findtime = 30 + [recidive] enabled = true maxretry = 10 + diff --git a/conf/fail2ban/miab-management-daemon.conf b/conf/fail2ban/miab-management-daemon.conf new file mode 100644 index 00000000..9f3fdb79 --- /dev/null +++ b/conf/fail2ban/miab-management-daemon.conf @@ -0,0 +1,12 @@ +# Fail2Ban filter Mail-in-a-Box management daemon + +[INCLUDES] + +before = common.conf + +[Definition] + +_daemon = mailinabox + +failregex = Failed login from ip +ignoreregex = diff --git a/management/daemon.py b/management/daemon.py index bf3c9134..e62fe07f 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -import os, os.path, re, json +import os, os.path, re, json, logging, logging.handlers from functools import wraps @@ -32,6 +32,19 @@ with open(os.path.join(os.path.dirname(me), "csr_country_codes.tsv")) as f: app = Flask(__name__, template_folder=os.path.abspath(os.path.join(os.path.dirname(me), "templates"))) +# Initialize the logger +# +# The logger wil automatically rotate the log if it gets to big, it will keep 3 old log files +# The log will contain timestap-level-message +logger = logging.getLogger('mailinabox') +fh = logging.handlers.RotatingFileHandler("/var/log/mailinabox.log", maxBytes=10240, backupCount=3) +fh.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')) +logger.addHandler(fh) +logger.setLevel(logging.INFO) + +# Log a line that the daemon was started +logger.info("Management daemon started") + # Decorator to protect views that require a user with 'admin' privileges. def authorized_personnel_only(viewfunc): @wraps(viewfunc) @@ -45,6 +58,9 @@ def authorized_personnel_only(viewfunc): privs = [] error = str(e) + # Write a line in the log recording the failed login + log_failed_login(request) + # Authorized to access an API view? if "admin" in privs: # Call view func. @@ -117,6 +133,9 @@ def me(): try: email, privs = auth_service.authenticate(request, env) except ValueError as e: + # Log the failed login + log_failed_login(request) + return json_response({ "status": "invalid", "reason": str(e), @@ -504,6 +523,9 @@ def munin(filename=""): if filename == "": filename = "index.html" return send_from_directory("/var/cache/munin/www", filename) +def log_failed_login(request): + logger.warning("Failed login from ip %s" % (request.headers.getlist("X-Forwarded-For")[0])) + # APP if __name__ == '__main__': diff --git a/setup/system.sh b/setup/system.sh index 1aeec458..cff423ce 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -232,5 +232,6 @@ cat conf/fail2ban/jail.local \ | sed "s/PUBLIC_IP/$PUBLIC_IP/g" \ > /etc/fail2ban/jail.local cp conf/fail2ban/dovecotimap.conf /etc/fail2ban/filter.d/dovecotimap.conf +cp conf/fail2ban/miab-management-daemon.conf /etc/fail2ban/filter.d/miab-management-daemon.conf restart_service fail2ban