mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-10-31 19:00:54 +00:00 
			
		
		
		
	Enable fail2ban for z-push and add a test for it
This commit is contained in:
		
							parent
							
								
									71b8b94276
								
							
						
					
					
						commit
						27dcb5d7ca
					
				
							
								
								
									
										15
									
								
								conf/fail2ban/filter.d/z-push.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								conf/fail2ban/filter.d/z-push.conf
									
									
									
									
									
										Normal file
									
								
							| @ -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 "<HOST>" can | ||||
| #          be used for standard IP/hostname matching and is only an alias for | ||||
| #          (?:::f{4,6}:)?(?P<host>[\w\-.^_]+) | ||||
| # Values:  TEXT | ||||
| # | ||||
| failregex = IP: <HOST> failed to authenticate user | ||||
| ignoreregex = | ||||
| [Init] | ||||
| journalmatch = _SYSTEMD_UNIT=fail2ban.service | ||||
| @ -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 | ||||
|  | ||||
| @ -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. | ||||
| 
 | ||||
|  | ||||
| @ -12,7 +12,7 @@ set +eu | ||||
| 
 | ||||
| MIAB_DIR=".." | ||||
| PYMAIL="./test_mail.py" | ||||
| 
 | ||||
| EDITCONF="../tools/editconf.py" | ||||
| 
 | ||||
| # options | ||||
| SKIP_REMOTE_SMTP_TESTS=no | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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,6 +139,7 @@ while len(input_lines) > 0: | ||||
| 		 | ||||
| 		# comment-out the existing line (also comment any folded lines) | ||||
| 		if is_comment is None: | ||||
| 			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 | ||||
| @ -135,6 +163,7 @@ while len(input_lines) > 0: | ||||
| 		 | ||||
| # Put any settings we didn't see at the end of the file, | ||||
| # except settings being cleared. | ||||
| 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) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user