tests: Adjust parallel tests

- The usual serial to parallel test conversion to utilize the `setup.bash` common setup structure, and adding a `TEST_PREFIX` var for each test case to leverage.
- Standardize on parallel test naming conventions for variables / values.
- More consistent use of `bash -c` instead of `/bin/bash -c` or `/bin/sh -c`.
- Using the `_run_in_container` helper instead of `run docker exec ${CONTAINER_NAME}`.
- Updates tests to use the `check_if_process_is_running` helper.

---

chore: Revise inline docs for the `ssl_letsencrypt` test

- Moves the override to be in closer proximity to the `initial_setup` call, and better communicates the intent to override.
- Removes top comment block that is no longer providing value or correct information to maintainers.
- Revised `acme.json` test case inline doc comments.
This commit is contained in:
Brennan Kinney 2023-01-03 18:58:09 +13:00
parent 306592fcad
commit 2ec6c4abc0
17 changed files with 623 additions and 591 deletions

View File

@ -30,10 +30,10 @@ function _default_teardown() {
# @param ${1} program name [REQUIRED] # @param ${1} program name [REQUIRED]
# @param ${2} container name [IF UNSET: ${CONTAINER_NAME}] # @param ${2} container name [IF UNSET: ${CONTAINER_NAME}]
function _check_if_process_is_running() { function check_if_process_is_running() {
local PROGRAM_NAME=${1:?Program name must be provided explicitly} local PROGRAM_NAME=${1:?Program name must be provided explicitly}
local CONTAINER_NAME=${2:-${CONTAINER_NAME}} local CONTAINER_NAME=${2:-${CONTAINER_NAME}}
_run_in_container_explicit "${CONTAINER_NAME}" pgrep "${PROGRAM_NAME}" docker exec "${CONTAINER_NAME}" pgrep "${PROGRAM_NAME}"
} }
# ------------------------------------------------------------------- # -------------------------------------------------------------------

View File

@ -1,15 +1,15 @@
load "${REPOSITORY_ROOT}/test/helper/setup" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
export TEST_NAME_PREFIX='default relay host:' TEST_NAME_PREFIX='[Relay] ENV:'
export CONTAINER_NAME='dms-test-default_relay_host' CONTAINER_NAME='dms-test_default-relay-host'
function setup_file() { function setup_file() {
init_with_defaults init_with_defaults
local CUSTOM_SETUP_ARGUMENTS=( local CUSTOM_SETUP_ARGUMENTS=(
--env DEFAULT_RELAY_HOST=default.relay.host.invalid:25 \ --env DEFAULT_RELAY_HOST=default.relay.host.invalid:25
--env PERMIT_DOCKER=host \ --env PERMIT_DOCKER=host
) )
common_container_setup 'CUSTOM_SETUP_ARGUMENTS' common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
@ -17,7 +17,7 @@ function setup_file() {
function teardown_file() { _default_teardown ; } function teardown_file() { _default_teardown ; }
@test "${TEST_NAME_PREFIX} default relay host is added to main.cf" { @test "${TEST_NAME_PREFIX} 'DEFAULT_RELAY_HOST' should configure 'main.cf:relayhost'" {
_run_in_container bash -c 'grep -e "^relayhost =" /etc/postfix/main.cf' _run_in_container bash -c 'grep -e "^relayhost =" /etc/postfix/main.cf'
assert_output 'relayhost = default.relay.host.invalid:25' assert_output 'relayhost = default.relay.host.invalid:25'
} }

View File

@ -2,7 +2,7 @@ load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='ClamAV:' TEST_NAME_PREFIX='ClamAV:'
CONTAINER_NAME='dms-test-clamav' CONTAINER_NAME='dms-test_clamav'
function setup_file() { function setup_file() {
init_with_defaults init_with_defaults
@ -35,7 +35,7 @@ function setup_file() {
function teardown_file() { _default_teardown ; } function teardown_file() { _default_teardown ; }
@test "${TEST_NAME_PREFIX} process clamd is running" { @test "${TEST_NAME_PREFIX} process clamd is running" {
_run_in_container bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/clamd'" run check_if_process_is_running 'clamd'
assert_success assert_success
} }
@ -66,6 +66,8 @@ function teardown_file() { _default_teardown ; }
} }
@test "${TEST_NAME_PREFIX} process clamd restarts when killed" { @test "${TEST_NAME_PREFIX} process clamd restarts when killed" {
_run_in_container bash -c "pkill clamd && sleep 10 && ps aux --forest | grep -v grep | grep '/usr/sbin/clamd'" _run_in_container pkill 'clamd'
assert_success assert_success
run_until_success_or_timeout 10 check_if_process_is_running 'clamd'
} }

View File

@ -1,48 +1,50 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
setup_file() { TEST_NAME_PREFIX='[ClamAV + SA] (disabled):'
local PRIVATE_CONFIG CONTAINER_NAME='dms-test_clamav-spamassasin_disabled'
PRIVATE_CONFIG=$(duplicate_config_for_container .)
docker run --rm -d --name mail_disabled_clamav_spamassassin \ function setup_file() {
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ init_with_defaults
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \
-e ENABLE_CLAMAV=0 \
-e ENABLE_SPAMASSASSIN=0 \
-e AMAVIS_LOGLEVEL=2 \
-h mail.my-domain.com -t "${NAME}"
# TODO: find a better way to know when we have waited long enough local CUSTOM_SETUP_ARGUMENTS=(
# for ClamAV to should have come up, if it were enabled --env ENABLE_AMAVIS=1
wait_for_smtp_port_in_container mail_disabled_clamav_spamassassin --env ENABLE_CLAMAV=0
docker exec mail_disabled_clamav_spamassassin /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/existing-user1.txt" --env ENABLE_SPAMASSASSIN=0
--env AMAVIS_LOGLEVEL=2
)
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
wait_for_smtp_port_in_container "${CONTAINER_NAME}"
_run_in_container bash -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/existing-user1.txt"
assert_success
wait_for_empty_mail_queue_in_container "${CONTAINER_NAME}"
} }
teardown_file() { function teardown_file() { _default_teardown ; }
docker rm -f mail_disabled_clamav_spamassassin
}
@test "checking process: ClamAV (ClamAV disabled by ENABLED_CLAMAV=0)" { @test "${TEST_NAME_PREFIX} ClamAV - should be disabled by ENV 'ENABLED_CLAMAV=0'" {
run docker exec mail_disabled_clamav_spamassassin /bin/bash -c "ps aux --forest | grep -v grep | grep '/usr/sbin/clamd'" run check_if_process_is_running 'clamd'
assert_failure assert_failure
} }
@test "checking spamassassin: should not be listed in amavis when disabled" { @test "${TEST_NAME_PREFIX} SA - Amavis integration should not be active" {
run docker exec mail_disabled_clamav_spamassassin /bin/sh -c "grep -i 'ANTI-SPAM-SA code' /var/log/mail/mail.log | grep 'NOT loaded'" _run_in_container /bin/sh -c "grep -i 'ANTI-SPAM-SA code' /var/log/mail/mail.log | grep 'NOT loaded'"
assert_success assert_success
} }
@test "checking ClamAV: should not be listed in amavis when disabled" { @test "${TEST_NAME_PREFIX} ClamAV - Amavis integration should not be active" {
run docker exec mail_disabled_clamav_spamassassin grep -i 'Found secondary av scanner ClamAV-clamscan' /var/log/mail/mail.log _run_in_container grep -i 'Found secondary av scanner ClamAV-clamscan' /var/log/mail/mail.log
assert_failure assert_failure
} }
@test "checking ClamAV: should not be called when disabled" { @test "${TEST_NAME_PREFIX} SA should not be called" {
run docker exec mail_disabled_clamav_spamassassin grep -i 'connect to /var/run/clamav/clamd.ctl failed' /var/log/mail/mail.log _run_in_container grep -i 'connect to /var/run/clamav/clamd.ctl failed' /var/log/mail/mail.log
assert_failure assert_failure
} }
@test "checking restart of process: ClamAV (ClamAV disabled by ENABLED_CLAMAV=0)" { @test "${TEST_NAME_PREFIX} ClamAV process should not be restarted when killed" {
run docker exec mail_disabled_clamav_spamassassin /bin/bash -c "pkill -f clamd && sleep 10 && ps aux --forest | grep -v grep | grep '/usr/sbin/clamd'" _run_in_container /bin/bash -c "pkill -f clamd && sleep 10 && ps aux --forest | grep -v grep | grep '/usr/sbin/clamd'"
assert_failure assert_failure
} }

View File

@ -1,61 +1,61 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
CONTAINER="mail_dnsbl_enabled" TEST_NAME_PREFIX='DNSBLs:'
CONTAINER2="mail_dnsbl_disabled"
CONTAINER1_NAME='dms-test_dnsbl_enabled'
CONTAINER2_NAME='dms-test_dnsbl_disabled'
function setup_file() { function setup_file() {
local PRIVATE_CONFIG local CONTAINER_NAME=${CONTAINER1_NAME}
PRIVATE_CONFIG=$(duplicate_config_for_container . "${CONTAINER}") local CUSTOM_SETUP_ARGUMENTS=(
--env ENABLE_DNSBL=1
)
init_with_defaults
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
wait_for_smtp_port_in_container "${CONTAINER_NAME}"
docker run --rm -d --name "${CONTAINER}" \ local CONTAINER_NAME=${CONTAINER2_NAME}
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ local CUSTOM_SETUP_ARGUMENTS=(
-e ENABLE_DNSBL=1 \ --env ENABLE_DNSBL=0
-h mail.my-domain.com \ )
-t "${NAME}" init_with_defaults
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
wait_for_smtp_port_in_container "${CONTAINER_NAME}"
}
docker run --rm -d --name "${CONTAINER2}" \ function teardown_file() {
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}"
-e ENABLE_DNSBL=0 \
-h mail.my-domain.com \
-t "${NAME}"
wait_for_smtp_port_in_container "${CONTAINER}"
wait_for_smtp_port_in_container "${CONTAINER2}"
} }
# ENABLE_DNSBL=1 # ENABLE_DNSBL=1
@test "checking enabled postfix DNS block list zen.spamhaus.org" { @test "${TEST_NAME_PREFIX} (enabled) Postfix DNS block list zen.spamhaus.org" {
run docker exec "${CONTAINER}" postconf smtpd_recipient_restrictions run docker exec "${CONTAINER1_NAME}" postconf smtpd_recipient_restrictions
assert_output --partial 'reject_rbl_client zen.spamhaus.org' assert_output --partial 'reject_rbl_client zen.spamhaus.org'
} }
@test "checking enabled postscreen DNS block lists --> postscreen_dnsbl_action" { @test "${TEST_NAME_PREFIX} (enabled) Postscreen DNS block lists -> postscreen_dnsbl_action" {
run docker exec "${CONTAINER}" postconf postscreen_dnsbl_action run docker exec "${CONTAINER1_NAME}" postconf postscreen_dnsbl_action
assert_output 'postscreen_dnsbl_action = enforce' assert_output 'postscreen_dnsbl_action = enforce'
} }
@test "checking enabled postscreen DNS block lists --> postscreen_dnsbl_sites" { @test "${TEST_NAME_PREFIX} (enabled) Postscreen DNS block lists -> postscreen_dnsbl_sites" {
run docker exec "${CONTAINER}" postconf postscreen_dnsbl_sites run docker exec "${CONTAINER1_NAME}" postconf postscreen_dnsbl_sites
assert_output 'postscreen_dnsbl_sites = zen.spamhaus.org=127.0.0.[2..11]*3 bl.mailspike.net=127.0.0.[2;14;13;12;11;10] b.barracudacentral.org*2 bl.spameatingmonkey.net=127.0.0.2 dnsbl.sorbs.net psbl.surriel.com list.dnswl.org=127.0.[0..255].0*-2 list.dnswl.org=127.0.[0..255].1*-3 list.dnswl.org=127.0.[0..255].[2..3]*-4' assert_output 'postscreen_dnsbl_sites = zen.spamhaus.org=127.0.0.[2..11]*3 bl.mailspike.net=127.0.0.[2;14;13;12;11;10] b.barracudacentral.org*2 bl.spameatingmonkey.net=127.0.0.2 dnsbl.sorbs.net psbl.surriel.com list.dnswl.org=127.0.[0..255].0*-2 list.dnswl.org=127.0.[0..255].1*-3 list.dnswl.org=127.0.[0..255].[2..3]*-4'
} }
# ENABLE_DNSBL=0 # ENABLE_DNSBL=0
@test "checking disabled postfix DNS block list zen.spamhaus.org" { @test "${TEST_NAME_PREFIX} (disabled) Postfix DNS block list zen.spamhaus.org" {
run docker exec "${CONTAINER2}" postconf smtpd_recipient_restrictions run docker exec "${CONTAINER2_NAME}" postconf smtpd_recipient_restrictions
refute_output --partial 'reject_rbl_client zen.spamhaus.org' refute_output --partial 'reject_rbl_client zen.spamhaus.org'
} }
@test "checking disabled postscreen DNS block lists --> postscreen_dnsbl_action" { @test "${TEST_NAME_PREFIX} (disabled) Postscreen DNS block lists -> postscreen_dnsbl_action" {
run docker exec "${CONTAINER2}" postconf postscreen_dnsbl_action run docker exec "${CONTAINER2_NAME}" postconf postscreen_dnsbl_action
assert_output 'postscreen_dnsbl_action = ignore' assert_output 'postscreen_dnsbl_action = ignore'
} }
@test "checking disabled postscreen DNS block lists --> postscreen_dnsbl_sites" { @test "${TEST_NAME_PREFIX} (disabled) Postscreen DNS block lists -> postscreen_dnsbl_sites" {
run docker exec "${CONTAINER2}" postconf postscreen_dnsbl_sites run docker exec "${CONTAINER2_NAME}" postconf postscreen_dnsbl_sites
assert_output 'postscreen_dnsbl_sites =' assert_output 'postscreen_dnsbl_sites ='
} }
# cleanup
function teardown_file() {
docker rm -f "${CONTAINER}" "${CONTAINER2}"
}

View File

@ -1,88 +1,90 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='Fail2Ban:'
CONTAINER1_NAME='dms-test_fail2ban'
CONTAINER2_NAME='dms-test_fail2ban_fail-auth-mailer'
function get_container2_ip() {
docker inspect --format '{{ .NetworkSettings.IPAddress }}' "${CONTAINER2_NAME}"
}
function setup_file() { function setup_file() {
local PRIVATE_CONFIG export CONTAINER_NAME
PRIVATE_CONFIG=$(duplicate_config_for_container .)
docker run --rm -d --name mail_fail2ban \ CONTAINER_NAME=${CONTAINER1_NAME}
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ local CUSTOM_SETUP_ARGUMENTS=(
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ --env ENABLE_FAIL2BAN=1
-e ENABLE_FAIL2BAN=1 \ --env POSTSCREEN_ACTION=ignore
-e POSTSCREEN_ACTION=ignore \ --cap-add=NET_ADMIN
--cap-add=NET_ADMIN \ --ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)"
--hostname mail.my-domain.com \ )
--tty \ init_with_defaults
--ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)" \ common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
"${NAME}" wait_for_smtp_port_in_container "${CONTAINER_NAME}"
# Create a container which will send wrong authentications and should get banned # Create a container which will send wrong authentications and should get banned
docker run --name fail-auth-mailer \ CONTAINER_NAME=${CONTAINER2_NAME}
-e MAIL_FAIL2BAN_IP="$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' mail_fail2ban)" \ local CUSTOM_SETUP_ARGUMENTS=(--env MAIL_FAIL2BAN_IP="$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${CONTAINER1_NAME})")
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test \ init_with_defaults
-d "${NAME}" \ common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
tail -f /var/log/faillog
wait_for_finished_setup_in_container mail_fail2ban # Set default implicit container fallback for helpers:
CONTAINER_NAME=${CONTAINER1_NAME}
} }
function teardown_file() { function teardown_file() {
docker rm -f mail_fail2ban fail-auth-mailer docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}"
} }
# @test "${TEST_NAME_PREFIX} Fail2Ban is running" {
# processes run check_if_process_is_running 'fail2ban-server'
#
@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 assert_success
} }
# @test "${TEST_NAME_PREFIX} localhost is not banned because ignored" {
# fail2ban _run_in_container fail2ban-client status postfix-sasl
# assert_success
refute_output --regexp '.*IP list:.*127\.0\.0\.1.*'
@test "checking fail2ban: localhost is not banned because ignored" { _run_in_container grep 'ignoreip = 127.0.0.1/8' /etc/fail2ban/jail.conf
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 assert_success
} }
@test "checking fail2ban: fail2ban-fail2ban.cf overrides" { @test "${TEST_NAME_PREFIX} fail2ban-fail2ban.cf overrides" {
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get loglevel | grep DEBUG" _run_in_container fail2ban-client get loglevel
assert_success assert_success
assert_output --partial 'DEBUG'
} }
@test "checking fail2ban: fail2ban-jail.cf overrides" { @test "${TEST_NAME_PREFIX} fail2ban-jail.cf overrides" {
FILTERS=(dovecot postfix postfix-sasl) for FILTER in 'dovecot' 'postfix' 'postfix-sasl'
do
for FILTER in "${FILTERS[@]}"; do _run_in_container fail2ban-client get "${FILTER}" bantime
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get ${FILTER} bantime"
assert_output 1234 assert_output 1234
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get ${FILTER} findtime" _run_in_container fail2ban-client get "${FILTER}" findtime
assert_output 321 assert_output 321
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client get ${FILTER} maxretry" _run_in_container fail2ban-client get "${FILTER}" maxretry
assert_output 2 assert_output 2
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client -d | grep -F \"['set', 'dovecot', 'addaction', 'nftables-multiport']\"" _run_in_container fail2ban-client -d
assert_output "['set', 'dovecot', 'addaction', 'nftables-multiport']" assert_output --partial "['set', 'dovecot', 'addaction', 'nftables-multiport']"
assert_output --partial "['set', 'postfix', 'addaction', 'nftables-multiport']"
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client -d | grep -F \"['set', 'postfix', 'addaction', 'nftables-multiport']\"" assert_output --partial "['set', 'postfix-sasl', '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 done
} }
@test "checking fail2ban: ban ip on multiple failed login" { # NOTE: This test case is fragile if other test cases were to be run concurrently
# can't pipe the file as usual due to postscreen. (respecting postscreen_greet_wait time and talking in turn): @test "${TEST_NAME_PREFIX} 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 # shellcheck disable=SC1004
for _ in {1,2} for _ in {1,2}
do do
docker exec fail-auth-mailer /bin/bash -c \ docker exec "${CONTAINER2_NAME}" /bin/bash -c \
'exec 3<>/dev/tcp/${MAIL_FAIL2BAN_IP}/25 && \ 'exec 3<>/dev/tcp/${MAIL_FAIL2BAN_IP}/25 && \
while IFS= read -r cmd; do \ while IFS= read -r cmd; do \
head -1 <&3; \ head -1 <&3; \
@ -93,108 +95,123 @@ function teardown_file() {
sleep 5 sleep 5
FAIL_AUTH_MAILER_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' fail-auth-mailer) # Checking that CONTAINER2_IP is banned in "${CONTAINER1_NAME}"
# Checking that FAIL_AUTH_MAILER_IP is banned in mail_fail2ban CONTAINER2_IP=$(get_container2_ip)
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client status postfix-sasl | grep '${FAIL_AUTH_MAILER_IP}'" _run_in_container fail2ban-client status postfix-sasl
assert_success assert_success
assert_output --partial "${CONTAINER2_IP}"
# Checking that FAIL_AUTH_MAILER_IP is banned by nftables # Checking that CONTAINER2_IP is banned by nftables
run docker exec mail_fail2ban /bin/sh -c "nft list set inet f2b-table addr-set-postfix-sasl" _run_in_container bash -c 'nft list set inet f2b-table addr-set-postfix-sasl'
assert_output --partial "elements = { ${FAIL_AUTH_MAILER_IP} }" assert_success
assert_output --partial "elements = { ${CONTAINER2_IP} }"
} }
@test "checking fail2ban: unban ip works" { @test "${TEST_NAME_PREFIX} unban ip works" {
FAIL_AUTH_MAILER_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' fail-auth-mailer) CONTAINER2_IP=$(get_container2_ip)
docker exec mail_fail2ban fail2ban-client set postfix-sasl unbanip "${FAIL_AUTH_MAILER_IP}" _run_in_container fail2ban-client set postfix-sasl unbanip "${CONTAINER2_IP}"
assert_success
sleep 5 sleep 5
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client status postfix-sasl | grep 'IP list:.*${FAIL_AUTH_MAILER_IP}'" # Checking that CONTAINER2_IP is unbanned in "${CONTAINER1_NAME}"
assert_failure _run_in_container fail2ban-client status postfix-sasl
assert_success
refute_output --partial "${CONTAINER2_IP}"
# Checking that FAIL_AUTH_MAILER_IP is unbanned by nftables # Checking that CONTAINER2_IP is unbanned by nftables
run docker exec mail_fail2ban /bin/sh -c "nft list set inet f2b-table addr-set-postfix-sasl" _run_in_container bash -c 'nft list set inet f2b-table addr-set-postfix-sasl'
refute_output --partial "${FAIL_AUTH_MAILER_IP}" refute_output --partial "${CONTAINER2_IP}"
} }
@test "checking fail2ban ban" { @test "${TEST_NAME_PREFIX} bans work properly (single IP)" {
# Ban single IP address _run_in_container fail2ban ban 192.0.66.7
run docker exec mail_fail2ban fail2ban ban 192.0.66.7
assert_success assert_success
assert_output "Banned custom IP: 1" assert_output 'Banned custom IP: 1'
run docker exec mail_fail2ban fail2ban _run_in_container fail2ban
assert_success assert_success
assert_output --regexp "Banned in custom:.*192\.0\.66\.7" assert_output --regexp 'Banned in custom:.*192\.0\.66\.7'
run docker exec mail_fail2ban nft list set inet f2b-table addr-set-custom _run_in_container nft list set inet f2b-table addr-set-custom
assert_success assert_success
assert_output --partial "elements = { 192.0.66.7 }" assert_output --partial 'elements = { 192.0.66.7 }'
run docker exec mail_fail2ban fail2ban unban 192.0.66.7 _run_in_container fail2ban unban 192.0.66.7
assert_success assert_success
assert_output --partial "Unbanned IP from custom: 1" assert_output --partial 'Unbanned IP from custom: 1'
run docker exec mail_fail2ban nft list set inet f2b-table addr-set-custom _run_in_container nft list set inet f2b-table addr-set-custom
refute_output --partial "192.0.66.7" refute_output --partial '192.0.66.7'
# Ban IP network
run docker exec mail_fail2ban fail2ban ban 192.0.66.0/24
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\.0/24"
run docker exec mail_fail2ban nft list set inet f2b-table addr-set-custom
assert_success
assert_output --partial "elements = { 192.0.66.0/24 }"
run docker exec mail_fail2ban fail2ban unban 192.0.66.0/24
assert_success
assert_output --partial "Unbanned IP from custom: 1"
run docker exec mail_fail2ban nft list set inet f2b-table addr-set-custom
refute_output --partial "192.0.66.0/24"
} }
@test "checking FAIL2BAN_BLOCKTYPE is really set to drop" { @test "${TEST_NAME_PREFIX} bans work properly (subnet)" {
run docker exec mail_fail2ban bash -c 'nft list table inet f2b-table' _run_in_container fail2ban ban 192.0.66.0/24
assert_success
assert_output 'Banned custom IP: 1'
_run_in_container fail2ban
assert_success
assert_output --regexp 'Banned in custom:.*192\.0\.66\.0/24'
_run_in_container nft list set inet f2b-table addr-set-custom
assert_success
assert_output --partial 'elements = { 192.0.66.0/24 }'
_run_in_container fail2ban unban 192.0.66.0/24
assert_success
assert_output --partial 'Unbanned IP from custom: 1'
_run_in_container nft list set inet f2b-table addr-set-custom
refute_output --partial '192.0.66.0/24'
}
@test "${TEST_NAME_PREFIX} FAIL2BAN_BLOCKTYPE is really set to drop" {
# ban IPs here manually so we can be sure something is inside the jails
for JAIL in dovecot postfix-sasl custom; do
_run_in_container fail2ban-client set "${JAIL}" banip 192.33.44.55
assert_success
done
_run_in_container nft list table inet f2b-table
assert_success assert_success
assert_output --partial 'tcp dport { 110, 143, 465, 587, 993, 995, 4190 } ip saddr @addr-set-dovecot drop' assert_output --partial 'tcp dport { 110, 143, 465, 587, 993, 995, 4190 } ip saddr @addr-set-dovecot drop'
assert_output --partial 'tcp dport { 25, 110, 143, 465, 587, 993, 995 } ip saddr @addr-set-postfix-sasl drop' assert_output --partial 'tcp dport { 25, 110, 143, 465, 587, 993, 995 } ip saddr @addr-set-postfix-sasl drop'
assert_output --partial 'tcp dport { 25, 110, 143, 465, 587, 993, 995, 4190 } ip saddr @addr-set-custom drop' assert_output --partial 'tcp dport { 25, 110, 143, 465, 587, 993, 995, 4190 } ip saddr @addr-set-custom drop'
# unban the IPs previously banned to get a clean state again
for JAIL in dovecot postfix-sasl custom; do
_run_in_container fail2ban-client set "${JAIL}" unbanip 192.33.44.55
assert_success
done
} }
@test "checking setup.sh: setup.sh fail2ban" { @test "${TEST_NAME_PREFIX} setup.sh fail2ban" {
run docker exec mail_fail2ban /bin/sh -c "fail2ban-client set dovecot banip 192.0.66.4" _run_in_container 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" _run_in_container fail2ban-client set dovecot banip 192.0.66.5
sleep 10 sleep 10
run ./setup.sh -c mail_fail2ban fail2ban # Originally: run ./setup.sh -c "${CONTAINER1_NAME}" fail2ban
_run_in_container setup fail2ban
assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.4' assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.4'
assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.5' assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.5'
run ./setup.sh -c mail_fail2ban fail2ban unban 192.0.66.4 _run_in_container setup fail2ban unban 192.0.66.4
assert_output --partial "Unbanned IP from dovecot: 1" assert_output --partial "Unbanned IP from dovecot: 1"
run ./setup.sh -c mail_fail2ban fail2ban _run_in_container setup fail2ban
assert_output --regexp "^Banned in dovecot:.*192\.0\.66\.5" assert_output --regexp '^Banned in dovecot:.*192\.0\.66\.5'
run ./setup.sh -c mail_fail2ban fail2ban unban 192.0.66.5 _run_in_container setup fail2ban unban 192.0.66.5
assert_output --partial "Unbanned IP from dovecot: 1" assert_output --partial 'Unbanned IP from dovecot: 1'
run ./setup.sh -c mail_fail2ban fail2ban unban _run_in_container setup fail2ban unban
assert_output --partial "You need to specify an IP address: Run" assert_output --partial 'You need to specify an IP address: Run'
} }
# @test "${TEST_NAME_PREFIX} restart of Fail2Ban" {
# supervisor _run_in_container pkill fail2ban
#
@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 assert_success
run_until_success_or_timeout 10 check_if_process_is_running 'fail2ban-server'
} }

View File

@ -1,92 +1,92 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='Postgrey (enabled):'
CONTAINER_NAME='dms-test_postgrey_enabled'
function setup_file() { function setup_file() {
local PRIVATE_CONFIG local CUSTOM_SETUP_ARGUMENTS=(
PRIVATE_CONFIG=$(duplicate_config_for_container .) --env ENABLE_DNSBL=1
--env ENABLE_POSTGREY=1
--env PERMIT_DOCKER=container
--env POSTGREY_AUTO_WHITELIST_CLIENTS=5
--env POSTGREY_DELAY=15
--env POSTGREY_MAX_AGE=35
--env POSTGREY_TEXT="Delayed by Postgrey"
)
docker run -d --name mail_with_postgrey \ init_with_defaults
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \
-e ENABLE_DNSBL=1 \
-e ENABLE_POSTGREY=1 \
-e PERMIT_DOCKER=container \
-e POSTGREY_AUTO_WHITELIST_CLIENTS=5 \
-e POSTGREY_DELAY=15 \
-e POSTGREY_MAX_AGE=35 \
-e POSTGREY_TEXT="Delayed by Postgrey" \
-h mail.my-domain.com -t "${NAME}"
# using postfix availability as start indicator, this might be insufficient for postgrey # Postfix needs to be ready on port 25 for nc usage below:
wait_for_smtp_port_in_container mail_with_postgrey wait_for_smtp_port_in_container "${CONTAINER_NAME}"
} }
function teardown_file() { function teardown_file() { _default_teardown ; }
docker rm -f mail_with_postgrey
}
@test "checking postgrey: /etc/postfix/main.cf correctly edited" { @test "${TEST_NAME_PREFIX} /etc/postfix/main.cf correctly edited" {
run docker exec mail_with_postgrey /bin/bash -c "grep -F 'zen.spamhaus.org=127.0.0.[2..11], check_policy_service inet:127.0.0.1:10023' /etc/postfix/main.cf | wc -l" _run_in_container bash -c "grep -F 'zen.spamhaus.org=127.0.0.[2..11], check_policy_service inet:127.0.0.1:10023' /etc/postfix/main.cf | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }
@test "checking postgrey: /etc/default/postgrey correctly edited and has the default values" { @test "${TEST_NAME_PREFIX} /etc/default/postgrey correctly edited and has the default values" {
run docker exec mail_with_postgrey /bin/bash -c "grep '^POSTGREY_OPTS=\"--inet=127.0.0.1:10023 --delay=15 --max-age=35 --auto-whitelist-clients=5\"$' /etc/default/postgrey | wc -l" _run_in_container bash -c "grep '^POSTGREY_OPTS=\"--inet=127.0.0.1:10023 --delay=15 --max-age=35 --auto-whitelist-clients=5\"$' /etc/default/postgrey | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
run docker exec mail_with_postgrey /bin/bash -c "grep '^POSTGREY_TEXT=\"Delayed by Postgrey\"$' /etc/default/postgrey | wc -l" _run_in_container bash -c "grep '^POSTGREY_TEXT=\"Delayed by Postgrey\"$' /etc/default/postgrey | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }
@test "checking process: postgrey (postgrey server enabled)" { @test "${TEST_NAME_PREFIX} Postgrey is running" {
run docker exec mail_with_postgrey /bin/bash -c "ps aux --forest | grep -v grep | grep 'postgrey'" run check_if_process_is_running 'postgrey'
assert_success assert_success
} }
@test "checking postgrey: there should be a log entry about a new greylisted e-mail user@external.tld in /var/log/mail/mail.log" { @test "${TEST_NAME_PREFIX} there should be a log entry about a new greylisted e-mail user@external.tld in /var/log/mail/mail.log" {
#editing the postfix config in order to ensure that postgrey handles the test e-mail. The other spam checks at smtpd_recipient_restrictions would interfere with it. #editing the postfix config in order to ensure that postgrey handles the test e-mail. The other spam checks at smtpd_recipient_restrictions would interfere with it.
run docker exec mail_with_postgrey /bin/sh -c "sed -ie 's/permit_sasl_authenticated.*policyd-spf,$//g' /etc/postfix/main.cf" _run_in_container bash -c "sed -ie 's/permit_sasl_authenticated.*policyd-spf,$//g' /etc/postfix/main.cf"
run docker exec mail_with_postgrey /bin/sh -c "sed -ie 's/reject_unauth_pipelining.*reject_unknown_recipient_domain,$//g' /etc/postfix/main.cf" _run_in_container bash -c "sed -ie 's/reject_unauth_pipelining.*reject_unknown_recipient_domain,$//g' /etc/postfix/main.cf"
run docker exec mail_with_postgrey /bin/sh -c "sed -ie 's/reject_rbl_client.*inet:127\.0\.0\.1:10023$//g' /etc/postfix/main.cf" _run_in_container bash -c "sed -ie 's/reject_rbl_client.*inet:127\.0\.0\.1:10023$//g' /etc/postfix/main.cf"
run docker exec mail_with_postgrey /bin/sh -c "sed -ie 's/smtpd_recipient_restrictions =/smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:10023/g' /etc/postfix/main.cf" _run_in_container bash -c "sed -ie 's/smtpd_recipient_restrictions =/smtpd_recipient_restrictions = check_policy_service inet:127.0.0.1:10023/g' /etc/postfix/main.cf"
_run_in_container postfix reload
run docker exec mail_with_postgrey /bin/sh -c "/etc/init.d/postfix reload" _run_in_container bash -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/postgrey.txt"
run docker exec mail_with_postgrey /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/postgrey.txt"
sleep 5 #ensure that the information has been written into the log sleep 5 #ensure that the information has been written into the log
run docker exec mail_with_postgrey /bin/bash -c "grep -i 'action=greylist.*user@external\.tld' /var/log/mail/mail.log | wc -l" _run_in_container bash -c "grep -i 'action=greylist.*user@external\.tld' /var/log/mail/mail.log | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }
@test "checking postgrey: there should be a log entry about the retried and passed e-mail user@external.tld in /var/log/mail/mail.log" { @test "${TEST_NAME_PREFIX} there should be a log entry about the retried and passed e-mail user@external.tld in /var/log/mail/mail.log" {
sleep 20 #wait 20 seconds so that postgrey would accept the message sleep 20 #wait 20 seconds so that postgrey would accept the message
run docker exec mail_with_postgrey /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/postgrey.txt" _run_in_container bash -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/postgrey.txt"
sleep 8 sleep 8
run docker exec mail_with_postgrey /bin/sh -c "grep -i 'action=pass, reason=triplet found.*user@external\.tld' /var/log/mail/mail.log | wc -l" _run_in_container bash -c "grep -i 'action=pass, reason=triplet found.*user@external\.tld' /var/log/mail/mail.log | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }
@test "checking postgrey: there should be a log entry about the whitelisted and passed e-mail user@whitelist.tld in /var/log/mail/mail.log" { @test "${TEST_NAME_PREFIX} there should be a log entry about the whitelisted and passed e-mail user@whitelist.tld in /var/log/mail/mail.log" {
run docker exec mail_with_postgrey /bin/sh -c "nc -w 8 0.0.0.0 10023 < /tmp/docker-mailserver-test/nc_templates/postgrey_whitelist.txt" _run_in_container bash -c "nc -w 8 0.0.0.0 10023 < /tmp/docker-mailserver-test/nc_templates/postgrey_whitelist.txt"
run docker exec mail_with_postgrey /bin/sh -c "grep -i 'action=pass, reason=client whitelist' /var/log/mail/mail.log | wc -l" _run_in_container bash -c "grep -i 'action=pass, reason=client whitelist' /var/log/mail/mail.log | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }
@test "checking postgrey: there should be a log entry about the whitelisted local and passed e-mail user@whitelistlocal.tld in /var/log/mail/mail.log" { @test "${TEST_NAME_PREFIX} there should be a log entry about the whitelisted local and passed e-mail user@whitelistlocal.tld in /var/log/mail/mail.log" {
run docker exec mail_with_postgrey /bin/sh -c "nc -w 8 0.0.0.0 10023 < /tmp/docker-mailserver-test/nc_templates/postgrey_whitelist_local.txt" _run_in_container bash -c "nc -w 8 0.0.0.0 10023 < /tmp/docker-mailserver-test/nc_templates/postgrey_whitelist_local.txt"
run docker exec mail_with_postgrey /bin/sh -c "grep -i 'action=pass, reason=client whitelist' /var/log/mail/mail.log | wc -l" _run_in_container bash -c "grep -i 'action=pass, reason=client whitelist' /var/log/mail/mail.log | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }
@test "checking postgrey: there should be a log entry about the whitelisted recipient user2@otherdomain.tld in /var/log/mail/mail.log" { @test "${TEST_NAME_PREFIX} there should be a log entry about the whitelisted recipient user2@otherdomain.tld in /var/log/mail/mail.log" {
run docker exec mail_with_postgrey /bin/sh -c "nc -w 8 0.0.0.0 10023 < /tmp/docker-mailserver-test/nc_templates/postgrey_whitelist_recipients.txt" _run_in_container bash -c "nc -w 8 0.0.0.0 10023 < /tmp/docker-mailserver-test/nc_templates/postgrey_whitelist_recipients.txt"
run docker exec mail_with_postgrey /bin/sh -c "grep -i 'action=pass, reason=recipient whitelist' /var/log/mail/mail.log | wc -l" _run_in_container bash -c "grep -i 'action=pass, reason=recipient whitelist' /var/log/mail/mail.log | wc -l"
assert_success assert_success
assert_output 1 assert_output 1
} }

View File

@ -1,44 +1,46 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
setup() { TEST_NAME_PREFIX='Postscreen:'
# Getting mail container IP CONTAINER1_NAME='dms-test_postscreen_enforce'
MAIL_POSTSCREEN_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' mail_postscreen) CONTAINER2_NAME='dms-test_postscreen_sender'
function setup() {
MAIL_POSTSCREEN_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "${CONTAINER1_NAME}")
} }
setup_file() { function setup_file() {
local PRIVATE_CONFIG local CONTAINER_NAME=${CONTAINER1_NAME}
PRIVATE_CONFIG=$(duplicate_config_for_container .) local CUSTOM_SETUP_ARGUMENTS=(
--env POSTSCREEN_ACTION=enforce
--cap-add=NET_ADMIN
)
init_with_defaults
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
wait_for_smtp_port_in_container "${CONTAINER_NAME}"
docker run -d --name mail_postscreen \ local CONTAINER_NAME=${CONTAINER2_NAME}
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ init_with_defaults
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ common_container_setup
-e POSTSCREEN_ACTION=enforce \ wait_for_smtp_port_in_container "${CONTAINER_NAME}"
--cap-add=NET_ADMIN \
-h mail.my-domain.com -t "${NAME}"
docker run --name mail_postscreen_sender \
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \
-d "${NAME}" \
tail -f /var/log/faillog
wait_for_smtp_port_in_container mail_postscreen
} }
teardown_file() { function teardown_file() {
docker rm -f mail_postscreen mail_postscreen_sender docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}"
} }
@test "checking postscreen: talk too fast" { @test "${TEST_NAME_PREFIX} talk too fast" {
docker exec mail_postscreen_sender /bin/sh -c "nc ${MAIL_POSTSCREEN_IP} 25 < /tmp/docker-mailserver-test/auth/smtp-auth-login.txt" run docker exec "${CONTAINER2_NAME}" /bin/sh -c "nc ${MAIL_POSTSCREEN_IP} 25 < /tmp/docker-mailserver-test/auth/smtp-auth-login.txt"
assert_success
repeat_until_success_or_timeout 10 run docker exec mail_postscreen grep 'COMMAND PIPELINING' /var/log/mail/mail.log repeat_until_success_or_timeout 10 run docker exec "${CONTAINER1_NAME}" grep 'COMMAND PIPELINING' /var/log/mail/mail.log
assert_success assert_success
} }
@test "checking postscreen: positive test (respecting postscreen_greet_wait time and talking in turn)" { @test "${TEST_NAME_PREFIX} positive test (respecting postscreen_greet_wait time and talking in turn)" {
for _ in {1,2}; do for _ in {1,2}; do
# shellcheck disable=SC1004 # shellcheck disable=SC1004
docker exec mail_postscreen_sender /bin/bash -c \ docker exec "${CONTAINER2_NAME}" /bin/bash -c \
'exec 3<>/dev/tcp/'"${MAIL_POSTSCREEN_IP}"'/25 && \ 'exec 3<>/dev/tcp/'"${MAIL_POSTSCREEN_IP}"'/25 && \
while IFS= read -r cmd; do \ while IFS= read -r cmd; do \
head -1 <&3; \ head -1 <&3; \
@ -47,6 +49,6 @@ teardown_file() {
done < "/tmp/docker-mailserver-test/auth/smtp-auth-login.txt"' done < "/tmp/docker-mailserver-test/auth/smtp-auth-login.txt"'
done done
repeat_until_success_or_timeout 10 run docker exec mail_postscreen grep 'PASS NEW ' /var/log/mail/mail.log repeat_until_success_or_timeout 10 run docker exec "${CONTAINER1_NAME}" grep 'PASS NEW ' /var/log/mail/mail.log
assert_success assert_success
} }

View File

@ -1,8 +1,8 @@
load "${REPOSITORY_ROOT}/test/helper/setup" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='spam (Amavis):' TEST_NAME_PREFIX='Spam bounced:'
CONTAINER_NAME='dms-test-spam_bounced' CONTAINER_NAME='dms-test_spam-bounced'
function setup_file() { function setup_file() {
init_with_defaults init_with_defaults

View File

@ -1,67 +1,67 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='Spam junk folder:'
CONTAINER1_NAME='dms-test_spam-junk-folder_1'
CONTAINER2_NAME='dms-test_spam-junk-folder_2'
function teardown() { _default_teardown ; }
# Test case # Test case
# --------- # ---------
# When SPAMASSASSIN_SPAM_TO_INBOX=1, spam messages must be delivered and eventually (MOVE_SPAM_TO_JUNK=1) moved to the Junk folder. # When SPAMASSASSIN_SPAM_TO_INBOX=1, spam messages must be delivered
# and eventually (MOVE_SPAM_TO_JUNK=1) moved to the Junk folder.
@test "checking amavis: spam message is delivered and moved to the Junk folder (MOVE_SPAM_TO_JUNK=1)" { @test "${TEST_NAME_PREFIX} (Amavis) spam message delivered & moved to Junk folder" {
local PRIVATE_CONFIG export CONTAINER_NAME=${CONTAINER1_NAME}
PRIVATE_CONFIG=$(duplicate_config_for_container . mail_spam_moved_junk) local CUSTOM_SETUP_ARGUMENTS=(
--env ENABLE_AMAVIS=1
docker run -d --name mail_spam_moved_junk \ --env ENABLE_SPAMASSASSIN=1
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ --env MOVE_SPAM_TO_JUNK=1
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ --env PERMIT_DOCKER=container
-e ENABLE_SPAMASSASSIN=1 \ --env SA_SPAM_SUBJECT="SPAM: "
-e MOVE_SPAM_TO_JUNK=1 \ --env SPAMASSASSIN_SPAM_TO_INBOX=1
-e PERMIT_DOCKER=container \ )
-e SA_SPAM_SUBJECT="SPAM: " \ init_with_defaults
-e SPAMASSASSIN_SPAM_TO_INBOX=1 \ common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
-h mail.my-domain.com -t "${NAME}" wait_for_smtp_port_in_container "${CONTAINER_NAME}"
teardown() { docker rm -f mail_spam_moved_junk; }
wait_for_smtp_port_in_container mail_spam_moved_junk
# send a spam message # send a spam message
run docker exec mail_spam_moved_junk /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/amavis-spam.txt" _run_in_container bash -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/amavis-spam.txt"
assert_success assert_success
# message will be added to a queue with varying delay until amavis receives it # message will be added to a queue with varying delay until amavis receives it
run repeat_until_success_or_timeout 60 sh -c "docker logs mail_spam_moved_junk | grep 'Passed SPAM {RelayedTaggedInbound,Quarantined}'" run repeat_until_success_or_timeout 60 bash -c "docker logs ${CONTAINER_NAME} | grep 'Passed SPAM {RelayedTaggedInbound,Quarantined}'"
assert_success assert_success
# spam moved to Junk folder # spam moved to Junk folder
run repeat_until_success_or_timeout 20 sh -c "docker exec mail_spam_moved_junk sh -c 'grep \"Subject: SPAM: \" /var/mail/localhost.localdomain/user1/.Junk/new/ -R'" run repeat_until_success_or_timeout 20 bash -c "docker exec ${CONTAINER_NAME} sh -c 'grep \"Subject: SPAM: \" /var/mail/localhost.localdomain/user1/.Junk/new/ -R'"
assert_success assert_success
} }
@test "checking amavis: spam message is delivered to INBOX (MOVE_SPAM_TO_JUNK=0)" { @test "${TEST_NAME_PREFIX} (Amavis) spam message delivered to INBOX" {
local PRIVATE_CONFIG export CONTAINER_NAME=${CONTAINER2_NAME}
PRIVATE_CONFIG=$(duplicate_config_for_container . mail_spam_moved_new) local CUSTOM_SETUP_ARGUMENTS=(
--env ENABLE_AMAVIS=1
docker run -d --name mail_spam_moved_new \ --env ENABLE_SPAMASSASSIN=1
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ --env MOVE_SPAM_TO_JUNK=0
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ --env PERMIT_DOCKER=container
-e ENABLE_SPAMASSASSIN=1 \ --env SA_SPAM_SUBJECT="SPAM: "
-e MOVE_SPAM_TO_JUNK=0 \ --env SPAMASSASSIN_SPAM_TO_INBOX=1
-e PERMIT_DOCKER=container \ )
-e SA_SPAM_SUBJECT="SPAM: " \ init_with_defaults
-e SPAMASSASSIN_SPAM_TO_INBOX=1 \ common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
-h mail.my-domain.com -t "${NAME}" wait_for_smtp_port_in_container "${CONTAINER_NAME}"
teardown() { docker rm -f mail_spam_moved_new; }
wait_for_smtp_port_in_container mail_spam_moved_new
# send a spam message # send a spam message
run docker exec mail_spam_moved_new /bin/sh -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/amavis-spam.txt" _run_in_container /bin/bash -c "nc 0.0.0.0 25 < /tmp/docker-mailserver-test/email-templates/amavis-spam.txt"
assert_success assert_success
# message will be added to a queue with varying delay until amavis receives it # message will be added to a queue with varying delay until amavis receives it
run repeat_until_success_or_timeout 60 sh -c "docker logs mail_spam_moved_new | grep 'Passed SPAM {RelayedTaggedInbound,Quarantined}'" run repeat_until_success_or_timeout 60 bash -c "docker logs ${CONTAINER_NAME} | grep 'Passed SPAM {RelayedTaggedInbound,Quarantined}'"
assert_success assert_success
# spam moved to INBOX # spam moved to INBOX
run repeat_until_success_or_timeout 20 sh -c "docker exec mail_spam_moved_new sh -c 'grep \"Subject: SPAM: \" /var/mail/localhost.localdomain/user1/new/ -R'" run repeat_until_success_or_timeout 20 bash -c "docker exec ${CONTAINER_NAME} sh -c 'grep \"Subject: SPAM: \" /var/mail/localhost.localdomain/user1/new/ -R'"
assert_success assert_success
} }

View File

@ -1,64 +1,61 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common"
function setup() { TEST_NAME_PREFIX='Undefined spam subject:'
local PRIVATE_CONFIG
PRIVATE_CONFIG=$(duplicate_config_for_container .) CONTAINER1_NAME='dms-test_spam-undef-subject_1'
docker run -d --name mail_undef_spam_subject \ CONTAINER2_NAME='dms-test_spam-undef-subject_2'
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ CONTAINER_NAME=${CONTAINER2_NAME}
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \
-e ENABLE_SPAMASSASSIN=1 \
-e SA_SPAM_SUBJECT="undef" \
--hostname mail.my-domain.com \
--tty \
"${NAME}"
CONTAINER='mail_undef_spam_subject_2' function setup_file() {
PRIVATE_CONFIG=$(duplicate_config_for_container . "${CONTAINER}") local CONTAINER_NAME=${CONTAINER1_NAME}
docker run -d \ local CUSTOM_SETUP_ARGUMENTS=(
-v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ --env ENABLE_AMAVIS=1
-v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ --env ENABLE_SPAMASSASSIN=1
-v "$(pwd)/test/onedir":/var/mail-state \ --env SA_SPAM_SUBJECT='undef'
-e ENABLE_CLAMAV=1 \ )
-e SPOOF_PROTECTION=1 \ init_with_defaults
-e ENABLE_SPAMASSASSIN=1 \ common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
-e REPORT_RECIPIENT=user1@localhost.localdomain \
-e REPORT_SENDER=report1@mail.my-domain.com \
-e SA_TAG=-5.0 \
-e SA_TAG2=2.0 \
-e SA_KILL=3.0 \
-e SA_SPAM_SUBJECT="SPAM: " \
-e VIRUSMAILS_DELETE_DELAY=7 \
-e ENABLE_SRS=1 \
-e ENABLE_MANAGESIEVE=1 \
-e PERMIT_DOCKER=host \
--name "${CONTAINER}" \
--hostname mail.my-domain.com \
--tty \
--ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)" \
"${NAME}"
wait_for_finished_setup_in_container mail_undef_spam_subject # ulimit required for `ENABLE_SRS=1`
wait_for_finished_setup_in_container "${CONTAINER}" local CONTAINER_NAME=${CONTAINER2_NAME}
local CUSTOM_SETUP_ARGUMENTS=(
--env ENABLE_CLAMAV=1
--env SPOOF_PROTECTION=1
--env ENABLE_SPAMASSASSIN=1
--env REPORT_RECIPIENT=user1@localhost.localdomain
--env REPORT_SENDER=report1@mail.my-domain.com
--env SA_TAG=-5.0
--env SA_TAG2=2.0
--env SA_KILL=3.0
--env SA_SPAM_SUBJECT="SPAM: "
--env VIRUSMAILS_DELETE_DELAY=7
--env ENABLE_SRS=1
--env ENABLE_MANAGESIEVE=1
--env PERMIT_DOCKER=host
--ulimit "nofile=$(ulimit -Sn):$(ulimit -Hn)"
)
init_with_defaults
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
} }
function teardown() { function teardown_file() {
docker rm -f mail_undef_spam_subject "${CONTAINER}" docker rm -f "${CONTAINER1_NAME}" "${CONTAINER2_NAME}"
} }
@test "checking spamassassin: docker env variables are set correctly (custom)" { @test "${TEST_NAME_PREFIX} Docker env variables are set correctly (custom)" {
run docker exec "${CONTAINER}" /bin/sh -c "grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= -5.0'" _run_in_container bash -c "grep '\$sa_tag_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= -5.0'"
assert_success assert_success
run docker exec "${CONTAINER}" /bin/sh -c "grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'" _run_in_container bash -c "grep '\$sa_tag2_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 2.0'"
assert_success assert_success
run docker exec "${CONTAINER}" /bin/sh -c "grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 3.0'" _run_in_container bash -c "grep '\$sa_kill_level_deflt' /etc/amavis/conf.d/20-debian_defaults | grep '= 3.0'"
assert_success assert_success
run docker exec "${CONTAINER}" /bin/sh -c "grep '\$sa_spam_subject_tag' /etc/amavis/conf.d/20-debian_defaults | grep '= .SPAM: .'" _run_in_container bash -c "grep '\$sa_spam_subject_tag' /etc/amavis/conf.d/20-debian_defaults | grep '= .SPAM: .'"
assert_success assert_success
run docker exec mail_undef_spam_subject /bin/sh -c "grep '\$sa_spam_subject_tag' /etc/amavis/conf.d/20-debian_defaults | grep '= undef'" run docker exec "${CONTAINER1_NAME}" bash -c "grep '\$sa_spam_subject_tag' /etc/amavis/conf.d/20-debian_defaults | grep '= undef'"
assert_success assert_success
} }

View File

@ -6,7 +6,7 @@ load "${REPOSITORY_ROOT}/test/helper/common"
# ? to identify the test easily # ? to identify the test easily
TEST_NAME_PREFIX='template:' TEST_NAME_PREFIX='template:'
# ? must be unique # ? must be unique
CONTAINER_NAME='dms-test-template' CONTAINER_NAME='dms-test_template'
# ? test setup # ? test setup

View File

@ -1,81 +1,128 @@
#!/usr/bin/env bats load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
# Globals ${BATS_TMPDIR} and ${NAME}
# `${NAME}` defaults to `mailserver-testing:ci`
function teardown() { TEST_NAME_PREFIX='[Security] TLS (cipher lists):'
docker rm -f tls_test_cipherlists CONTAINER_PREFIX='dms-test_tls-cipherlists'
}
# NOTE: Tests cases here cannot be run concurrently:
# - The `testssl.txt` file configures `testssl.sh` to connect to `example.test` (TEST_DOMAIN)
# and this is set as a network alias to the DMS container being tested.
# - If multiple containers are active with this alias, the connection is not deterministic and will result
# in comparing the wrong results for a given variant.
function setup_file() { function setup_file() {
export DOMAIN="example.test" export TEST_DOMAIN='example.test'
export NETWORK="test-network" export TEST_FQDN="mail.${TEST_DOMAIN}"
export TEST_NETWORK='test-network'
# Shared config for TLS testing (read-only) # Contains various certs for testing TLS support (read-only):
export TLS_CONFIG_VOLUME export TLS_CONFIG_VOLUME
TLS_CONFIG_VOLUME="$(pwd)/test/test-files/ssl/${DOMAIN}/:/config/ssl/:ro" TLS_CONFIG_VOLUME="${PWD}/test/test-files/ssl/${TEST_DOMAIN}/:/config/ssl/:ro"
# `${BATS_TMPDIR}` maps to `/tmp`
export TLS_RESULTS_DIR="${BATS_TMPDIR}/results"
# NOTE: If the network already exists, test will fail to start. # Used for connecting testssl and DMS containers via network name `TEST_DOMAIN`:
docker network create "${NETWORK}" # NOTE: If the network already exists, the test will fail to start
docker network create "${TEST_NETWORK}"
# Copies all of `./test/config/` to specific directory for testing
# `${PRIVATE_CONFIG}` becomes `$(pwd)/test/duplicate_configs/<bats test filename>`
export PRIVATE_CONFIG
PRIVATE_CONFIG=$(duplicate_config_for_container .)
# Pull `testssl.sh` image in advance to avoid it interfering with the `run` captured output. # Pull `testssl.sh` image in advance to avoid it interfering with the `run` captured output.
# Only interferes (potential test failure) with `assert_output` not `assert_success`? # Only interferes (potential test failure) with `assert_output` not `assert_success`?
docker pull drwetter/testssl.sh:3.1dev docker pull drwetter/testssl.sh:3.1dev
# Only used in `should_support_expected_cipherlists()` to set a storage location for `testssl.sh` JSON output:
# `${BATS_TMPDIR}` maps to `/tmp`: https://bats-core.readthedocs.io/en/v1.8.2/writing-tests.html#special-variables
export TLS_RESULTS_DIR="${BATS_TMPDIR}/results"
} }
function teardown_file() { function teardown_file() {
docker network rm "${NETWORK}" docker network rm "${TEST_NETWORK}"
} }
@test "checking tls: cipher list - rsa intermediate" { function teardown() { _default_teardown ; }
check_ports 'rsa' 'intermediate'
@test "${TEST_NAME_PREFIX} 'TLS_LEVEL=intermediate' + RSA" {
configure_and_run_dms_container 'intermediate' 'rsa'
should_support_expected_cipherlists
} }
@test "checking tls: cipher list - rsa modern" { @test "${TEST_NAME_PREFIX} 'TLS_LEVEL=intermediate' + ECDSA" {
check_ports 'rsa' 'modern' configure_and_run_dms_container 'intermediate' 'ecdsa'
should_support_expected_cipherlists
} }
@test "checking tls: cipher list - ecdsa intermediate" { # Only ECDSA with an RSA fallback is tested.
check_ports 'ecdsa' 'intermediate' # There isn't a situation where RSA with an ECDSA fallback would make sense.
@test "${TEST_NAME_PREFIX} 'TLS_LEVEL=intermediate' + ECDSA with RSA fallback" {
configure_and_run_dms_container 'intermediate' 'ecdsa' 'rsa'
should_support_expected_cipherlists
} }
@test "checking tls: cipher list - ecdsa modern" { @test "${TEST_NAME_PREFIX} 'TLS_LEVEL=modern' + RSA" {
check_ports 'ecdsa' 'modern' configure_and_run_dms_container 'modern' 'rsa'
should_support_expected_cipherlists
} }
@test "${TEST_NAME_PREFIX} 'TLS_LEVEL=modern' + ECDSA" {
# Only ECDSA with RSA fallback is tested. configure_and_run_dms_container 'modern' 'ecdsa'
# There isn't a situation where RSA with ECDSA fallback would make sense. should_support_expected_cipherlists
@test "checking tls: cipher list - ecdsa intermediate, with rsa fallback" {
check_ports 'ecdsa' 'intermediate' 'rsa'
} }
@test "checking tls: cipher list - ecdsa modern, with rsa fallback" { @test "${TEST_NAME_PREFIX} 'TLS_LEVEL=modern' + ECDSA with RSA fallback" {
check_ports 'ecdsa' 'modern' 'rsa' configure_and_run_dms_container 'modern' 'ecdsa' 'rsa'
should_support_expected_cipherlists
} }
function check_ports() { function configure_and_run_dms_container() {
local KEY_TYPE=$1 local TLS_LEVEL=$1
local TLS_LEVEL=$2 local KEY_TYPE=$2
local ALT_KEY_TYPE=$3 # Optional parameter local ALT_KEY_TYPE=$3 # Optional parameter
local KEY_TYPE_LABEL="${KEY_TYPE}" export TEST_VARIANT="${TLS_LEVEL}-${KEY_TYPE}"
# This is just to add a `_` delimiter between the two key types for readability
if [[ -n ${ALT_KEY_TYPE} ]] if [[ -n ${ALT_KEY_TYPE} ]]
then then
KEY_TYPE_LABEL="${KEY_TYPE}_${ALT_KEY_TYPE}" TEST_VARIANT+="-${ALT_KEY_TYPE}"
fi fi
local RESULTS_PATH="${KEY_TYPE_LABEL}/${TLS_LEVEL}"
collect_cipherlist_data export CONTAINER_NAME="${CONTAINER_PREFIX}_${TEST_VARIANT}"
# The initial set of args is static across test cases:
local CUSTOM_SETUP_ARGUMENTS=(
--volume "${TLS_CONFIG_VOLUME}"
--network "${TEST_NETWORK}"
--network-alias "${TEST_DOMAIN}"
--env ENABLE_POP3=1
--env SSL_TYPE="manual"
)
# The remaining args are dependent upon test case vars:
CUSTOM_SETUP_ARGUMENTS+=(
--env TLS_LEVEL="${TLS_LEVEL}"
--env SSL_CERT_PATH="/config/ssl/cert.${KEY_TYPE}.pem"
--env SSL_KEY_PATH="/config/ssl/key.${KEY_TYPE}.pem"
)
if [[ -n ${ALT_KEY_TYPE} ]]
then
CUSTOM_SETUP_ARGUMENTS+=(
--env SSL_ALT_CERT_PATH="/config/ssl/cert.${ALT_KEY_TYPE}.pem"
--env SSL_ALT_KEY_PATH="/config/ssl/key.${ALT_KEY_TYPE}.pem"
)
fi
init_with_defaults
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
wait_for_smtp_port_in_container "${CONTAINER_NAME}"
}
function should_support_expected_cipherlists() {
# Make a directory with test user ownership. Avoids Docker creating this with root ownership.
# TODO: Can switch to filename prefix for JSON output when this is resolved: https://github.com/drwetter/testssl.sh/issues/1845
local RESULTS_PATH="${TLS_RESULTS_DIR}/${TEST_VARIANT}"
mkdir -p "${RESULTS_PATH}"
collect_cipherlists
verify_cipherlists
}
# Verify that the collected results match our expected cipherlists:
function verify_cipherlists() {
# SMTP: Opportunistic STARTTLS Explicit(25) # SMTP: Opportunistic STARTTLS Explicit(25)
# Needs to test against cipher lists specific to Port 25 ('_p25' parameter) # Needs to test against cipher lists specific to Port 25 ('_p25' parameter)
check_cipherlists "${RESULTS_PATH}/port_25.json" '_p25' check_cipherlists "${RESULTS_PATH}/port_25.json" '_p25'
@ -93,82 +140,56 @@ function check_ports() {
check_cipherlists "${RESULTS_PATH}/port_995.json" check_cipherlists "${RESULTS_PATH}/port_995.json"
} }
function collect_cipherlist_data() { # Using `testssl.sh` we can test each port to collect a list of supported cipher suites (ordered):
local ALT_CERT=() function collect_cipherlists() {
local ALT_KEY=()
if [[ -n ${ALT_KEY_TYPE} ]]
then
ALT_CERT=(--env SSL_ALT_CERT_PATH="/config/ssl/cert.${ALT_KEY_TYPE}.pem")
ALT_KEY=(--env SSL_ALT_KEY_PATH="/config/ssl/key.${ALT_KEY_TYPE}.pem")
fi
run docker run -d --name tls_test_cipherlists \
--volume "${PRIVATE_CONFIG}/:/tmp/docker-mailserver/" \
--volume "${TLS_CONFIG_VOLUME}" \
--env ENABLE_POP3=1 \
--env SSL_TYPE="manual" \
--env SSL_CERT_PATH="/config/ssl/cert.${KEY_TYPE}.pem" \
--env SSL_KEY_PATH="/config/ssl/key.${KEY_TYPE}.pem" \
"${ALT_CERT[@]}" \
"${ALT_KEY[@]}" \
--env TLS_LEVEL="${TLS_LEVEL}" \
--network "${NETWORK}" \
--network-alias "${DOMAIN}" \
--hostname "mail.${DOMAIN}" \
--tty \
"${NAME}" # Image name
assert_success
wait_for_tcp_port_in_container 25 tls_test_cipherlists
# NOTE: An rDNS query for the container IP will resolve to `<container name>.<network name>.` # NOTE: An rDNS query for the container IP will resolve to `<container name>.<network name>.`
# Make directory with test user ownership. Avoids Docker creating with root ownership.
# TODO: Can switch to filename prefix for JSON output when this is resolved: https://github.com/drwetter/testssl.sh/issues/1845
mkdir -p "${TLS_RESULTS_DIR}/${RESULTS_PATH}"
# For non-CI test runs, instead of removing prior test files after this test suite completes, # For non-CI test runs, instead of removing prior test files after this test suite completes,
# they're retained and overwritten by future test runs instead. Useful for inspection. # they're retained and overwritten by future test runs instead. Useful for inspection.
# `--preference` reduces the test scope to the cipher suites reported as supported by the server. Completes in ~35% of the time. # `--preference` reduces the test scope to the cipher suites reported as supported by the server. Completes in ~35% of the time.
local TESTSSL_CMD=(--quiet --file "/config/ssl/testssl.txt" --mode parallel --overwrite --preference) local TESTSSL_CMD=(
--quiet
--file "/config/ssl/testssl.txt"
--mode parallel
--overwrite
--preference
)
# NOTE: Batch testing ports via `--file` doesn't properly bubble up failure. # NOTE: Batch testing ports via `--file` doesn't properly bubble up failure.
# If the failure for a test is misleading consider testing a single port with: # If the failure for a test is misleading consider testing a single port with:
# local TESTSSL_CMD=(--quiet --jsonfile-pretty "${RESULTS_PATH}/port_${PORT}.json" --starttls smtp "${DOMAIN}:${PORT}") # local TESTSSL_CMD=(--quiet --jsonfile-pretty "/output/port_${PORT}.json" --starttls smtp "${TEST_DOMAIN}:${PORT}")
# TODO: Can use `jq` to check for failure when this is resolved: https://github.com/drwetter/testssl.sh/issues/1844 # TODO: Can use `jq` to check for failure when this is resolved: https://github.com/drwetter/testssl.sh/issues/1844
# `--user "<uid>:<gid>"` is a workaround: Avoids `permission denied` write errors for json output, uses `id` to match user uid & gid. # `--user "<uid>:<gid>"` is a workaround: Avoids `permission denied` write errors for json output, uses `id` to match user uid & gid.
run docker run --rm \ run docker run --rm \
--user "$(id -u):$(id -g)" \ --user "$(id -u):$(id -g)" \
--network "${NETWORK}" \ --network "${TEST_NETWORK}" \
--volume "${TLS_CONFIG_VOLUME}" \ --volume "${TLS_CONFIG_VOLUME}" \
--volume "${TLS_RESULTS_DIR}/${RESULTS_PATH}/:/output" \ --volume "${RESULTS_PATH}:/output" \
--workdir "/output" \ --workdir "/output" \
drwetter/testssl.sh:3.1dev "${TESTSSL_CMD[@]}" drwetter/testssl.sh:3.1dev "${TESTSSL_CMD[@]}"
assert_success assert_success
} }
# Compares the expected cipher lists against logged test results from `testssl.sh`
function check_cipherlists() {
local RESULTS_FILEPATH=$1
local p25=$2 # optional suffix
compare_cipherlist "cipherorder_TLSv1_2" "$(get_cipherlist "TLSv1_2${p25}")"
compare_cipherlist "cipherorder_TLSv1_3" "$(get_cipherlist 'TLSv1_3')"
}
# Use `jq` to extract a specific cipher list from the target`testssl.sh` results json output file # Use `jq` to extract a specific cipher list from the target`testssl.sh` results json output file
function compare_cipherlist() { function compare_cipherlist() {
local TARGET_CIPHERLIST=$1 local TARGET_CIPHERLIST=$1
local RESULTS_FILE=$2 local EXPECTED_CIPHERLIST=$2
local EXPECTED_CIPHERLIST=$3
run jq '.scanResult[0].serverPreferences[] | select(.id=="'"${TARGET_CIPHERLIST}"'") | .finding' "${TLS_RESULTS_DIR}/${RESULTS_FILE}" run jq '.scanResult[0].serverPreferences[] | select(.id=="'"${TARGET_CIPHERLIST}"'") | .finding' "${RESULTS_FILEPATH}"
assert_success assert_success
assert_output "${EXPECTED_CIPHERLIST}" assert_output "${EXPECTED_CIPHERLIST}"
} }
# Compares the expected cipher lists against logged test results from `testssl.sh`
function check_cipherlists() {
local RESULTS_FILE=$1
local p25=$2 # optional suffix
compare_cipherlist "cipherorder_TLSv1_2" "${RESULTS_FILE}" "$(get_cipherlist "TLSv1_2${p25}")"
compare_cipherlist "cipherorder_TLSv1_3" "${RESULTS_FILE}" "$(get_cipherlist 'TLSv1_3')"
}
# Expected cipher lists. Should match `TLS_LEVEL` cipher lists set in `scripts/helpers/ssl.sh`. # Expected cipher lists. Should match `TLS_LEVEL` cipher lists set in `scripts/helpers/ssl.sh`.
# Excluding Port 25 which uses defaults from Postfix after applying `smtpd_tls_exclude_ciphers` rules. # Excluding Port 25 which uses defaults from Postfix after applying `smtpd_tls_exclude_ciphers` rules.
# NOTE: If a test fails, look at the `check_ports` params, then update the corresponding associative key's value # NOTE: If a test fails, look at the `check_ports` params, then update the corresponding associative key's value
@ -185,34 +206,33 @@ function get_cipherlist() {
# Associative array for easy querying of required cipher list # Associative array for easy querying of required cipher list
declare -A CIPHER_LIST declare -A CIPHER_LIST
CIPHER_LIST["rsa_intermediate_TLSv1_2"]='"ECDHE-RSA-CHACHA20-POLY1305 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256"' # RSA:
CIPHER_LIST["rsa_modern_TLSv1_2"]='"ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384"' CIPHER_LIST["intermediate-rsa_TLSv1_2"]='"ECDHE-RSA-CHACHA20-POLY1305 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256"'
CIPHER_LIST["modern-rsa_TLSv1_2"]='"ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384"'
# ECDSA: # ECDSA:
CIPHER_LIST["ecdsa_intermediate_TLSv1_2"]='"ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384"' CIPHER_LIST["intermediate-ecdsa_TLSv1_2"]='"ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384"'
CIPHER_LIST["ecdsa_modern_TLSv1_2"]='"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305"' CIPHER_LIST["modern-ecdsa_TLSv1_2"]='"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305"'
# ECDSA + RSA fallback, dual cert support: # ECDSA + RSA fallback, dual cert support:
CIPHER_LIST["ecdsa_rsa_intermediate_TLSv1_2"]='"ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA384 DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256"' CIPHER_LIST["intermediate-ecdsa-rsa_TLSv1_2"]='"ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA384 DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256"'
CIPHER_LIST["ecdsa_rsa_modern_TLSv1_2"]='"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384"' CIPHER_LIST["modern-ecdsa-rsa_TLSv1_2"]='"ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384"'
# Port 25 # Port 25 has a different server order, and also includes ARIA, CCM, DHE+CHACHA20-POLY1305 cipher suites:
# TLSv1_2 has different server order and also includes ARIA, CCM, DHE+CHACHA20-POLY1305 cipher suites: # RSA (Port 25):
CIPHER_LIST["rsa_intermediate_TLSv1_2_p25"]='"ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES256-CCM8 DHE-RSA-AES256-CCM ECDHE-ARIA256-GCM-SHA384 DHE-RSA-ARIA256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES256-SHA256 ARIA256-GCM-SHA384 ECDHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-CCM8 DHE-RSA-AES128-CCM ECDHE-ARIA128-GCM-SHA256 DHE-RSA-ARIA128-GCM-SHA256 ECDHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA256 ARIA128-GCM-SHA256"' CIPHER_LIST["intermediate-rsa_TLSv1_2_p25"]='"ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-CHACHA20-POLY1305 DHE-RSA-AES256-CCM8 DHE-RSA-AES256-CCM ECDHE-ARIA256-GCM-SHA384 DHE-RSA-ARIA256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES256-SHA256 ARIA256-GCM-SHA384 ECDHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-CCM8 DHE-RSA-AES128-CCM ECDHE-ARIA128-GCM-SHA256 DHE-RSA-ARIA128-GCM-SHA256 ECDHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA256 ARIA128-GCM-SHA256"'
# Port 25 is unaffected by `TLS_LEVEL` profiles, it has the same TLS v1.2 cipher list under both:
CIPHER_LIST["rsa_modern_TLSv1_2_p25"]=${CIPHER_LIST["rsa_intermediate_TLSv1_2_p25"]}
# ECDSA (Port 25): # ECDSA (Port 25):
CIPHER_LIST["ecdsa_intermediate_TLSv1_2_p25"]='"ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-CCM8 ECDHE-ECDSA-AES256-CCM ECDHE-ECDSA-ARIA256-GCM-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-CCM8 ECDHE-ECDSA-AES128-CCM ECDHE-ECDSA-ARIA128-GCM-SHA256 ECDHE-ECDSA-AES128-SHA256"' CIPHER_LIST["intermediate-ecdsa_TLSv1_2_p25"]='"ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-CCM8 ECDHE-ECDSA-AES256-CCM ECDHE-ECDSA-ARIA256-GCM-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-CCM8 ECDHE-ECDSA-AES128-CCM ECDHE-ECDSA-ARIA128-GCM-SHA256 ECDHE-ECDSA-AES128-SHA256"'
CIPHER_LIST["ecdsa_modern_TLSv1_2_p25"]=${CIPHER_LIST["ecdsa_intermediate_TLSv1_2_p25"]}
# ECDSA + RSA fallback, dual cert support (Port 25): # ECDSA + RSA fallback, dual cert support (Port 25):
CIPHER_LIST["ecdsa_rsa_intermediate_TLSv1_2_p25"]='"ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-CCM8 ECDHE-ECDSA-AES256-CCM DHE-RSA-AES256-CCM8 DHE-RSA-AES256-CCM ECDHE-ECDSA-ARIA256-GCM-SHA384 ECDHE-ARIA256-GCM-SHA384 DHE-RSA-ARIA256-GCM-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES256-SHA256 ARIA256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-CCM8 ECDHE-ECDSA-AES128-CCM DHE-RSA-AES128-CCM8 DHE-RSA-AES128-CCM ECDHE-ECDSA-ARIA128-GCM-SHA256 ECDHE-ARIA128-GCM-SHA256 DHE-RSA-ARIA128-GCM-SHA256 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA256 ARIA128-GCM-SHA256"' CIPHER_LIST["intermediate-ecdsa-rsa_TLSv1_2_p25"]='"ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-CHACHA20-POLY1305 DHE-RSA-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-CCM8 ECDHE-ECDSA-AES256-CCM DHE-RSA-AES256-CCM8 DHE-RSA-AES256-CCM ECDHE-ECDSA-ARIA256-GCM-SHA384 ECDHE-ARIA256-GCM-SHA384 DHE-RSA-ARIA256-GCM-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES256-SHA384 DHE-RSA-AES256-SHA256 ARIA256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-CCM8 ECDHE-ECDSA-AES128-CCM DHE-RSA-AES128-CCM8 DHE-RSA-AES128-CCM ECDHE-ECDSA-ARIA128-GCM-SHA256 ECDHE-ARIA128-GCM-SHA256 DHE-RSA-ARIA128-GCM-SHA256 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA256 DHE-RSA-AES128-SHA256 ARIA128-GCM-SHA256"'
CIPHER_LIST["ecdsa_rsa_modern_TLSv1_2_p25"]=${CIPHER_LIST["ecdsa_rsa_intermediate_TLSv1_2_p25"]}
# Port 25 is unaffected by `TLS_LEVEL` profiles, thus no difference for modern:
CIPHER_LIST["modern-rsa_TLSv1_2_p25"]=${CIPHER_LIST["intermediate-rsa_TLSv1_2_p25"]}
CIPHER_LIST["modern-ecdsa_TLSv1_2_p25"]=${CIPHER_LIST["intermediate-ecdsa_TLSv1_2_p25"]}
CIPHER_LIST["modern-ecdsa-rsa_TLSv1_2_p25"]=${CIPHER_LIST["intermediate-ecdsa-rsa_TLSv1_2_p25"]}
local TARGET_QUERY="${KEY_TYPE_LABEL}_${TLS_LEVEL}_${TLS_VERSION}" local TARGET_QUERY="${TEST_VARIANT}_${TLS_VERSION}"
echo "${CIPHER_LIST[${TARGET_QUERY}]}" echo "${CIPHER_LIST[${TARGET_QUERY}]}"
fi fi
} }

View File

@ -1,29 +1,22 @@
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/test_helper/tls" load "${REPOSITORY_ROOT}/test/helper/common"
load "${REPOSITORY_ROOT}/test/helper/tls"
# Globals referenced from `test_helper/common`: TEST_NAME_PREFIX='[Security] TLS (SSL_TYPE=letsencrypt):'
# TEST_NAME TEST_FQDN TEST_TMP_CONFIG CONTAINER1_NAME='dms-test_tls-letsencrypt_default-hostname'
CONTAINER2_NAME='dms-test_tls-letsencrypt_fallback-domainname'
# Requires maintenance (TODO): Yes CONTAINER3_NAME='dms-test_tls-letsencrypt_support-acme-json'
# Can run tests in parallel?: No
# Not parallelize friendly when TEST_NAME is static,
# presently name of test file: `mail_ssl_letsencrypt`.
#
# Also shares a common TEST_TMP_CONFIG local folder,
# Instead of individual PRIVATE_CONFIG copies.
# For this test that is a non-issue, unless run in parallel.
# Applies to all tests:
function setup_file() {
init_with_defaults
# Override default to match the hostname we want to test against instead:
export TEST_FQDN='mail.example.test' export TEST_FQDN='mail.example.test'
function teardown() { _default_teardown ; }
# Similar to BATS `setup()` method, but invoked manually after
# CONTAINER_NAME has been adjusted for the running testcase.
function _initial_setup() {
init_with_defaults
# Prepare certificates in the letsencrypt supported file structure: # Prepare certificates in the letsencrypt supported file structure:
# Note Certbot uses `privkey.pem`. # NOTE: Certbot uses `privkey.pem`.
# `fullchain.pem` is currently what's detected, but we're actually providing the equivalent of `cert.pem` here. # `fullchain.pem` is currently what's detected, but we're actually providing the equivalent of `cert.pem` here.
# TODO: Verify format/structure is supported for nginx-proxy + acme-companion (uses `acme.sh` to provision). # TODO: Verify format/structure is supported for nginx-proxy + acme-companion (uses `acme.sh` to provision).
@ -36,45 +29,38 @@ function setup_file() {
_copy_to_letsencrypt_storage 'example.test/with_ca/ecdsa/key.rsa.pem' 'example.test/privkey.pem' _copy_to_letsencrypt_storage 'example.test/with_ca/ecdsa/key.rsa.pem' 'example.test/privkey.pem'
} }
# Not used
# function teardown_file() {
# }
function teardown() {
docker rm -f "${TEST_NAME}"
}
# Should detect and choose the cert for FQDN `mail.example.test` (HOSTNAME): # Should detect and choose the cert for FQDN `mail.example.test` (HOSTNAME):
@test "ssl(letsencrypt): Should default to HOSTNAME (mail.example.test)" { @test "${TEST_NAME_PREFIX} Should default to HOSTNAME (${TEST_FQDN})" {
local TARGET_DOMAIN='mail.example.test' export CONTAINER_NAME=${CONTAINER1_NAME}
_initial_setup
local TEST_DOCKER_ARGS=( local TARGET_DOMAIN=${TEST_FQDN}
local CUSTOM_SETUP_ARGUMENTS=(
--volume "${TEST_TMP_CONFIG}/letsencrypt/${TARGET_DOMAIN}/:/etc/letsencrypt/live/${TARGET_DOMAIN}/:ro" --volume "${TEST_TMP_CONFIG}/letsencrypt/${TARGET_DOMAIN}/:/etc/letsencrypt/live/${TARGET_DOMAIN}/:ro"
--env PERMIT_DOCKER='container' --env PERMIT_DOCKER='container'
--env SSL_TYPE='letsencrypt' --env SSL_TYPE='letsencrypt'
) )
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
common_container_setup 'TEST_DOCKER_ARGS' # Test that certificate files exist for the configured `hostname`:
#test hostname has certificate files
_should_have_valid_config "${TARGET_DOMAIN}" 'privkey.pem' 'fullchain.pem' _should_have_valid_config "${TARGET_DOMAIN}" 'privkey.pem' 'fullchain.pem'
_should_succesfully_negotiate_tls "${TARGET_DOMAIN}" _should_succesfully_negotiate_tls "${TARGET_DOMAIN}"
_should_not_support_fqdn_in_cert 'example.test' _should_not_support_fqdn_in_cert 'example.test'
} }
# Should detect and choose cert for FQDN `example.test` (DOMAINNAME), # Should detect and choose cert for FQDN `example.test` (DOMAINNAME),
# as fallback when no cert for FQDN `mail.example.test` (HOSTNAME) exists: # as fallback when no cert for FQDN `mail.example.test` (HOSTNAME) exists:
@test "ssl(letsencrypt): Should fallback to DOMAINNAME (example.test)" { @test "${TEST_NAME_PREFIX} Should fallback to DOMAINNAME (example.test)" {
local TARGET_DOMAIN='example.test' export CONTAINER_NAME=${CONTAINER2_NAME}
_initial_setup
local TEST_DOCKER_ARGS=( local TARGET_DOMAIN='example.test'
local CUSTOM_SETUP_ARGUMENTS=(
--volume "${TEST_TMP_CONFIG}/letsencrypt/${TARGET_DOMAIN}/:/etc/letsencrypt/live/${TARGET_DOMAIN}/:ro" --volume "${TEST_TMP_CONFIG}/letsencrypt/${TARGET_DOMAIN}/:/etc/letsencrypt/live/${TARGET_DOMAIN}/:ro"
--env PERMIT_DOCKER='container' --env PERMIT_DOCKER='container'
--env SSL_TYPE='letsencrypt' --env SSL_TYPE='letsencrypt'
) )
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
common_container_setup 'TEST_DOCKER_ARGS'
#test domain has certificate files #test domain has certificate files
_should_have_valid_config "${TARGET_DOMAIN}" 'privkey.pem' 'fullchain.pem' _should_have_valid_config "${TARGET_DOMAIN}" 'privkey.pem' 'fullchain.pem'
@ -87,34 +73,36 @@ function teardown() {
# #
# NOTE: Currently all of the `acme.json` configs have the FQDN match a SAN value, # NOTE: Currently all of the `acme.json` configs have the FQDN match a SAN value,
# all Subject CN (`main` in acme.json) are `Smallstep Leaf` which is not an FQDN. # all Subject CN (`main` in acme.json) are `Smallstep Leaf` which is not an FQDN.
# While valid for that field, it does mean there is no test coverage against `main`. # While not using a FQDN is valid for that field,
@test "ssl(letsencrypt): Traefik 'acme.json' (*.example.test)" { # it does mean there is no test coverage against the `acme.json` field `main`.
# This test group changes to certs signed with an RSA Root CA key, @test "${TEST_NAME_PREFIX} Traefik 'acme.json' (*.example.test)" {
# These certs all support both FQDNs: `mail.example.test` and `example.test`, export CONTAINER_NAME=${CONTAINER3_NAME}
# Except for the wildcard cert `*.example.test`, which intentionally excluded `example.test` when created. _initial_setup
# We want to maintain the same FQDN (mail.example.test) between the _acme_ecdsa and _acme_rsa tests.
local LOCAL_BASE_PATH="${PWD}/test/test-files/ssl/example.test/with_ca/rsa"
# Change default Root CA cert used for verifying chain of trust with openssl: # Override the `_initial_setup()` default Root CA cert (used for verifying the chain of trust via `openssl`):
# shellcheck disable=SC2034 # shellcheck disable=SC2034
local TEST_CA_CERT="${TEST_FILES_CONTAINER_PATH}/ssl/example.test/with_ca/rsa/ca-cert.rsa.pem" local TEST_CA_CERT="${TEST_FILES_CONTAINER_PATH}/ssl/example.test/with_ca/rsa/ca-cert.rsa.pem"
# This test group switches to certs that are signed with an RSA Root CA key instead.
# All of these certs support both FQDNs (`mail.example.test` and `example.test`),
# Except for the wildcard cert (`*.example.test`), that was created with `example.test` intentionally excluded from SAN.
# We want to maintain the same FQDN (`mail.example.test`) between the _acme_ecdsa and _acme_rsa tests.
local LOCAL_BASE_PATH="${PWD}/test/test-files/ssl/example.test/with_ca/rsa"
function _prepare() { function _prepare() {
# Default `acme.json` for _acme_ecdsa test: # Default `acme.json` for _acme_ecdsa test:
cp "${LOCAL_BASE_PATH}/ecdsa.acme.json" "${TEST_TMP_CONFIG}/letsencrypt/acme.json" cp "${LOCAL_BASE_PATH}/ecdsa.acme.json" "${TEST_TMP_CONFIG}/letsencrypt/acme.json"
# TODO: Provision wildcard certs via Traefik to inspect if `example.test` non-wildcard is also added to the cert. # TODO: Provision wildcard certs via Traefik to inspect if `example.test` non-wildcard is also added to the cert.
# shellcheck disable=SC2034 local CUSTOM_SETUP_ARGUMENTS=(
local TEST_DOCKER_ARGS=(
--volume "${TEST_TMP_CONFIG}/letsencrypt/acme.json:/etc/letsencrypt/acme.json:ro" --volume "${TEST_TMP_CONFIG}/letsencrypt/acme.json:/etc/letsencrypt/acme.json:ro"
--env LOG_LEVEL='trace' --env LOG_LEVEL='trace'
--env PERMIT_DOCKER='container' --env PERMIT_DOCKER='container'
--env SSL_DOMAIN='*.example.test' --env SSL_DOMAIN='*.example.test'
--env SSL_TYPE='letsencrypt' --env SSL_TYPE='letsencrypt'
) )
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
common_container_setup 'TEST_DOCKER_ARGS' wait_for_service "${CONTAINER_NAME}" 'changedetector'
wait_for_service "${TEST_NAME}" 'changedetector'
# Wait until the changedetector service startup delay is over: # Wait until the changedetector service startup delay is over:
repeat_until_success_or_timeout 20 sh -c "$(_get_service_logs 'changedetector') | grep 'Changedetector is ready'" repeat_until_success_or_timeout 20 sh -c "$(_get_service_logs 'changedetector') | grep 'Changedetector is ready'"
@ -184,7 +172,6 @@ function teardown() {
# Test Methods # Test Methods
# #
# Check that Dovecot and Postfix are configured to use a cert for the expected FQDN: # Check that Dovecot and Postfix are configured to use a cert for the expected FQDN:
function _should_have_valid_config() { function _should_have_valid_config() {
local EXPECTED_FQDN=${1} local EXPECTED_FQDN=${1}
@ -199,16 +186,14 @@ function _should_have_valid_config() {
# CMD ${1} run in container with output checked to match value of ${2}: # CMD ${1} run in container with output checked to match value of ${2}:
function _has_matching_line() { function _has_matching_line() {
run docker exec "${TEST_NAME}" sh -c "${1} | grep '${2}'" _run_in_container bash -c "${1} | grep '${2}'"
assert_output "${2}" assert_output "${2}"
} }
# #
# Traefik `acme.json` specific # Traefik `acme.json` specific
# #
# It should log success of extraction for the expected domain and restart Postfix. # It should log success of extraction for the expected domain and restart Postfix.
function _should_have_succeeded_at_extraction() { function _should_have_succeeded_at_extraction() {
local EXPECTED_DOMAIN=${1} local EXPECTED_DOMAIN=${1}
@ -249,7 +234,7 @@ function _should_have_service_reload_count() {
local NUM_RELOADS=${1} local NUM_RELOADS=${1}
# Count how many times processes (like Postfix and Dovecot) have been reloaded by the `changedetector` service: # Count how many times processes (like Postfix and Dovecot) have been reloaded by the `changedetector` service:
run docker exec "${TEST_NAME}" sh -c "grep -c 'Completed handling of detected change' /var/log/supervisor/changedetector.log" _run_in_container grep --count 'Completed handling of detected change' '/var/log/supervisor/changedetector.log'
assert_output "${NUM_RELOADS}" assert_output "${NUM_RELOADS}"
} }
@ -265,12 +250,10 @@ function _should_have_expected_files() {
_should_be_equal_in_content "${LE_CERT_PATH}" "${EXPECTED_CERT_PATH}" _should_be_equal_in_content "${LE_CERT_PATH}" "${EXPECTED_CERT_PATH}"
} }
# #
# Misc # Misc
# #
# Rename test certificate files to match the expected file structure for letsencrypt: # Rename test certificate files to match the expected file structure for letsencrypt:
function _copy_to_letsencrypt_storage() { function _copy_to_letsencrypt_storage() {
local SRC=${1} local SRC=${1}
@ -280,14 +263,18 @@ function _copy_to_letsencrypt_storage() {
FQDN_DIR=$(echo "${DEST}" | cut -d '/' -f1) FQDN_DIR=$(echo "${DEST}" | cut -d '/' -f1)
mkdir -p "${TEST_TMP_CONFIG}/letsencrypt/${FQDN_DIR}" mkdir -p "${TEST_TMP_CONFIG}/letsencrypt/${FQDN_DIR}"
cp "${PWD}/test/test-files/ssl/${SRC}" "${TEST_TMP_CONFIG}/letsencrypt/${DEST}" if ! cp "${PWD}/test/test-files/ssl/${SRC}" "${TEST_TMP_CONFIG}/letsencrypt/${DEST}"
then
echo "Could not copy cert file '${SRC}'' to '${DEST}'" >&2
exit 1
fi
} }
function _should_be_equal_in_content() { function _should_be_equal_in_content() {
local CONTAINER_PATH=${1} local CONTAINER_PATH=${1}
local LOCAL_PATH=${2} local LOCAL_PATH=${2}
run docker exec "${TEST_NAME}" sh -c "cat ${CONTAINER_PATH}" _run_in_container /bin/bash -c "cat ${CONTAINER_PATH}"
assert_output "$(cat "${LOCAL_PATH}")" assert_output "$(cat "${LOCAL_PATH}")"
assert_success assert_success
} }
@ -295,13 +282,13 @@ function _should_be_equal_in_content() {
function _get_service_logs() { function _get_service_logs() {
local SERVICE=${1:-'mailserver'} local SERVICE=${1:-'mailserver'}
local CMD_LOGS=(docker exec "${TEST_NAME}" "supervisorctl tail -2200 ${SERVICE}") local CMD_LOGS=(docker exec "${CONTAINER_NAME}" "supervisorctl tail -2200 ${SERVICE}")
# As the `mailserver` service logs are not stored in a file but output to stdout/stderr, # As the `mailserver` service logs are not stored in a file but output to stdout/stderr,
# The `supervisorctl tail` command won't work; we must instead query via `docker logs`: # The `supervisorctl tail` command won't work; we must instead query via `docker logs`:
if [[ ${SERVICE} == 'mailserver' ]] if [[ ${SERVICE} == 'mailserver' ]]
then then
CMD_LOGS=(docker logs "${TEST_NAME}") CMD_LOGS=(docker logs "${CONTAINER_NAME}")
fi fi
echo "${CMD_LOGS[@]}" echo "${CMD_LOGS[@]}"

View File

@ -1,8 +1,11 @@
#!/usr/bin/env bats load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/test_helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='[Security] TLS (SSL_TYPE=manual):'
CONTAINER_NAME='dms-test_tls-manual'
function setup_file() { function setup_file() {
# Internal copies made by `start-mailserver.sh`: # Internal copies made by `scripts/helpers/ssl.sh`:
export PRIMARY_KEY='/etc/dms/tls/key' export PRIMARY_KEY='/etc/dms/tls/key'
export PRIMARY_CERT='/etc/dms/tls/cert' export PRIMARY_CERT='/etc/dms/tls/cert'
export FALLBACK_KEY='/etc/dms/tls/fallback_key' export FALLBACK_KEY='/etc/dms/tls/fallback_key'
@ -14,98 +17,101 @@ function setup_file() {
export SSL_ALT_KEY_PATH='/config/ssl/key.rsa.pem' export SSL_ALT_KEY_PATH='/config/ssl/key.rsa.pem'
export SSL_ALT_CERT_PATH='/config/ssl/cert.rsa.pem' export SSL_ALT_CERT_PATH='/config/ssl/cert.rsa.pem'
local PRIVATE_CONFIG export TEST_DOMAIN='example.test'
export DOMAIN_SSL_MANUAL='example.test'
PRIVATE_CONFIG=$(duplicate_config_for_container .)
docker run -d --name mail_manual_ssl \ local CUSTOM_SETUP_ARGUMENTS=(
--volume "${PRIVATE_CONFIG}/:/tmp/docker-mailserver/" \ --volume "${PWD}/test/test-files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/:/config/ssl/:ro"
--volume "$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/:/config/ssl/:ro" \ --env LOG_LEVEL='trace'
--env LOG_LEVEL='trace' \ --env SSL_TYPE='manual'
--env SSL_TYPE='manual' \ --env TLS_LEVEL='modern'
--env TLS_LEVEL='modern' \ --env SSL_KEY_PATH="${SSL_KEY_PATH}"
--env SSL_KEY_PATH="${SSL_KEY_PATH}" \ --env SSL_CERT_PATH="${SSL_CERT_PATH}"
--env SSL_CERT_PATH="${SSL_CERT_PATH}" \ --env SSL_ALT_KEY_PATH="${SSL_ALT_KEY_PATH}"
--env SSL_ALT_KEY_PATH="${SSL_ALT_KEY_PATH}" \ --env SSL_ALT_CERT_PATH="${SSL_ALT_CERT_PATH}"
--env SSL_ALT_CERT_PATH="${SSL_ALT_CERT_PATH}" \ )
--hostname "mail.${DOMAIN_SSL_MANUAL}" \
--tty \
"${NAME}" # Image name
wait_for_finished_setup_in_container mail_manual_ssl init_with_defaults
# Override the default set in `common_container_setup`:
export TEST_FQDN="mail.${TEST_DOMAIN}"
common_container_setup 'CUSTOM_SETUP_ARGUMENTS'
} }
function teardown_file() { function teardown_file() { _default_teardown ; }
docker rm -f mail_manual_ssl
@test "${TEST_NAME_PREFIX} ENV vars provided are valid files" {
_run_in_container [ -f "${SSL_CERT_PATH}" ]
assert_success
_run_in_container [ -f "${SSL_KEY_PATH}" ]
assert_success
_run_in_container [ -f "${SSL_ALT_CERT_PATH}" ]
assert_success
_run_in_container [ -f "${SSL_ALT_KEY_PATH}" ]
assert_success
} }
@test "checking ssl: ENV vars provided are valid files" { @test "${TEST_NAME_PREFIX} manual configuration is correct" {
assert docker exec mail_manual_ssl [ -f "${SSL_CERT_PATH}" ]
assert docker exec mail_manual_ssl [ -f "${SSL_KEY_PATH}" ]
assert docker exec mail_manual_ssl [ -f "${SSL_ALT_CERT_PATH}" ]
assert docker exec mail_manual_ssl [ -f "${SSL_ALT_KEY_PATH}" ]
}
@test "checking ssl: manual configuration is correct" {
local DOVECOT_CONFIG_SSL='/etc/dovecot/conf.d/10-ssl.conf' local DOVECOT_CONFIG_SSL='/etc/dovecot/conf.d/10-ssl.conf'
run docker exec mail_manual_ssl grep '^smtpd_tls_chain_files =' '/etc/postfix/main.cf' _run_in_container grep '^smtpd_tls_chain_files =' '/etc/postfix/main.cf'
assert_success assert_success
assert_output "smtpd_tls_chain_files = ${PRIMARY_KEY} ${PRIMARY_CERT} ${FALLBACK_KEY} ${FALLBACK_CERT}" assert_output "smtpd_tls_chain_files = ${PRIMARY_KEY} ${PRIMARY_CERT} ${FALLBACK_KEY} ${FALLBACK_CERT}"
run docker exec mail_manual_ssl grep '^ssl_key =' "${DOVECOT_CONFIG_SSL}" _run_in_container grep '^ssl_key =' "${DOVECOT_CONFIG_SSL}"
assert_success assert_success
assert_output "ssl_key = <${PRIMARY_KEY}" assert_output "ssl_key = <${PRIMARY_KEY}"
run docker exec mail_manual_ssl grep '^ssl_cert =' "${DOVECOT_CONFIG_SSL}" _run_in_container grep '^ssl_cert =' "${DOVECOT_CONFIG_SSL}"
assert_success assert_success
assert_output "ssl_cert = <${PRIMARY_CERT}" assert_output "ssl_cert = <${PRIMARY_CERT}"
run docker exec mail_manual_ssl grep '^ssl_alt_key =' "${DOVECOT_CONFIG_SSL}" _run_in_container grep '^ssl_alt_key =' "${DOVECOT_CONFIG_SSL}"
assert_success assert_success
assert_output "ssl_alt_key = <${FALLBACK_KEY}" assert_output "ssl_alt_key = <${FALLBACK_KEY}"
run docker exec mail_manual_ssl grep '^ssl_alt_cert =' "${DOVECOT_CONFIG_SSL}" _run_in_container grep '^ssl_alt_cert =' "${DOVECOT_CONFIG_SSL}"
assert_success assert_success
assert_output "ssl_alt_cert = <${FALLBACK_CERT}" assert_output "ssl_alt_cert = <${FALLBACK_CERT}"
} }
@test "checking ssl: manual configuration copied files correctly " { @test "${TEST_NAME_PREFIX} manual configuration copied files correctly " {
run docker exec mail_manual_ssl cmp -s "${PRIMARY_KEY}" "${SSL_KEY_PATH}" _run_in_container cmp -s "${PRIMARY_KEY}" "${SSL_KEY_PATH}"
assert_success assert_success
run docker exec mail_manual_ssl cmp -s "${PRIMARY_CERT}" "${SSL_CERT_PATH}" _run_in_container cmp -s "${PRIMARY_CERT}" "${SSL_CERT_PATH}"
assert_success assert_success
# Fallback cert # Fallback cert
run docker exec mail_manual_ssl cmp -s "${FALLBACK_KEY}" "${SSL_ALT_KEY_PATH}" _run_in_container cmp -s "${FALLBACK_KEY}" "${SSL_ALT_KEY_PATH}"
assert_success assert_success
run docker exec mail_manual_ssl cmp -s "${FALLBACK_CERT}" "${SSL_ALT_CERT_PATH}" _run_in_container cmp -s "${FALLBACK_CERT}" "${SSL_ALT_CERT_PATH}"
assert_success assert_success
} }
@test "checking ssl: manual cert works correctly" { @test "${TEST_NAME_PREFIX} manual cert works correctly" {
wait_for_tcp_port_in_container 587 mail_manual_ssl wait_for_tcp_port_in_container 587 "${CONTAINER_NAME}"
local TEST_COMMAND=(timeout 1 openssl s_client -connect mail.example.test:587 -starttls smtp) local TEST_COMMAND=(timeout 1 openssl s_client -connect mail.example.test:587 -starttls smtp)
local RESULT local RESULT
# Should fail as a chain of trust is required to verify successfully: # Should fail as a chain of trust is required to verify successfully:
RESULT=$(docker exec mail_manual_ssl "${TEST_COMMAND[@]}" | grep 'Verification error:') RESULT=$(docker exec "${CONTAINER_NAME}" "${TEST_COMMAND[@]}" | grep 'Verification error:')
assert_equal "${RESULT}" 'Verification error: unable to verify the first certificate' assert_equal "${RESULT}" 'Verification error: unable to verify the first certificate'
# Provide the Root CA cert for successful verification: # Provide the Root CA cert for successful verification:
local CA_CERT='/config/ssl/ca-cert.ecdsa.pem' local CA_CERT='/config/ssl/ca-cert.ecdsa.pem'
assert docker exec mail_manual_ssl [ -f "${CA_CERT}" ] assert docker exec "${CONTAINER_NAME}" [ -f "${CA_CERT}" ]
RESULT=$(docker exec mail_manual_ssl "${TEST_COMMAND[@]}" -CAfile "${CA_CERT}" | grep 'Verification: OK') RESULT=$(docker exec "${CONTAINER_NAME}" "${TEST_COMMAND[@]}" -CAfile "${CA_CERT}" | grep 'Verification: OK')
assert_equal "${RESULT}" 'Verification: OK' assert_equal "${RESULT}" 'Verification: OK'
} }
@test "checking ssl: manual cert changes are picked up by check-for-changes" { @test "${TEST_NAME_PREFIX} manual cert changes are picked up by check-for-changes" {
printf '%s' 'someThingsChangedHere' \ printf '%s' 'someThingsChangedHere' \
>>"$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/key.ecdsa.pem" >>"$(pwd)/test/test-files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/key.ecdsa.pem"
run timeout 15 docker exec mail_manual_ssl bash -c "tail -F /var/log/supervisor/changedetector.log | sed '/Manual certificates have changed/ q'" run timeout 15 docker exec "${CONTAINER_NAME}" bash -c "tail -F /var/log/supervisor/changedetector.log | sed '/Manual certificates have changed/ q'"
assert_success assert_success
sed -i '/someThingsChangedHere/d' "$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/key.ecdsa.pem" sed -i '/someThingsChangedHere/d' "$(pwd)/test/test-files/ssl/${TEST_DOMAIN}/with_ca/ecdsa/key.ecdsa.pem"
} }

View File

@ -2,9 +2,14 @@ load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='Dovecot protocols:' TEST_NAME_PREFIX='Dovecot protocols:'
CONTAINER1_NAME='dms-test_dovecot_protocols_all'
CONTAINER2_NAME='dms-test_dovecot_protocols_ipv4'
CONTAINER3_NAME='dms-test_dovecot_protocols_ipv6'
function teardown() { _default_teardown ; }
@test "${TEST_NAME_PREFIX} dual-stack IP configuration" { @test "${TEST_NAME_PREFIX} dual-stack IP configuration" {
local CONTAINER_NAME='dms-test-dovecot_protocols_all' export CONTAINER_NAME=${CONTAINER1_NAME}
local CUSTOM_SETUP_ARGUMENTS=(--env DOVECOT_INET_PROTOCOLS=) local CUSTOM_SETUP_ARGUMENTS=(--env DOVECOT_INET_PROTOCOLS=)
init_with_defaults init_with_defaults
@ -13,12 +18,10 @@ TEST_NAME_PREFIX='Dovecot protocols:'
_run_in_container grep '^#listen = \*, ::' /etc/dovecot/dovecot.conf _run_in_container grep '^#listen = \*, ::' /etc/dovecot/dovecot.conf
assert_success assert_success
assert_output '#listen = *, ::' assert_output '#listen = *, ::'
docker rm -f "${CONTAINER_NAME}"
} }
@test "${TEST_NAME_PREFIX} IPv4 configuration" { @test "${TEST_NAME_PREFIX} IPv4 configuration" {
local CONTAINER_NAME='dms-test-dovecot_protocols_ipv4' export CONTAINER_NAME=${CONTAINER2_NAME}
local CUSTOM_SETUP_ARGUMENTS=(--env DOVECOT_INET_PROTOCOLS=ipv4) local CUSTOM_SETUP_ARGUMENTS=(--env DOVECOT_INET_PROTOCOLS=ipv4)
init_with_defaults init_with_defaults
@ -27,12 +30,10 @@ TEST_NAME_PREFIX='Dovecot protocols:'
_run_in_container grep '^listen = \*$' /etc/dovecot/dovecot.conf _run_in_container grep '^listen = \*$' /etc/dovecot/dovecot.conf
assert_success assert_success
assert_output 'listen = *' assert_output 'listen = *'
docker rm -f "${CONTAINER_NAME}"
} }
@test "${TEST_NAME_PREFIX} IPv6 configuration" { @test "${TEST_NAME_PREFIX} IPv6 configuration" {
local CONTAINER_NAME='dms-test-dovecot_protocols_ipv6' export CONTAINER_NAME=${CONTAINER3_NAME}
local CUSTOM_SETUP_ARGUMENTS=(--env DOVECOT_INET_PROTOCOLS=ipv6) local CUSTOM_SETUP_ARGUMENTS=(--env DOVECOT_INET_PROTOCOLS=ipv6)
init_with_defaults init_with_defaults
@ -41,6 +42,4 @@ TEST_NAME_PREFIX='Dovecot protocols:'
_run_in_container grep '^listen = \[::\]$' /etc/dovecot/dovecot.conf _run_in_container grep '^listen = \[::\]$' /etc/dovecot/dovecot.conf
assert_success assert_success
assert_output 'listen = [::]' assert_output 'listen = [::]'
docker rm -f "${CONTAINER_NAME}"
} }

View File

@ -2,7 +2,7 @@ load "${REPOSITORY_ROOT}/test/helper/setup"
load "${REPOSITORY_ROOT}/test/helper/common" load "${REPOSITORY_ROOT}/test/helper/common"
TEST_NAME_PREFIX='helper functions inside container:' TEST_NAME_PREFIX='helper functions inside container:'
CONTAINER_NAME='dms-test-helper_functions' CONTAINER_NAME='dms-test_helper_functions'
function setup_file() { function setup_file() {
init_with_defaults init_with_defaults