diff --git a/Dockerfile b/Dockerfile index 3f5472ff..752fb501 100644 --- a/Dockerfile +++ b/Dockerfile @@ -225,7 +225,13 @@ RUN \ COPY target/fetchmail/fetchmailrc /etc/fetchmailrc_general COPY target/postfix/main.cf target/postfix/master.cf /etc/postfix/ -COPY target/shared/ffdhe4096.pem /etc/postfix/shared/ffdhe4096.pem + +# DH parameters for DHE cipher suites, ffdhe4096 is the official standard 4096-bit DH params now part of TLS 1.3 +# This file is for TLS <1.3 handshakes that rely on DHE cipher suites +# Handled at build to avoid failures by doveadm validating ssl_dh filepath in 10-ssl.auth (eg generate-accounts) +COPY target/shared/ffdhe4096.pem /etc/postfix/dhparams.pem +COPY target/shared/ffdhe4096.pem /etc/dovecot/dh.pem + COPY \ target/postfix/header_checks.pcre \ target/postfix/sender_header_filter.pcre \ diff --git a/docs/content/config/security/ssl.md b/docs/content/config/security/ssl.md index 2d200694..812fab6f 100644 --- a/docs/content/config/security/ssl.md +++ b/docs/content/config/security/ssl.md @@ -654,6 +654,12 @@ if [ "$certcheck_2weeks" = "Certificate will not expire" ]; then fi ``` +## Custom DH Parameters + +By default `docker-mailserver` uses [`ffdhe4096`][ffdhe4096-src] from [IETF RFC 7919][ietf::rfc::ffdhe]. These are standardized pre-defined DH groups and the only available DH groups for TLS 1.3. It is [discouraged to generate your own DH parameters][dh-avoid-selfgenerated] as it is often less secure. + +Despite this, if you must use non-standard DH parameters or you would like to swap `ffdhe4096` for a different group (eg `ffdhe2048`); Add your own PEM encoded DH params file via a volume to `/tmp/docker-mailserver/dhparams.pem`. This will replace DH params for both Dovecot and Postfix services during container startup. + [docs-optional-config]: ../advanced/optional-config.md [github-file-compose]: https://github.com/docker-mailserver/docker-mailserver/blob/master/docker-compose.yml @@ -661,4 +667,8 @@ fi [hanscees-renewcerts]: https://github.com/hanscees/dockerscripts/blob/master/scripts/tomav-renew-certs [traefik::github]: https://github.com/containous/traefik -[ietf::rfc::acme]: https://tools.ietf.org/html/rfc8555 +[ietf::rfc::acme]: https://datatracker.ietf.org/doc/html/rfc8555 + +[ietf::rfc::ffdhe]: https://datatracker.ietf.org/doc/html/rfc7919 +[ffdhe4096-src]: https://github.com/internetstandards/dhe_groups +[dh-avoid-selfgenerated]: https://crypto.stackexchange.com/questions/29926/what-diffie-hellman-parameters-should-i-use diff --git a/target/scripts/startup/setup-stack.sh b/target/scripts/startup/setup-stack.sh index 27ed552e..cd20a5e6 100644 --- a/target/scripts/startup/setup-stack.sh +++ b/target/scripts/startup/setup-stack.sh @@ -1301,92 +1301,30 @@ function _setup_postfix_relay_hosts function _setup_postfix_dhparam { - _notify 'task' 'Setting up Postfix dhparam' - - if [[ ${ONE_DIR} -eq 1 ]] - then - DHPARAMS_FILE=/var/mail-state/lib-shared/dhparams.pem - - if [[ ! -f ${DHPARAMS_FILE} ]] - then - _notify 'inf' "Use ffdhe4096 for dhparams (postfix)" - cp -f /etc/postfix/shared/ffdhe4096.pem /etc/postfix/dhparams.pem - else - _notify 'inf' "Use postfix dhparams that was generated previously" - _notify 'warn' "Using self-generated dhparams is considered as insecure." - _notify 'warn' "Unless you known what you are doing, please remove /var/mail-state/lib-shared/dhparams.pem." - - # Copy from the state directory to the working location - cp -f "${DHPARAMS_FILE}" /etc/postfix/dhparams.pem - fi - else - if [[ ! -f /etc/postfix/dhparams.pem ]] - then - if [[ -f /etc/dovecot/dh.pem ]] - then - _notify 'inf' "Copy dovecot dhparams to postfix" - cp /etc/dovecot/dh.pem /etc/postfix/dhparams.pem - elif [[ -f /tmp/docker-mailserver/dhparams.pem ]] - then - _notify 'inf' "Copy pre-generated dhparams to postfix" - _notify 'warn' "Using self-generated dhparams is considered as insecure." - _notify 'warn' "Unless you known what you are doing, please remove /var/mail-state/lib-shared/dhparams.pem." - cp /tmp/docker-mailserver/dhparams.pem /etc/postfix/dhparams.pem - else - _notify 'inf' "Use ffdhe4096 for dhparams (postfix)" - cp /etc/postfix/shared/ffdhe4096.pem /etc/postfix/dhparams.pem - fi - else - _notify 'inf' "Use existing postfix dhparams" - _notify 'warn' "Using self-generated dhparams is considered insecure." - _notify 'warn' "Unless you known what you are doing, please remove /etc/postfix/dhparams.pem." - fi - fi + _setup_dhparam 'postfix' '/etc/postfix/dhparams.pem' } function _setup_dovecot_dhparam { - _notify 'task' 'Setting up Dovecot dhparam' + _setup_dhparam 'dovecot' '/etc/dovecot/dh.pem' +} - if [[ ${ONE_DIR} -eq 1 ]] - then - DHPARAMS_FILE=/var/mail-state/lib-shared/dhparams.pem +function _setup_dhparam +{ + local DH_SERVICE=$1 + local DH_DEST=$2 + local DH_CUSTOM=/tmp/docker-mailserver/dhparams.pem - if [[ ! -f ${DHPARAMS_FILE} ]] - then - _notify 'inf' "Use ffdhe4096 for dhparams (dovecot)" - cp -f /etc/postfix/shared/ffdhe4096.pem /etc/dovecot/dh.pem - else - _notify 'inf' "Use dovecot dhparams that was generated previously" - _notify 'warn' "Using self-generated dhparams is considered as insecure." - _notify 'warn' "Unless you known what you are doing, please remove /var/mail-state/lib-shared/dhparams.pem." + _notify 'task' "Setting up ${DH_SERVICE} dhparam" - # Copy from the state directory to the working location - cp -f "${DHPARAMS_FILE}" /etc/dovecot/dh.pem - fi - else - if [[ ! -f /etc/dovecot/dh.pem ]] - then - if [[ -f /etc/postfix/dhparams.pem ]] - then - _notify 'inf' "Copy postfix dhparams to dovecot" - cp /etc/postfix/dhparams.pem /etc/dovecot/dh.pem - elif [[ -f /tmp/docker-mailserver/dhparams.pem ]] - then - _notify 'inf' "Copy pre-generated dhparams to dovecot" - _notify 'warn' "Using self-generated dhparams is considered as insecure." - _notify 'warn' "Unless you known what you are doing, please remove /tmp/docker-mailserver/dhparams.pem." + if [[ -f ${DH_CUSTOM} ]] + then # use custom supplied dh params (assumes they're probably insecure) + _notify 'inf' "${DH_SERVICE} will use custom provided DH paramters." + _notify 'warn' "Using self-generated dhparams is considered insecure. Unless you know what you are doing, please remove ${DH_CUSTOM}." - cp /tmp/docker-mailserver/dhparams.pem /etc/dovecot/dh.pem - else - _notify 'inf' "Use ffdhe4096 for dhparams (dovecot)" - cp /etc/postfix/shared/ffdhe4096.pem /etc/dovecot/dh.pem - fi - else - _notify 'inf' "Use existing dovecot dhparams" - _notify 'warn' "Using self-generated dhparams is considered as insecure." - _notify 'warn' "Unless you known what you are doing, please remove /etc/dovecot/dh.pem." - fi + cp -f "${DH_CUSTOM}" "${DH_DEST}" + else # use official standardized dh params (provided via Dockerfile) + _notify 'inf' "${DH_SERVICE} will use official standardized DH parameters (ffdhe4096)." fi } diff --git a/test/mail_dhparams_default.bats b/test/mail_dhparams_default.bats deleted file mode 100644 index 59c98b1d..00000000 --- a/test/mail_dhparams_default.bats +++ /dev/null @@ -1,81 +0,0 @@ -load 'test_helper/common' - -# Test case -# --------- -# By default, this image is using audited FFDHE groups (https://github.com/docker-mailserver/docker-mailserver/pull/1463) -# -# This test case covers the described case against both boolean states for `ONE_DIR`. -# -# Description: -# - When no DHE parameters are supplied by the user: -# ~ The file `ffdhe4096.pem` has not been modified (checksum verification). -# ~ `ffdhe4096.pem` is copied to the configuration directories for postfix and dovecot. - - -function setup() { - run_setup_file_if_necessary -} - -function teardown() { - run_teardown_file_if_necessary -} - -function setup_file() { - local PRIVATE_CONFIG - PRIVATE_CONFIG="$(duplicate_config_for_container . mail_default_dhparams_both_one_dir)" - docker run -d --name mail_default_dhparams_one_dir \ - -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ - -e DMS_DEBUG=0 \ - -e ONE_DIR=1 \ - -h mail.my-domain.com -t "${NAME}" - wait_for_finished_setup_in_container mail_default_dhparams_one_dir - - PRIVATE_CONFIG="$(duplicate_config_for_container . mail_default_dhparams_both_not_one_dir)" - docker run -d --name mail_default_dhparams_not_one_dir \ - -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ - -e DMS_DEBUG=0 \ - -e ONE_DIR=0 \ - -h mail.my-domain.com -t "${NAME}" - wait_for_finished_setup_in_container mail_default_dhparams_not_one_dir -} - -function teardown_file() { - docker rm -f mail_default_dhparams_one_dir - docker rm -f mail_default_dhparams_not_one_dir -} - -@test "first" { - skip 'this test must come first to reliably identify when to run setup_file' -} - -@test "checking ssl: checking dhe params are sufficient" { - # reference used: (22/04/2020) https://english.ncsc.nl/publications/publications/2019/juni/01/it-security-guidelines-for-transport-layer-security-tls - - # check ffdhe params are inchanged - REPO_CHECKSUM=$(sha512sum "$(pwd)/target/shared/ffdhe4096.pem" | awk '{print $1}') - MOZILLA_CHECKSUM=$(curl https://ssl-config.mozilla.org/ffdhe4096.txt -s | sha512sum | awk '{print $1}') - assert_equal "${REPO_CHECKSUM}" "${MOZILLA_CHECKSUM}" - run echo "${REPO_CHECKSUM}" - refute_output '' # checksum must not be empty - - # by default, ffdhe4096 should be used - - # ONE_DIR=1 - DOCKER_DOVECOT_CHECKSUM_ONE_DIR=$(docker exec mail_default_dhparams_one_dir sha512sum /etc/dovecot/dh.pem | awk '{print $1}') - DOCKER_POSTFIX_CHECKSUM_ONE_DIR=$(docker exec mail_default_dhparams_one_dir sha512sum /etc/postfix/dhparams.pem | awk '{print $1}') - assert_equal "${DOCKER_DOVECOT_CHECKSUM_ONE_DIR}" "${REPO_CHECKSUM}" - assert_equal "${DOCKER_POSTFIX_CHECKSUM_ONE_DIR}" "${REPO_CHECKSUM}" - - # ONE_DIR=0 - DOCKER_DOVECOT_CHECKSUM_NOT_ONE_DIR=$(docker exec mail_default_dhparams_not_one_dir sha512sum /etc/dovecot/dh.pem | awk '{print $1}') - DOCKER_POSTFIX_CHECKSUM_NOT_ONE_DIR=$(docker exec mail_default_dhparams_not_one_dir sha512sum /etc/postfix/dhparams.pem | awk '{print $1}') - assert_equal "${DOCKER_DOVECOT_CHECKSUM_NOT_ONE_DIR}" "${REPO_CHECKSUM}" - assert_equal "${DOCKER_POSTFIX_CHECKSUM_NOT_ONE_DIR}" "${REPO_CHECKSUM}" -} - - -@test "last" { - skip 'this test is only there to reliably mark the end for the teardown_file' -} diff --git a/test/mail_dhparams_manual_not_one_dir.bats b/test/mail_dhparams_manual_not_one_dir.bats deleted file mode 100644 index a873d61b..00000000 --- a/test/mail_dhparams_manual_not_one_dir.bats +++ /dev/null @@ -1,64 +0,0 @@ -load 'test_helper/common' - -# Test case -# --------- -# By default, this image is using audited FFDHE groups (https://github.com/docker-mailserver/docker-mailserver/pull/1463) -# -# This test case covers the described case when `ONE_DIR=0`. -# -# Description: -# - When custom DHE parameters are supplied by the user: -# ~ User supplied DHE parameters are copied to the configuration directories for postfix and dovecot. -# ~ A warning is raised about usage of insecure parameters. - - -function setup() { - run_setup_file_if_necessary -} - -function teardown() { - run_teardown_file_if_necessary -} - -function setup_file() { - local PRIVATE_CONFIG - PRIVATE_CONFIG=$(duplicate_config_for_container .) - # copy the custom DHE params in local config - cp "$(pwd)/test/test-files/ssl/custom-dhe-params.pem" "${PRIVATE_CONFIG}/dhparams.pem" - - docker run -d --name mail_manual_dhparams_not_one_dir \ - -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ - -e DMS_DEBUG=0 \ - -e ONE_DIR=0 \ - -h mail.my-domain.com -t "${NAME}" - wait_for_finished_setup_in_container mail_manual_dhparams_not_one_dir -} - -function teardown_file() { - docker rm -f mail_manual_dhparams_not_one_dir -} - -@test "first" { - skip 'this test must come first to reliably identify when to run setup_file' -} - -@test "checking dhparams: ONE_DIR=0 check manual dhparams is used" { - test_checksum=$(sha512sum "$(pwd)/test/test-files/ssl/custom-dhe-params.pem" | awk '{print $1}') - run echo "${test_checksum}" - refute_output '' # checksum must not be empty - - docker_dovecot_checksum=$(docker exec mail_manual_dhparams_not_one_dir sha512sum /etc/dovecot/dh.pem | awk '{print $1}') - docker_postfix_checksum=$(docker exec mail_manual_dhparams_not_one_dir sha512sum /etc/postfix/dhparams.pem | awk '{print $1}') - assert_equal "${docker_dovecot_checksum}" "${test_checksum}" - assert_equal "${docker_postfix_checksum}" "${test_checksum}" -} - -@test "checking dhparams: ONE_DIR=0 check warning output when using manual dhparams" { - run sh -c "docker logs mail_manual_dhparams_not_one_dir | grep 'Using self-generated dhparams is considered as insecure'" - assert_success -} - -@test "last" { - skip 'this test is only there to reliably mark the end for the teardown_file' -} diff --git a/test/mail_dhparams_manual_one_dir.bats b/test/mail_dhparams_manual_one_dir.bats deleted file mode 100644 index 6fa8c331..00000000 --- a/test/mail_dhparams_manual_one_dir.bats +++ /dev/null @@ -1,61 +0,0 @@ -load 'test_helper/common' - -# Test case -# --------- -# By default, this image is using audited FFDHE groups (https://github.com/docker-mailserver/docker-mailserver/pull/1463) -# -# This test case covers the described case when `ONE_DIR=1`. -# -# Description: -# - When custom DHE parameters are supplied by the user: -# ~ User supplied DHE parameters are copied to the configuration directories for postfix and dovecot. -# ~ A warning is raised about usage of insecure parameters. - -function setup() { - run_setup_file_if_necessary -} - -function teardown() { - run_teardown_file_if_necessary -} - -function setup_file() { - local PRIVATE_CONFIG - PRIVATE_CONFIG="$(duplicate_config_for_container .)" - docker run -d --name mail_manual_dhparams_one_dir \ - -v "${PRIVATE_CONFIG}":/tmp/docker-mailserver \ - -v "$(pwd)/test/test-files":/tmp/docker-mailserver-test:ro \ - -v "$(pwd)/test/test-files/ssl/custom-dhe-params.pem":/var/mail-state/lib-shared/dhparams.pem:ro \ - -e DMS_DEBUG=0 \ - -e ONE_DIR=1 \ - -h mail.my-domain.com -t "${NAME}" - wait_for_finished_setup_in_container mail_manual_dhparams_one_dir -} - -function teardown_file() { - docker rm -f mail_manual_dhparams_one_dir -} - -@test "first" { - skip 'this test must come first to reliably identify when to run setup_file' -} - -@test "checking dhparams: ONE_DIR=1 check manual dhparams is used" { - test_checksum=$(sha512sum "$(pwd)/test/test-files/ssl/custom-dhe-params.pem" | awk '{print $1}') - run echo "${test_checksum}" - refute_output '' # checksum must not be empty - - docker_dovecot_checksum=$(docker exec mail_manual_dhparams_one_dir sha512sum /etc/dovecot/dh.pem | awk '{print $1}') - docker_postfix_checksum=$(docker exec mail_manual_dhparams_one_dir sha512sum /etc/postfix/dhparams.pem | awk '{print $1}') - assert_equal "${docker_dovecot_checksum}" "${test_checksum}" - assert_equal "${docker_postfix_checksum}" "${test_checksum}" -} - -@test "checking dhparams: ONE_DIR=1 check warning output when using manual dhparams" { - run sh -c "docker logs mail_manual_dhparams_one_dir | grep 'Using self-generated dhparams is considered as insecure'" - assert_success -} - -@test "last" { - skip 'this test is only there to reliably mark the end for the teardown_file' -} diff --git a/test/mail_tls_dhparams.bats b/test/mail_tls_dhparams.bats new file mode 100644 index 00000000..bf787e4f --- /dev/null +++ b/test/mail_tls_dhparams.bats @@ -0,0 +1,142 @@ +load 'test_helper/common' + +# Test case +# --------- +# By default, this image is using audited FFDHE groups (https://github.com/docker-mailserver/docker-mailserver/pull/1463) +# +# This test case covers the described case against both boolean states for `ONE_DIR`. +# +# Description: +# 1. Verify that the file `ffdhe4096.pem` has not been modified (checksum verification). +# 2. Verify Postfix and Dovecot are using the default `ffdhe4096.pem` from Dockerfile build. +# 3. When custom DHE parameters are supplied by the user as `/tmp/docker-mailserver/dhparams.pem`: +# - Verify Postfix and Dovecot use the custom `custom-dhe-params.pem` (contents is actually `ffdhe2048.pem`). +# - A warning is raised about usage of potentially insecure parameters. + +function setup() { + run_setup_file_if_necessary +} + +function teardown() { + docker rm -f mail_dhparams + run_teardown_file_if_necessary +} + +function setup_file() { + # Delegated container setup to common_container_setup + # DRY - Explicit config changes between tests are more apparent this way. + + # Global scope + # Copies all of `./test/config/` to specific directory for testing + # `${PRIVATE_CONFIG}` becomes `$(pwd)/test/duplicate_configs/` + export PRIVATE_CONFIG + + export DMS_ONE_DIR=1 # default + + local DH_DEFAULT_PARAMS + export DH_DEFAULT_CHECKSUM + export DH_CUSTOM_PARAMS + export DH_CUSTOM_CHECKSUM + + + DH_DEFAULT_PARAMS="$(pwd)/target/shared/ffdhe4096.pem" + DH_DEFAULT_CHECKSUM="$(sha512sum "${DH_DEFAULT_PARAMS}" | awk '{print $1}')" + + DH_CUSTOM_PARAMS="$(pwd)/test/test-files/ssl/custom-dhe-params.pem" + DH_CUSTOM_CHECKSUM="$(sha512sum "${DH_CUSTOM_PARAMS}" | awk '{print $1}')" +} + +# Not used +# function teardown_file() { +# } + +@test "first" { + skip 'this test must come first to reliably identify when to run setup_file' +} + +@test "testing tls: DH Parameters - Verify integrity of Default (ffdhe4096)" { + # Reference used (22/04/2020): + # https://english.ncsc.nl/publications/publications/2019/juni/01/it-security-guidelines-for-transport-layer-security-tls + + run echo "${DH_DEFAULT_CHECKSUM}" + refute_output '' # checksum must not be empty + + # Verify the FFDHE params file has not been modified (equivalent to `target/shared/ffdhe4096.pem.sha512sum`): + local DH_MOZILLA_CHECKSUM + DH_MOZILLA_CHECKSUM="$(curl https://ssl-config.mozilla.org/ffdhe4096.txt -s | sha512sum | awk '{print $1}')" + assert_equal "${DH_DEFAULT_CHECKSUM}" "${DH_MOZILLA_CHECKSUM}" +} + +@test "testing tls: DH Parameters - Default [ONE_DIR=0]" { + PRIVATE_CONFIG="$(duplicate_config_for_container . mail_dhparams_default_0)" + DMS_ONE_DIR=0 + + common_container_setup + should_have_valid_checksum "${DH_DEFAULT_CHECKSUM}" +} + +@test "testing tls: DH Parameters - Default [ONE_DIR=1]" { + PRIVATE_CONFIG="$(duplicate_config_for_container . mail_dhparams_default_1)" + + common_container_setup + should_have_valid_checksum "${DH_DEFAULT_CHECKSUM}" +} + +@test "testing tls: DH Parameters - Custom [ONE_DIR=0]" { + PRIVATE_CONFIG="$(duplicate_config_for_container . mail_dhparams_custom_0)" + # shellcheck disable=SC2030 + DMS_ONE_DIR=0 + + cp "${DH_CUSTOM_PARAMS}" "${PRIVATE_CONFIG}/dhparams.pem" + + common_container_setup + should_have_valid_checksum "${DH_CUSTOM_CHECKSUM}" + should_emit_warning +} + +@test "testing tls: DH Parameters - Custom [ONE_DIR=1]" { + # shellcheck disable=SC2030 + PRIVATE_CONFIG="$(duplicate_config_for_container . mail_dhparams_custom_1)" + + cp "${DH_CUSTOM_PARAMS}" "${PRIVATE_CONFIG}/dhparams.pem" + + common_container_setup + should_have_valid_checksum "${DH_CUSTOM_CHECKSUM}" + should_emit_warning +} + +@test "last" { + skip 'this test is only there to reliably mark the end for the teardown_file' +} + +function common_container_setup() { + # shellcheck disable=SC2031 + docker run -d --name mail_dhparams \ + -v "${PRIVATE_CONFIG}:/tmp/docker-mailserver" \ + -v "$(pwd)/test/test-files:/tmp/docker-mailserver-test:ro" \ + -e DMS_DEBUG=0 \ + -e ONE_DIR="${DMS_ONE_DIR}" \ + -h mail.my-domain.com \ + --tty \ + "${NAME}" + + wait_for_finished_setup_in_container mail_dhparams +} + +# Ensures the docker image services (Postfix and Dovecot) have the intended DH files +function should_have_valid_checksum() { + local DH_CHECKSUM=$1 + + local DH_CHECKSUM_DOVECOT + DH_CHECKSUM_DOVECOT=$(docker exec mail_dhparams sha512sum /etc/dovecot/dh.pem | awk '{print $1}') + assert_equal "${DH_CHECKSUM_DOVECOT}" "${DH_CHECKSUM}" + + local DH_CHECKSUM_POSTFIX + DH_CHECKSUM_POSTFIX=$(docker exec mail_dhparams sha512sum /etc/postfix/dhparams.pem | awk '{print $1}') + assert_equal "${DH_CHECKSUM_POSTFIX}" "${DH_CHECKSUM}" +} + +function should_emit_warning() { + run sh -c "docker logs mail_dhparams | grep 'Using self-generated dhparams is considered insecure.'" + assert_success +}