diff --git a/conf/fail2ban/filter.d/z-push.conf b/conf/fail2ban/filter.d/z-push.conf new file mode 100644 index 00000000..190c2e63 --- /dev/null +++ b/conf/fail2ban/filter.d/z-push.conf @@ -0,0 +1,15 @@ +# Source: https://kb.kopano.io/display/ZP/Fail2Ban+support +[INCLUDES] +before = common.conf +[Definition] +# Option: failregex +# Notes.: regex to match the password failures messages in the logfile. The +# host must be matched by a group named "host". The tag "" can +# be used for standard IP/hostname matching and is only an alias for +# (?:::f{4,6}:)?(?P[\w\-.^_]+) +# Values: TEXT +# +failregex = IP: failed to authenticate user +ignoreregex = +[Init] +journalmatch = _SYSTEMD_UNIT=fail2ban.service diff --git a/conf/fail2ban/jails.conf b/conf/fail2ban/jails.conf index 44fb6022..4556d043 100644 --- a/conf/fail2ban/jails.conf +++ b/conf/fail2ban/jails.conf @@ -88,3 +88,10 @@ bantime = 3600 [slapd] enabled = true logpath = /var/log/ldap/slapd.log + +[z-push] +enabled = true +port = http,https +filter = z-push +logpath = /var/log/z-push/z-push-error.log +maxretry = 20 diff --git a/setup/zpush.sh b/setup/zpush.sh index 4fdfadc4..ecbbffc6 100755 --- a/setup/zpush.sh +++ b/setup/zpush.sh @@ -75,6 +75,7 @@ rm -f /usr/local/lib/z-push/autodiscover/config.php cp conf/zpush/autodiscover_config.php /usr/local/lib/z-push/autodiscover/config.php sed -i "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" /usr/local/lib/z-push/autodiscover/config.php sed -i "s^define('TIMEZONE', .*^define('TIMEZONE', '$(cat /etc/timezone)');^" /usr/local/lib/z-push/autodiscover/config.php +sed -i "s/define('LOGAUTHFAIL', .*/define('LOGAUTHFAIL', true);/" /usr/local/lib/z-push/config.php # Some directories it will use. diff --git a/tests/suites/_init_miabldap.sh b/tests/suites/_init_miabldap.sh index 5c2d4ef7..47298d95 100644 --- a/tests/suites/_init_miabldap.sh +++ b/tests/suites/_init_miabldap.sh @@ -12,7 +12,7 @@ set +eu MIAB_DIR=".." PYMAIL="./test_mail.py" - +EDITCONF="../tools/editconf.py" # options SKIP_REMOTE_SMTP_TESTS=no diff --git a/tests/suites/z-push.sh b/tests/suites/z-push.sh index 8c9057b0..3ea9dc77 100644 --- a/tests/suites/z-push.sh +++ b/tests/suites/z-push.sh @@ -2,7 +2,7 @@ test_zpush_logon() { - test_start "zpush-logon" + test_start "logon" # create regular user alice local alice="alice@somedomain.com" @@ -62,9 +62,143 @@ test_zpush_logon() { } +test_zpush_fail2ban() { + test_start "fail2ban" + + # create regular user with password "alice" + local alice="alice@somedomain.com" + local alice_pw="alice" + create_user "$alice" "$alice_pw" + + # The default fail2ban configuration ignores failed logins coming + # from our private ip and localhost. Change it so that it does not + # ignore the private ip in the z-push configuration only. Also + # change the allowed number of failures to a lower value to speed + # up the tests. + + record "[override default fail2ban options]" + local fail2ban_conf_temp="/tmp/runner_zpush_fail2ban.conf" + if [ -e "$fail2ban_conf_temp" ]; then + # if this test was somehow interrupted, the temp still exists + record "1. restore /etc/fail2ban/jail.d/mailinabox.conf" + cp "$fail2ban_conf_temp" "/etc/fail2ban/jail.d/mailinabox.conf" 1>>$TEST_OF 2>&1 || test_failure "Unable to setup test - could not restore fail2ban config" + else + record "1. duplicate /etc/fail2ban/jail.d/mailinabox.conf" + cp --no-clobber /etc/fail2ban/jail.d/mailinabox.conf $fail2ban_conf_temp 1>>$TEST_OF 2>&1 || test_failure "Unable to setup test - could not copy fail2ban config" + fi + + if ! have_test_failures; then + record "2. edit /etc/fail2ban/jail.d/mailinabox.conf" + $EDITCONF /etc/fail2ban/jail.d/mailinabox.conf \ + -ini-section z-push \ + "ignoreip=127.0.0.1/8 ::1" \ + "maxretry=5" >>$TEST_OF 2>&1 || + test_failure "Unable to setup test - changing fail2ban config failed" + if ! have_test_failures; then + record "3. reload fail2ban" + systemctl reload fail2ban >>$TEST_OF 2>&1 || test_failure "Unable to setup test - reloading fail2ban failed" + fi + + # reset fail2ban - unban all + if ! have_test_failures; then + record "4. unban all" + fail2ban-client unban --all >>$TEST_OF 2>&1 || + test_failure "Unable to setup test - executing unban --all failed" + fi + fi + + if have_test_failures; then + test_end + return + fi + + + # log in a bunch of times with wrong password + local devid="device1" + local devtype="iPhone" + local n=0 t1 t2 t + local total=10 + local banned=no + local code=0 + + start_log_capture + + record "[log in $total times with wrong password]" + while ! have_test_failures && [ $n -lt $total ]; do + t1=$(date +%s) + rest_urlencoded POST "https://$PRIVATE_IP/Microsoft-Server-ActiveSync?Cmd=Ping&DeviceId=$devid&DeviceType=$devtype" "$alice" "bad-alice" --insecure 2>>$TEST_OF + code=$? + t2=$(date +%s) + let t="$t2 - $t1" + record "TRY $n (${t}s): result code $code" + if [ $code -eq 0 ]; then + test_failure "Unexpected logon success" + continue + elif grep -F 'code 7' <<<"$REST_ERROR" >/dev/null; then + # curl error for connection refused + record "BANNED!" + banned=yes + break + elif [ $REST_HTTP_CODE -eq 401 ]; then + # assume a logon failure, reset log monitor + check_logs false zpush nginx_access + start_log_capture + else + test_failure "Error in REST call to z-push: $REST_ERROR" + assert_check_logs zpush nginx_access + continue + fi + record "$REST_OUTPUT" + let n+=1 + done + + if ! have_test_failures; then + if [ "$banned" == "no" ]; then + test_failure "Multiple failed logons did not ban ip" + + else + record "[logging in with correct password should also fail]" + rest_urlencoded POST "https://$PRIVATE_IP/Microsoft-Server-ActiveSync?Cmd=Ping&DeviceId=$devid&DeviceType=$devtype" "$alice" "$alice_pw" --insecure 2>>$TEST_OF + code=$? + record "result: $code" + if [ $code -eq 0 ]; then + test_failure "Expected user logon to fail due to ban" + elif grep -F 'code 7' <<<"$REST_ERROR" >/dev/null; then + # curl error for connection refused + record "OK: banned: $REST_ERROR" + else + test_failure "Error in REST call to z-push: $REST_ERROR" + fi + fi + fi + + # delete alice + delete_user "$alice" + + # reset fail2ban + record "[reset fail2ban config changes]" + record "restore /etc/fail2ban/jail.d/mailinabox.conf" + cp $fail2ban_conf_temp /etc/fail2ban/jail.d/mailinabox.conf + if [ $? -ne 0 ]; then + test_failure "Unable to restore fail2ban config" + else + systemctl reload fail2ban >>$TEST_OF 2>&1 || + test_failure "Unable reload fail2ban" + fi + rm -f $fail2ban_conf_temp + + fail2ban-client unban --all >>$TEST_OF 2>&1 || + test_failure "Unable to execute unban --all" + + # done + test_end +} + + suite_start "z-push" zpush_start test_zpush_logon +test_zpush_fail2ban suite_end zpush_end diff --git a/tools/editconf.py b/tools/editconf.py index c641a3af..caae003d 100755 --- a/tools/editconf.py +++ b/tools/editconf.py @@ -40,9 +40,11 @@ settings = sys.argv[2:] delimiter = "=" delimiter_re = r"\s*=\s*" erase_setting = False +erase_setting_via_comment = True comment_char = "#" folded_lines = False testing = False +ini_section = None while settings[0][0] == "-" and settings[0] != "--": opt = settings.pop(0) if opt == "-s": @@ -52,12 +54,18 @@ while settings[0][0] == "-" and settings[0] != "--": elif opt == "-e": # Erase settings that have empty values. erase_setting = True + elif opt == "-E": + # Erase settings (remove from file) that have empty values. + erase_setting = True + erase_setting_via_comment = False elif opt == "-w": # Line folding is possible in this file. folded_lines = True elif opt == "-c": # Specifies a different comment character. comment_char = settings.pop(0) + elif opt == "-ini-section": + ini_section = settings.pop(0) elif opt == "-t": testing = True else: @@ -77,6 +85,7 @@ for setting in settings: found = set() buf = "" input_lines = list(open(filename)) +cur_section = None while len(input_lines) > 0: line = input_lines.pop(0) @@ -87,6 +96,24 @@ while len(input_lines) > 0: while len(input_lines) > 0 and input_lines[0][0] in " \t": line += input_lines.pop(0) + # If an ini file, keep track of what section we're in + if ini_section and line.startswith('[') and line.strip().endswith(']'): + if cur_section == ini_section.lower(): + # Put any settings we didn't see at the end of the section. + for i in range(len(settings)): + if i not in found: + name, val = settings[i].split("=", 1) + if not (not val and erase_setting): + buf += name + delimiter + val + "\n" + cur_section = line.strip()[1:-1].strip().lower() + buf += line + continue + + if ini_section and cur_section != ini_section.lower(): + # we're not processing the desired section, just append + buf += line + continue + # See if this line is for any settings passed on the command line. for i in range(len(settings)): # Check if this line contain this setting from the command-line arguments. @@ -112,7 +139,8 @@ while len(input_lines) > 0: # comment-out the existing line (also comment any folded lines) if is_comment is None: - buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n" + if val or not erase_setting or erase_setting_via_comment: + buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n" else: # the line is already commented, pass it through buf += line @@ -135,11 +163,12 @@ while len(input_lines) > 0: # Put any settings we didn't see at the end of the file, # except settings being cleared. -for i in range(len(settings)): - if (i not in found): - name, val = settings[i].split("=", 1) - if not (not val and erase_setting): - buf += name + delimiter + val + "\n" +if not ini_section or cur_section == ini_section.lower(): + for i in range(len(settings)): + if (i not in found): + name, val = settings[i].split("=", 1) + if not (not val and erase_setting): + buf += name + delimiter + val + "\n" if not testing: # Write out the new file.