docker-mailserver/test/mail_fail2ban.bats
Brennan Kinney 672e9cf19a
tests: Ensure excessive FD limits are avoided (#2730)
* tests: Ensure excessive FD limits are avoided

Processes that run as daemons (`postsrsd` and `fail2ban-server`) initialize by closing all FDs (File Descriptors).

This behaviour queries that maximum limit and iterates through the entire range even if only a few FDs are open. In some environments (Docker, limit configured by distro) this can be a range exceeding 1 billion (from kernel default of 1024 soft, 4096 hard), causing an 8 minute delay with heavy CPU activity.

`postsrsd` has since been updated to use `close_range()` syscall, and `fail2ban` will now iterate through `/proc/self/fd` (open FDs) which should resolve the performance hit. Until those updates reach our Docker image, we need to workaround it with `--ulimit` option.

NOTE: If `docker.service` on a distro sets `LimitNOFILE=` to approx 1 million or lower, it should not be an issue. On distros such as Fedora 36, it is `LimitNOFILE=infinity` (approx 1 billion) that causes excessive delays.

* chore: Use Docker host limits instead

Typically on modern distros with systemd, this should equate to 1024 (soft) and 512K (hard) limits. A distro may override the built-in global defaults systemd sets via setting `DefaultLimitNOFILE=` in `/etc/systemd/user.conf` and `/etc/systemd/system.conf`.

* tests(fix): Better prevent non-deterministic failures

- `no_containers.bats` tests the external script `setup.sh` (without `-c`). It's expected that no existing DMS container is running  - otherwise it may attempt to use that container and fail. Detect this and fail early via `setup_file()` step.

- `mail_hostname.bats` had a odd timing failure with teardown due to the last tests bringing the containers down earlier (`docker stop` paired with the `docker run --rm`). Adding a moment of delay via `sleep` helps avoid that false positive scenario.
2022-08-23 11:24:23 +12:00

165 lines
5.9 KiB
Bash

load 'test_helper/common'
function setup_file() {
local PRIVATE_CONFIG
PRIVATE_CONFIG=$(duplicate_config_for_container .)
docker run --rm -d --name mail_fail2ban \
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \
-e ENABLE_FAIL2BAN=1 \
-e POSTSCREEN_ACTION=ignore \
--cap-add=NET_ADMIN \
--hostname mail.my-domain.com \
--tty \
--ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)" \
"${NAME}"
# Create a container which will send wrong authentications and should get banned
docker run --name fail-auth-mailer \
-e MAIL_FAIL2BAN_IP="$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' mail_fail2ban)" \
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test \
-d "${NAME}" \
tail -f /var/log/faillog
wait_for_finished_setup_in_container mail_fail2ban
}
function teardown_file() {
docker rm -f mail_fail2ban fail-auth-mailer
}
#
# processes
#
@test "checking process: fail2ban (fail2ban server enabled)" {
run docker exec mail_fail2ban /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/bin/python3 /usr/bin/fail2ban-server'"
assert_success
}
#
# fail2ban
#
@test "checking fail2ban: localhost is not banned because ignored" {
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client status postfix-sasl | grep 'IP list:.*127.0.0.1'"
assert_failure
run docker exec mail_fail2ban /bin/sh -c "grep 'ignoreip = 127.0.0.1/8' /etc/fail2ban/jail.conf"
assert_success
}
@test "checking fail2ban: fail2ban-fail2ban.cf overrides" {
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get loglevel | grep DEBUG"
assert_success
}
@test "checking fail2ban: fail2ban-jail.cf overrides" {
FILTERS=(dovecot postfix postfix-sasl)
for FILTER in "${FILTERS[@]}"; do
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get ${FILTER} bantime"
assert_output 1234
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get ${FILTER} findtime"
assert_output 321
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get ${FILTER} maxretry"
assert_output 2
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client -d | grep -F \"['set', 'dovecot', 'addaction', 'nftables-multiport']\""
assert_output "['set', 'dovecot', 'addaction', 'nftables-multiport']"
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client -d | grep -F \"['set', 'postfix', 'addaction', 'nftables-multiport']\""
assert_output "['set', 'postfix', 'addaction', 'nftables-multiport']"
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client -d | grep -F \"['set', 'postfix-sasl', 'addaction', 'nftables-multiport']\""
assert_output "['set', 'postfix-sasl', 'addaction', 'nftables-multiport']"
done
}
@test "checking fail2ban: ban ip on multiple failed login" {
# can't pipe the file as usual due to postscreen. (respecting postscreen_greet_wait time and talking in turn):
# shellcheck disable=SC1004
for _ in {1,2}
do
docker exec fail-auth-mailer /bin/bash -c \
'exec 3<>/dev/tcp/${MAIL_FAIL2BAN_IP}/25 && \
while IFS= read -r cmd; do \
head -1 <&3; \
[[ ${cmd} == "EHLO"* ]] && sleep 6; \
echo ${cmd} >&3; \
done < "/tmp/docker-mailserver-test/auth/smtp-auth-login-wrong.txt"'
done
sleep 5
FAIL_AUTH_MAILER_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' fail-auth-mailer)
# Checking that FAIL_AUTH_MAILER_IP is banned in mail_fail2ban
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client status postfix-sasl | grep '${FAIL_AUTH_MAILER_IP}'"
assert_success
# Checking that FAIL_AUTH_MAILER_IP is banned by nftables and blocktype set to DROP
run docker exec mail_fail2ban /bin/sh -c "nft list set inet f2b-table addr-set-postfix-sasl 2>/dev/null"
assert_output --regexp "${FAIL_AUTH_MAILER_IP}"
}
@test "checking fail2ban: unban ip works" {
FAIL_AUTH_MAILER_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' fail-auth-mailer)
docker exec mail_fail2ban fail2ban-client set postfix-sasl unbanip "${FAIL_AUTH_MAILER_IP}"
sleep 5
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client status postfix-sasl | grep 'IP list:.*${FAIL_AUTH_MAILER_IP}'"
assert_failure
# Checking that FAIL_AUTH_MAILER_IP is unbanned by nftables
run docker exec mail_fail2ban /bin/sh -c "nft list set inet f2b-table addr-set-postfix-sasl 2>/dev/null"
refute_output "${FAIL_AUTH_MAILER_IP}"
}
@test "checking fail2ban ban" {
run docker exec mail_fail2ban fail2ban ban 192.0.66.7
assert_success
assert_output "Banned custom IP: 1"
run docker exec mail_fail2ban fail2ban
assert_success
assert_output --regexp "Banned in custom:.*192\.0\.66\.7"
run docker exec mail_fail2ban fail2ban unban 192.0.66.7
assert_success
assert_output --partial "Unbanned IP from custom: 1"
}
@test "checking setup.sh: setup.sh fail2ban" {
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client set dovecot banip 192.0.66.4"
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client set dovecot banip 192.0.66.5"
sleep 10
run ./setup.sh -c mail_fail2ban fail2ban
assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.4'
assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.5'
run ./setup.sh -c mail_fail2ban fail2ban unban 192.0.66.4
assert_output --partial "Unbanned IP from dovecot: 1"
run ./setup.sh -c mail_fail2ban fail2ban
assert_output --regexp "^Banned in dovecot:.*192\.0\.66\.5"
run ./setup.sh -c mail_fail2ban fail2ban unban 192.0.66.5
assert_output --partial "Unbanned IP from dovecot: 1"
run ./setup.sh -c mail_fail2ban fail2ban unban
assert_output --partial "You need to specify an IP address: Run"
}
#
# supervisor
#
@test "checking restart of process: fail2ban (fail2ban server enabled)" {
run docker exec mail_fail2ban /bin/bash -c "pkill fail2ban && sleep 10 && ps aux --forest | grep -v grep | grep '/usr/bin/python3 /usr/bin/fail2ban-server'"
assert_success
}