From ec8b99335ea3d1d9754217cf4826582caf0c0bec Mon Sep 17 00:00:00 2001 From: Georg Lauterbach <44545919+georglauterbach@users.noreply.github.com> Date: Fri, 18 Feb 2022 11:29:51 +0100 Subject: [PATCH] Add changedetector functionality for `${SSL_TYPE} == manual` (#2404) Now, setups that use `SSL_TYPE=manual` will profit from the changedetector as well. Certificate changes are picked up and properly propagated. --- docs/content/config/advanced/auth-ldap.md | 2 +- docs/content/config/environment.md | 2 +- target/scripts/check-for-changes.sh | 18 +- target/scripts/helper-functions.sh | 86 +--- target/scripts/helpers/index.sh | 1 + target/scripts/helpers/ssl.sh | 499 ++++++++++++++++++++++ target/scripts/startup/setup-stack.sh | 408 ------------------ test/mail_ssl_letsencrypt.bats | 2 +- test/mail_ssl_manual.bats | 21 +- 9 files changed, 545 insertions(+), 494 deletions(-) create mode 100644 target/scripts/helpers/ssl.sh diff --git a/docs/content/config/advanced/auth-ldap.md b/docs/content/config/advanced/auth-ldap.md index 10e450f2..598ddfa6 100644 --- a/docs/content/config/advanced/auth-ldap.md +++ b/docs/content/config/advanced/auth-ldap.md @@ -96,7 +96,7 @@ Set this to `yes` to enable authentication binds ([more details in the dovecot d ### `SASLAUTHD_LDAP_FILTER` -This filter is used for `saslauthd`, which is called by postfix when someone is authenticating through SMTP (assuming that `SASLAUTHD_MECHANISMS=ldap` is being used). Note that you'll need to set up the LDAP server for saslauthd seperately from postfix. +This filter is used for `saslauthd`, which is called by postfix when someone is authenticating through SMTP (assuming that `SASLAUTHD_MECHANISMS=ldap` is being used). Note that you'll need to set up the LDAP server for saslauthd separately from postfix. The filter variables are explained in detail [in the `LDAP_SASLAUTHD` file](https://github.com/winlibs/cyrus-sasl/blob/master/saslauthd/LDAP_SASLAUTHD#L121), but unfortunately, this method doesn't really support domains right now - that means that `%U` is the only token that makes sense in this variable. diff --git a/docs/content/config/environment.md b/docs/content/config/environment.md index 63df4ba9..19c2502c 100644 --- a/docs/content/config/environment.md +++ b/docs/content/config/environment.md @@ -415,7 +415,7 @@ Note: activate this only if you are confident in your bayes database for identif ##### FETCHMAIL_PARALLEL **0** => `fetchmail` runs with a single config file `/etc/fetchmailrc` - **1** => `/etc/fetchmailrc` is split per poll entry. For every poll entry a seperate fetchmail instance is started to allow having multiple imap idle configurations defined. + **1** => `/etc/fetchmailrc` is split per poll entry. For every poll entry a separate fetchmail instance is started to allow having multiple imap idle configurations defined. Note: The defaults of your fetchmailrc file need to be at the top of the file. Otherwise it won't be added correctly to all separate `fetchmail` instances. diff --git a/target/scripts/check-for-changes.sh b/target/scripts/check-for-changes.sh index 65dc575b..b1ff9f6e 100755 --- a/target/scripts/check-for-changes.sh +++ b/target/scripts/check-for-changes.sh @@ -40,6 +40,8 @@ _obtain_hostname_and_domainname PM_ADDRESS="${POSTMASTER_ADDRESS:=postmaster@${DOMAINNAME}}" _notify 'inf' "${LOG_DATE} Using postmaster address ${PM_ADDRESS}" +REGEX_NEVER_MATCH="(?\!)" + # Change detection delayed during startup to avoid conflicting writes sleep 10 @@ -65,10 +67,24 @@ do # Also note that changes are performed in place and are not atomic # We should fix that and write to temporary files, stop, swap and start + if [[ ${SSL_TYPE} == 'manual' ]] + then + # only run the SSL setup again if certificates have really changed. + if [[ ${CHANGED} =~ ${SSL_CERT_PATH:-${REGEX_NEVER_MATCH}} ]] \ + || [[ ${CHANGED} =~ ${SSL_KEY_PATH:-${REGEX_NEVER_MATCH}} ]] \ + || [[ ${CHANGED} =~ ${SSL_ALT_CERT_PATH:-${REGEX_NEVER_MATCH}} ]] \ + || [[ ${CHANGED} =~ ${SSL_ALT_KEY_PATH:-${REGEX_NEVER_MATCH}} ]] + then + _notify 'inf' "Manual certificates have changed, extracting certs.." + # we need to run the SSL setup again, because the + # certificates DMS is working with are copies of + # the (now changed) files + _setup_ssl + fi # `acme.json` is only relevant to Traefik, and is where it stores the certificates it manages. # When a change is detected it's assumed to be a possible cert renewal that needs to be # extracted for `docker-mailserver` services to adjust to. - if [[ ${CHANGED} =~ /etc/letsencrypt/acme.json ]] + elif [[ ${CHANGED} =~ /etc/letsencrypt/acme.json ]] then _notify 'inf' "'/etc/letsencrypt/acme.json' has changed, extracting certs.." diff --git a/target/scripts/helper-functions.sh b/target/scripts/helper-functions.sh index 1ff133d0..e9b39152 100755 --- a/target/scripts/helper-functions.sh +++ b/target/scripts/helper-functions.sh @@ -1,5 +1,9 @@ #! /bin/bash +# TODO this file may be split up in the future +# into separate files under `target/scripts/helper/` +# which is a more fitting place + # These helpers are used by `setup-stack.sh` and `check-for-changes.sh`, # not by anything within `helper-functions.sh` itself: # shellcheck source=target/scripts/helpers/index.sh @@ -9,6 +13,10 @@ DMS_DEBUG="${DMS_DEBUG:=0}" SCRIPT_NAME="$(basename "$0")" # This becomes the sourcing script name (Example: check-for-changes.sh) LOCK_ID="$(uuid)" # Used inside of lock files to identify them and prevent removal by other instances of docker-mailserver +# file storing the checksums of the monitored files. +# shellcheck disable=SC2034 +CHKSUM_FILE=/tmp/docker-mailserver-config-chksum + # ? --------------------------------------------- BIN HELPER function errex @@ -151,47 +159,6 @@ function _sanitize_ipv4_to_subnet_cidr } export -f _sanitize_ipv4_to_subnet_cidr -# ? --------------------------------------------- ACME - -function _extract_certs_from_acme -{ - local CERT_DOMAIN=${1} - if [[ -z ${CERT_DOMAIN} ]] - then - _notify 'err' "_extract_certs_from_acme | CERT_DOMAIN is empty" - return 1 - fi - - local KEY CERT - KEY=$(acme_extract /etc/letsencrypt/acme.json "${CERT_DOMAIN}" --key) - CERT=$(acme_extract /etc/letsencrypt/acme.json "${CERT_DOMAIN}" --cert) - - if [[ -z ${KEY} ]] || [[ -z ${CERT} ]] - then - _notify 'warn' "_extract_certs_from_acme | Unable to find key and/or cert for '${CERT_DOMAIN}' in '/etc/letsencrypt/acme.json'" - return 1 - fi - - # Currently we advise SSL_DOMAIN for wildcard support using a `*.example.com` value, - # The filepath however should be `example.com`, avoiding the wildcard part: - if [[ ${SSL_DOMAIN} == "${CERT_DOMAIN}" ]] - then - CERT_DOMAIN=$(_strip_wildcard_prefix "${SSL_DOMAIN}") - fi - - mkdir -p "/etc/letsencrypt/live/${CERT_DOMAIN}/" - echo "${KEY}" | base64 -d > "/etc/letsencrypt/live/${CERT_DOMAIN}/key.pem" || exit 1 - echo "${CERT}" | base64 -d > "/etc/letsencrypt/live/${CERT_DOMAIN}/fullchain.pem" || exit 1 - - _notify 'inf' "_extract_certs_from_acme | Certificate successfully extracted for '${CERT_DOMAIN}'" -} -export -f _extract_certs_from_acme - -# Remove the `*.` prefix if it exists, else returns the input value -function _strip_wildcard_prefix { - [[ ${1} == "*."* ]] && echo "${1:2}" || echo "${1}" -} - # ? --------------------------------------------- Notifications function _notify @@ -218,43 +185,6 @@ function _notify } export -f _notify -# ? --------------------------------------------- File Checksums - -# file storing the checksums of the monitored files. -# shellcheck disable=SC2034 -CHKSUM_FILE=/tmp/docker-mailserver-config-chksum - -# Compute checksums of monitored files, -# returned output on `stdout`: hash + filepath tuple on each line -function _monitored_files_checksums -{ - # If a wildcard path pattern (or an empty ENV) would yield an invalid path - # or no results, `shopt -s nullglob` prevents it from being added. - shopt -s nullglob - declare -a CERT_FILES - - # React to any cert changes within the following letsencrypt locations: - CERT_FILES=( - /etc/letsencrypt/live/"${SSL_DOMAIN}"/*.pem - /etc/letsencrypt/live/"${HOSTNAME}"/*.pem - /etc/letsencrypt/live/"${DOMAINNAME}"/*.pem - ) - - if [[ ! -d /tmp/docker-mailserver ]] - then - return 1 - fi - - sha512sum 2>/dev/null -- \ - /tmp/docker-mailserver/postfix-accounts.cf \ - /tmp/docker-mailserver/postfix-virtual.cf \ - /tmp/docker-mailserver/postfix-aliases.cf \ - /tmp/docker-mailserver/dovecot-quotas.cf \ - /etc/letsencrypt/acme.json \ - "${CERT_FILES[@]}" -} -export -f _monitored_files_checksums - # ? --------------------------------------------- General # Outputs the DNS label count (delimited by `.`) for the given input string. diff --git a/target/scripts/helpers/index.sh b/target/scripts/helpers/index.sh index 699e1b2b..2fc2c289 100755 --- a/target/scripts/helpers/index.sh +++ b/target/scripts/helpers/index.sh @@ -11,5 +11,6 @@ function _import_scripts . "${PATH_TO_SCRIPTS}/aliases.sh" . "${PATH_TO_SCRIPTS}/relay.sh" . "${PATH_TO_SCRIPTS}/sasl.sh" + . "${PATH_TO_SCRIPTS}/ssl.sh" } _import_scripts diff --git a/target/scripts/helpers/ssl.sh b/target/scripts/helpers/ssl.sh new file mode 100644 index 00000000..c19b8227 --- /dev/null +++ b/target/scripts/helpers/ssl.sh @@ -0,0 +1,499 @@ +#! /bin/bash + +function _setup_ssl +{ + _notify 'task' 'Setting up SSL' + + local POSTFIX_CONFIG_MAIN='/etc/postfix/main.cf' + local POSTFIX_CONFIG_MASTER='/etc/postfix/master.cf' + local DOVECOT_CONFIG_SSL='/etc/dovecot/conf.d/10-ssl.conf' + + local TMP_DMS_TLS_PATH='/tmp/docker-mailserver/ssl' # config volume + local DMS_TLS_PATH='/etc/dms/tls' + mkdir -p "${DMS_TLS_PATH}" + + # Primary certificate to serve for TLS + function _set_certificate + { + local POSTFIX_KEY_WITH_FULLCHAIN=${1} + local DOVECOT_KEY=${1} + local DOVECOT_CERT=${1} + + # If a 2nd param is provided, a separate key and cert was received instead of a fullkeychain + if [[ -n ${2} ]] + then + local PRIVATE_KEY=$1 + local CERT_CHAIN=$2 + + POSTFIX_KEY_WITH_FULLCHAIN="${PRIVATE_KEY} ${CERT_CHAIN}" + DOVECOT_KEY="${PRIVATE_KEY}" + DOVECOT_CERT="${CERT_CHAIN}" + fi + + # Postfix configuration + # NOTE: `smtpd_tls_chain_files` expects private key defined before public cert chain + # Value can be a single PEM file, or a sequence of files; so long as the order is key->leaf->chain + sedfile -i -r "s|^(smtpd_tls_chain_files =).*|\1 ${POSTFIX_KEY_WITH_FULLCHAIN}|" "${POSTFIX_CONFIG_MAIN}" + + # Dovecot configuration + sedfile -i -r \ + -e "s|^(ssl_key =).*|\1 <${DOVECOT_KEY}|" \ + -e "s|^(ssl_cert =).*|\1 <${DOVECOT_CERT}|" \ + "${DOVECOT_CONFIG_SSL}" + } + + # Enables supporting two certificate types such as ECDSA with an RSA fallback + function _set_alt_certificate + { + local COPY_KEY_FROM_PATH=$1 + local COPY_CERT_FROM_PATH=$2 + local PRIVATE_KEY_ALT="${DMS_TLS_PATH}/fallback_key" + local CERT_CHAIN_ALT="${DMS_TLS_PATH}/fallback_cert" + + cp "${COPY_KEY_FROM_PATH}" "${PRIVATE_KEY_ALT}" + cp "${COPY_CERT_FROM_PATH}" "${CERT_CHAIN_ALT}" + chmod 600 "${PRIVATE_KEY_ALT}" + chmod 644 "${CERT_CHAIN_ALT}" + + # Postfix configuration + # NOTE: This operation doesn't replace the line, it appends to the end of the line. + # Thus this method should only be used when this line has explicitly been replaced earlier in the script. + # Otherwise without `docker-compose down` first, a `docker-compose up` may + # persist previous container state and cause a failure in postfix configuration. + sedfile -i "s|^smtpd_tls_chain_files =.*|& ${PRIVATE_KEY_ALT} ${CERT_CHAIN_ALT}|" "${POSTFIX_CONFIG_MAIN}" + + # Dovecot configuration + # Conditionally checks for `#`, in the event that internal container state is accidentally persisted, + # can be caused by: `docker-compose up` run again after a `ctrl+c`, without running `docker-compose down` + sedfile -i -r \ + -e "s|^#?(ssl_alt_key =).*|\1 <${PRIVATE_KEY_ALT}|" \ + -e "s|^#?(ssl_alt_cert =).*|\1 <${CERT_CHAIN_ALT}|" \ + "${DOVECOT_CONFIG_SSL}" + } + + function _apply_tls_level + { + local TLS_CIPHERS_ALLOW=$1 + local TLS_PROTOCOL_IGNORE=$2 + local TLS_PROTOCOL_MINIMUM=$3 + + # Postfix configuration + sed -i -r \ + -e "s|^(smtpd?_tls_mandatory_protocols =).*|\1 ${TLS_PROTOCOL_IGNORE}|" \ + -e "s|^(smtpd?_tls_protocols =).*|\1 ${TLS_PROTOCOL_IGNORE}|" \ + -e "s|^(tls_high_cipherlist =).*|\1 ${TLS_CIPHERS_ALLOW}|" \ + "${POSTFIX_CONFIG_MAIN}" + + # Dovecot configuration (secure by default though) + sed -i -r \ + -e "s|^(ssl_min_protocol =).*|\1 ${TLS_PROTOCOL_MINIMUM}|" \ + -e "s|^(ssl_cipher_list =).*|\1 ${TLS_CIPHERS_ALLOW}|" \ + "${DOVECOT_CONFIG_SSL}" + } + + # 2020 feature intended for Traefik v2 support only: + # https://github.com/docker-mailserver/docker-mailserver/pull/1553 + # Extracts files `key.pem` and `fullchain.pem`. + # `_extract_certs_from_acme` is located in `helper-functions.sh` + # NOTE: See the `SSL_TYPE=letsencrypt` case below for more details. + function _traefik_support + { + if [[ -f /etc/letsencrypt/acme.json ]] + then + # Variable only intended for troubleshooting via debug output + local EXTRACTED_DOMAIN + + # Conditional handling depends on the success of `_extract_certs_from_acme`, + # Failure tries the next fallback FQDN to try extract a certificate from. + # Subshell not used in conditional to ensure extraction log output is still captured + if [[ -n ${SSL_DOMAIN} ]] && _extract_certs_from_acme "${SSL_DOMAIN}" + then + EXTRACTED_DOMAIN=('SSL_DOMAIN' "${SSL_DOMAIN}") + elif _extract_certs_from_acme "${HOSTNAME}" + then + EXTRACTED_DOMAIN=('HOSTNAME' "${HOSTNAME}") + elif _extract_certs_from_acme "${DOMAINNAME}" + then + EXTRACTED_DOMAIN=('DOMAINNAME' "${DOMAINNAME}") + else + _notify 'err' "'setup-stack.sh' | letsencrypt (acme.json) failed to identify a certificate to extract" + fi + + _notify 'inf' "'setup-stack.sh' | letsencrypt (acme.json) extracted certificate using ${EXTRACTED_DOMAIN[0]}: '${EXTRACTED_DOMAIN[1]}'" + fi + } + + # TLS strength/level configuration + case "${TLS_LEVEL}" in + ( "modern" ) + local TLS_MODERN_SUITE='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' + local TLS_MODERN_IGNORE='!SSLv2,!SSLv3,!TLSv1,!TLSv1.1' + local TLS_MODERN_MIN='TLSv1.2' + + _apply_tls_level "${TLS_MODERN_SUITE}" "${TLS_MODERN_IGNORE}" "${TLS_MODERN_MIN}" + + _notify 'inf' "TLS configured with 'modern' ciphers" + ;; + + ( "intermediate" ) + local TLS_INTERMEDIATE_SUITE='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-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA' + local TLS_INTERMEDIATE_IGNORE='!SSLv2,!SSLv3' + local TLS_INTERMEDIATE_MIN='TLSv1' + + _apply_tls_level "${TLS_INTERMEDIATE_SUITE}" "${TLS_INTERMEDIATE_IGNORE}" "${TLS_INTERMEDIATE_MIN}" + + # Lowers the minimum acceptable TLS version connection to `TLSv1` (from Debian upstream `TLSv1.2`) + # Lowers Security Level to `1` (from Debian upstream `2`, openssl release defaults to `1`) + # https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html + # https://wiki.debian.org/ContinuousIntegration/TriagingTips/openssl-1.1.1 + # https://dovecot.org/pipermail/dovecot/2020-October/120225.html + # TODO: This is a fix for Debian Bullseye Dovecot. Can remove when we only support TLS >=1.2. + # WARNING: This applies to all processes that use openssl and respect these settings. + sedfile -i -r \ + -e 's|^(MinProtocol).*|\1 = TLSv1|' \ + -e 's|^(CipherString).*|\1 = DEFAULT@SECLEVEL=1|' \ + /usr/lib/ssl/openssl.cnf + + _notify 'inf' "TLS configured with 'intermediate' ciphers" + ;; + + ( * ) + _notify 'err' "TLS_LEVEL not found [ in ${FUNCNAME[0]} ]" + ;; + + esac + + local SCOPE_SSL_TYPE="TLS Setup [SSL_TYPE=${SSL_TYPE}]" + # SSL certificate Configuration + # TODO: Refactor this feature, it's been extended multiple times for specific inputs/providers unnecessarily. + # NOTE: Some `SSL_TYPE` logic uses mounted certs/keys directly, some make an internal copy either retaining filename or renaming. + case "${SSL_TYPE}" in + ( "letsencrypt" ) + _notify 'inf' "Configuring SSL using 'letsencrypt'" + + # `docker-mailserver` will only use one certificate from an FQDN folder in `/etc/letsencrypt/live/`. + # We iterate the sequence [SSL_DOMAIN, HOSTNAME, DOMAINNAME] to find a matching FQDN folder. + # This same sequence is used for the Traefik `acme.json` certificate extraction process, which outputs the FQDN folder. + # + # eg: If HOSTNAME (mail.example.test) doesn't exist, try DOMAINNAME (example.test). + # SSL_DOMAIN if set will take priority and is generally expected to have a wildcard prefix. + # SSL_DOMAIN will have any wildcard prefix stripped for the output FQDN folder it is stored in. + # TODO: A wildcard cert needs to be provisioned via Traefik to validate if acme.json contains any other value for `main` or `sans` beyond the wildcard. + # + # NOTE: HOSTNAME is set via `helper-functions.sh`, it is not the original system HOSTNAME ENV anymore. + # TODO: SSL_DOMAIN is Traefik specific, it no longer seems relevant and should be considered for removal. + + _traefik_support + + # letsencrypt folders and files mounted in /etc/letsencrypt + local LETSENCRYPT_DOMAIN + local LETSENCRYPT_KEY + + # Identify a valid letsencrypt FQDN folder to use. + if [[ -n ${SSL_DOMAIN} ]] && [[ -e /etc/letsencrypt/live/$(_strip_wildcard_prefix "${SSL_DOMAIN}")/fullchain.pem ]] + then + LETSENCRYPT_DOMAIN=$(_strip_wildcard_prefix "${SSL_DOMAIN}") + elif [[ -e /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem ]] + then + LETSENCRYPT_DOMAIN=${HOSTNAME} + elif [[ -e /etc/letsencrypt/live/${DOMAINNAME}/fullchain.pem ]] + then + LETSENCRYPT_DOMAIN=${DOMAINNAME} + else + _notify 'err' "Cannot find a valid DOMAIN for '/etc/letsencrypt/live//', tried: '${SSL_DOMAIN}', '${HOSTNAME}', '${DOMAINNAME}'" + dms_panic__misconfigured 'LETSENCRYPT_DOMAIN' "${SCOPE_SSL_TYPE}" + return 1 + fi + + # Verify the FQDN folder also includes a valid private key (`privkey.pem` for Certbot, `key.pem` for extraction by Traefik) + if [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/privkey.pem ]] + then + LETSENCRYPT_KEY='privkey' + elif [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/key.pem ]] + then + LETSENCRYPT_KEY='key' + else + _notify 'err' "Cannot find key file ('privkey.pem' or 'key.pem') in '/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/'" + dms_panic__misconfigured 'LETSENCRYPT_KEY' "${SCOPE_SSL_TYPE}" + return 1 + fi + + # Update relevant config for Postfix and Dovecot + _notify 'inf' "Adding ${LETSENCRYPT_DOMAIN} SSL certificate to the postfix and dovecot configuration" + + # LetsEncrypt `fullchain.pem` and `privkey.pem` contents are detailed here from CertBot: + # https://certbot.eff.org/docs/using.html#where-are-my-certificates + # `key.pem` was added for `simp_le` support (2016): https://github.com/docker-mailserver/docker-mailserver/pull/288 + # `key.pem` is also a filename used by the `_extract_certs_from_acme` method (implemented for Traefik v2 only) + local PRIVATE_KEY="/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/${LETSENCRYPT_KEY}.pem" + local CERT_CHAIN="/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/fullchain.pem" + + _set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}" + + _notify 'inf' "SSL configured with 'letsencrypt' certificates" + ;; + + ( "custom" ) # (hard-coded path) Use a private key with full certificate chain all in a single PEM file. + _notify 'inf' "Adding ${HOSTNAME} SSL certificate" + + # NOTE: Dovecot works fine still as both values are bundled into the keychain + local COMBINED_PEM_NAME="${HOSTNAME}-full.pem" + local TMP_KEY_WITH_FULLCHAIN="${TMP_DMS_TLS_PATH}/${COMBINED_PEM_NAME}" + local KEY_WITH_FULLCHAIN="${DMS_TLS_PATH}/${COMBINED_PEM_NAME}" + + if [[ -f ${TMP_KEY_WITH_FULLCHAIN} ]] + then + cp "${TMP_KEY_WITH_FULLCHAIN}" "${KEY_WITH_FULLCHAIN}" + chmod 600 "${KEY_WITH_FULLCHAIN}" + + _set_certificate "${KEY_WITH_FULLCHAIN}" + + _notify 'inf' "SSL configured with 'CA signed/custom' certificates" + else + dms_panic__no_file "${TMP_KEY_WITH_FULLCHAIN}" "${SCOPE_SSL_TYPE}" + fi + ;; + + ( "manual" ) # (dynamic path via ENV) Use separate private key and cert/chain files (should be PEM encoded) + _notify 'inf' "Configuring certificates using key ${SSL_KEY_PATH} and cert ${SSL_CERT_PATH}" + + # Source files are copied internally to these destinations: + local PRIVATE_KEY="${DMS_TLS_PATH}/key" + local CERT_CHAIN="${DMS_TLS_PATH}/cert" + + # Fail early: + if [[ -z ${SSL_KEY_PATH} ]] && [[ -z ${SSL_CERT_PATH} ]] + then + dms_panic__no_env 'SSL_KEY_PATH or SSL_CERT_PATH' "${SCOPE_SSL_TYPE}" + fi + + if [[ -n ${SSL_ALT_KEY_PATH} ]] \ + && [[ -n ${SSL_ALT_CERT_PATH} ]] \ + && [[ ! -f ${SSL_ALT_KEY_PATH} ]] \ + && [[ ! -f ${SSL_ALT_CERT_PATH} ]] + then + dms_panic__no_file "(ALT) ${SSL_ALT_KEY_PATH} or ${SSL_ALT_CERT_PATH}" "${SCOPE_SSL_TYPE}" + fi + + if [[ -f ${SSL_KEY_PATH} ]] && [[ -f ${SSL_CERT_PATH} ]] + then + cp "${SSL_KEY_PATH}" "${PRIVATE_KEY}" + cp "${SSL_CERT_PATH}" "${CERT_CHAIN}" + chmod 600 "${PRIVATE_KEY}" + chmod 644 "${CERT_CHAIN}" + + _set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}" + + # Support for a fallback certificate, useful for hybrid/dual ECDSA + RSA certs + if [[ -n ${SSL_ALT_KEY_PATH} ]] && [[ -n ${SSL_ALT_CERT_PATH} ]] + then + _notify 'inf' "Configuring fallback certificates using key ${SSL_ALT_KEY_PATH} and cert ${SSL_ALT_CERT_PATH}" + + _set_alt_certificate "${SSL_ALT_KEY_PATH}" "${SSL_ALT_CERT_PATH}" + else + # If the Dovecot settings for alt cert has been enabled (doesn't start with `#`), + # but required ENV var is missing, reset to disabled state: + sed -i -r \ + -e 's|^(ssl_alt_key =).*|#\1 The plaintext authentication is always allowed (and SSL not required) for connections from localhost, as they’re assumed to be secure anyway. + # > This applies to all connections where the local and the remote IP addresses are equal. + # > Also IP ranges specified by login_trusted_networks setting are assumed to be secure. + # + # no => insecure auth allowed, yes (default) => plaintext auth only allowed over a secure connection (insecure connection acceptable for non-plaintext auth) + local DISABLE_PLAINTEXT_AUTH='no' + # no => disabled, yes => optional (secure connections not required), required (default) => mandatory (only secure connections allowed) + local DOVECOT_SSL_ENABLED='no' + sed -i -r "s|^#?(disable_plaintext_auth =).*|\1 ${DISABLE_PLAINTEXT_AUTH}|" /etc/dovecot/conf.d/10-auth.conf + sed -i -r "s|^(ssl =).*|\1 ${DOVECOT_SSL_ENABLED}|" "${DOVECOT_CONFIG_SSL}" + ;; + + ( 'snakeoil' ) # This is a temporary workaround for testing only, using the insecure snakeoil cert. + # mail_privacy.bats and mail_with_ldap.bats both attempt to make a starttls connection with openssl, + # failing if SSL/TLS is not available. + ;; + + ( * ) # Unknown option, panic. + dms_panic__invalid_value 'SSL_TYPE' "${SCOPE_TLS_LEVEL}" + ;; + + esac +} +export -f _setup_ssl + +function _extract_certs_from_acme +{ + local CERT_DOMAIN=${1} + if [[ -z ${CERT_DOMAIN} ]] + then + _notify 'err' "_extract_certs_from_acme | CERT_DOMAIN is empty" + return 1 + fi + + local KEY CERT + KEY=$(acme_extract /etc/letsencrypt/acme.json "${CERT_DOMAIN}" --key) + CERT=$(acme_extract /etc/letsencrypt/acme.json "${CERT_DOMAIN}" --cert) + + if [[ -z ${KEY} ]] || [[ -z ${CERT} ]] + then + _notify 'warn' "_extract_certs_from_acme | Unable to find key and/or cert for '${CERT_DOMAIN}' in '/etc/letsencrypt/acme.json'" + return 1 + fi + + # Currently we advise SSL_DOMAIN for wildcard support using a `*.example.com` value, + # The filepath however should be `example.com`, avoiding the wildcard part: + if [[ ${SSL_DOMAIN} == "${CERT_DOMAIN}" ]] + then + CERT_DOMAIN=$(_strip_wildcard_prefix "${SSL_DOMAIN}") + fi + + mkdir -p "/etc/letsencrypt/live/${CERT_DOMAIN}/" + echo "${KEY}" | base64 -d > "/etc/letsencrypt/live/${CERT_DOMAIN}/key.pem" || exit 1 + echo "${CERT}" | base64 -d > "/etc/letsencrypt/live/${CERT_DOMAIN}/fullchain.pem" || exit 1 + + _notify 'inf' "_extract_certs_from_acme | Certificate successfully extracted for '${CERT_DOMAIN}'" +} +export -f _extract_certs_from_acme + +# Remove the `*.` prefix if it exists, else returns the input value +function _strip_wildcard_prefix { + [[ ${1} == "*."* ]] && echo "${1:2}" || echo "${1}" +} +export -f _strip_wildcard_prefix + +# Compute checksums of monitored files, +# returned output on `stdout`: hash + filepath tuple on each line +function _monitored_files_checksums +{ + local DMS_DIR=/tmp/docker-mailserver + [[ -d ${DMS_DIR} ]] || return 1 + + # If a wildcard path pattern (or an empty ENV) would yield an invalid path + # or no results, `shopt -s nullglob` prevents it from being added. + shopt -s nullglob + declare -a STAGING_FILES CHANGED_FILES + + STAGING_FILES=( + "${DMS_DIR}/postfix-accounts.cf" + "${DMS_DIR}/postfix-virtual.cf" + "${DMS_DIR}/postfix-aliases.cf" + "${DMS_DIR}/dovecot-quotas.cf" + ) + + if [[ ${SSL_TYPE:-} == 'manual' ]] + then + # When using "manual" as the SSL type, + # the following variables may contain the certificate files + STAGING_FILES+=( + "${SSL_CERT_PATH:-}" + "${SSL_KEY_PATH:-}" + "${SSL_ALT_CERT_PATH:-}" + "${SSL_ALT_KEY_PATH:-}" + ) + elif [[ ${SSL_TYPE:-} == 'letsencrypt' ]] + then + # React to any cert changes within the following Let'sEncrypt locations: + STAGING_FILES+=( + /etc/letsencrypt/acme.json + /etc/letsencrypt/live/"${SSL_DOMAIN}"/*.pem + /etc/letsencrypt/live/"${HOSTNAME}"/*.pem + /etc/letsencrypt/live/"${DOMAINNAME}"/*.pem + ) + fi + + for FILE in "${STAGING_FILES[@]}" + do + [[ -f "${FILE}" ]] && CHANGED_FILES+=("${FILE}") + done + + sha512sum -- "${CHANGED_FILES[@]}" +} +export -f _monitored_files_checksums diff --git a/target/scripts/startup/setup-stack.sh b/target/scripts/startup/setup-stack.sh index 6d25c165..814b4e65 100644 --- a/target/scripts/startup/setup-stack.sh +++ b/target/scripts/startup/setup-stack.sh @@ -691,414 +691,6 @@ function _setup_dkim fi } -function _setup_ssl -{ - _notify 'task' 'Setting up SSL' - - local POSTFIX_CONFIG_MAIN='/etc/postfix/main.cf' - local POSTFIX_CONFIG_MASTER='/etc/postfix/master.cf' - local DOVECOT_CONFIG_SSL='/etc/dovecot/conf.d/10-ssl.conf' - - local TMP_DMS_TLS_PATH='/tmp/docker-mailserver/ssl' # config volume - local DMS_TLS_PATH='/etc/dms/tls' - mkdir -p "${DMS_TLS_PATH}" - - # Primary certificate to serve for TLS - function _set_certificate - { - local POSTFIX_KEY_WITH_FULLCHAIN=${1} - local DOVECOT_KEY=${1} - local DOVECOT_CERT=${1} - - # If a 2nd param is provided, a separate key and cert was received instead of a fullkeychain - if [[ -n ${2} ]] - then - local PRIVATE_KEY=$1 - local CERT_CHAIN=$2 - - POSTFIX_KEY_WITH_FULLCHAIN="${PRIVATE_KEY} ${CERT_CHAIN}" - DOVECOT_KEY="${PRIVATE_KEY}" - DOVECOT_CERT="${CERT_CHAIN}" - fi - - # Postfix configuration - # NOTE: `smtpd_tls_chain_files` expects private key defined before public cert chain - # Value can be a single PEM file, or a sequence of files; so long as the order is key->leaf->chain - sedfile -i -r "s|^(smtpd_tls_chain_files =).*|\1 ${POSTFIX_KEY_WITH_FULLCHAIN}|" "${POSTFIX_CONFIG_MAIN}" - - # Dovecot configuration - sedfile -i -r \ - -e "s|^(ssl_key =).*|\1 <${DOVECOT_KEY}|" \ - -e "s|^(ssl_cert =).*|\1 <${DOVECOT_CERT}|" \ - "${DOVECOT_CONFIG_SSL}" - } - - # Enables supporting two certificate types such as ECDSA with an RSA fallback - function _set_alt_certificate - { - local COPY_KEY_FROM_PATH=$1 - local COPY_CERT_FROM_PATH=$2 - local PRIVATE_KEY_ALT="${DMS_TLS_PATH}/fallback_key" - local CERT_CHAIN_ALT="${DMS_TLS_PATH}/fallback_cert" - - cp "${COPY_KEY_FROM_PATH}" "${PRIVATE_KEY_ALT}" - cp "${COPY_CERT_FROM_PATH}" "${CERT_CHAIN_ALT}" - chmod 600 "${PRIVATE_KEY_ALT}" - chmod 644 "${CERT_CHAIN_ALT}" - - # Postfix configuration - # NOTE: This operation doesn't replace the line, it appends to the end of the line. - # Thus this method should only be used when this line has explicitly been replaced earlier in the script. - # Otherwise without `docker-compose down` first, a `docker-compose up` may - # persist previous container state and cause a failure in postfix configuration. - sedfile -i "s|^smtpd_tls_chain_files =.*|& ${PRIVATE_KEY_ALT} ${CERT_CHAIN_ALT}|" "${POSTFIX_CONFIG_MAIN}" - - # Dovecot configuration - # Conditionally checks for `#`, in the event that internal container state is accidentally persisted, - # can be caused by: `docker-compose up` run again after a `ctrl+c`, without running `docker-compose down` - sedfile -i -r \ - -e "s|^#?(ssl_alt_key =).*|\1 <${PRIVATE_KEY_ALT}|" \ - -e "s|^#?(ssl_alt_cert =).*|\1 <${CERT_CHAIN_ALT}|" \ - "${DOVECOT_CONFIG_SSL}" - } - - function _apply_tls_level - { - local TLS_CIPHERS_ALLOW=$1 - local TLS_PROTOCOL_IGNORE=$2 - local TLS_PROTOCOL_MINIMUM=$3 - - # Postfix configuration - sed -i -r \ - -e "s|^(smtpd?_tls_mandatory_protocols =).*|\1 ${TLS_PROTOCOL_IGNORE}|" \ - -e "s|^(smtpd?_tls_protocols =).*|\1 ${TLS_PROTOCOL_IGNORE}|" \ - -e "s|^(tls_high_cipherlist =).*|\1 ${TLS_CIPHERS_ALLOW}|" \ - "${POSTFIX_CONFIG_MAIN}" - - # Dovecot configuration (secure by default though) - sed -i -r \ - -e "s|^(ssl_min_protocol =).*|\1 ${TLS_PROTOCOL_MINIMUM}|" \ - -e "s|^(ssl_cipher_list =).*|\1 ${TLS_CIPHERS_ALLOW}|" \ - "${DOVECOT_CONFIG_SSL}" - } - - # 2020 feature intended for Traefik v2 support only: - # https://github.com/docker-mailserver/docker-mailserver/pull/1553 - # Extracts files `key.pem` and `fullchain.pem`. - # `_extract_certs_from_acme` is located in `helper-functions.sh` - # NOTE: See the `SSL_TYPE=letsencrypt` case below for more details. - function _traefik_support - { - if [[ -f /etc/letsencrypt/acme.json ]] - then - # Variable only intended for troubleshooting via debug output - local EXTRACTED_DOMAIN - - # Conditional handling depends on the success of `_extract_certs_from_acme`, - # Failure tries the next fallback FQDN to try extract a certificate from. - # Subshell not used in conditional to ensure extraction log output is still captured - if [[ -n ${SSL_DOMAIN} ]] && _extract_certs_from_acme "${SSL_DOMAIN}" - then - EXTRACTED_DOMAIN=('SSL_DOMAIN' "${SSL_DOMAIN}") - elif _extract_certs_from_acme "${HOSTNAME}" - then - EXTRACTED_DOMAIN=('HOSTNAME' "${HOSTNAME}") - elif _extract_certs_from_acme "${DOMAINNAME}" - then - EXTRACTED_DOMAIN=('DOMAINNAME' "${DOMAINNAME}") - else - _notify 'err' "'setup-stack.sh' | letsencrypt (acme.json) failed to identify a certificate to extract" - fi - - _notify 'inf' "'setup-stack.sh' | letsencrypt (acme.json) extracted certificate using ${EXTRACTED_DOMAIN[0]}: '${EXTRACTED_DOMAIN[1]}'" - fi - } - - # TLS strength/level configuration - case "${TLS_LEVEL}" in - ( "modern" ) - local TLS_MODERN_SUITE='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' - local TLS_MODERN_IGNORE='!SSLv2,!SSLv3,!TLSv1,!TLSv1.1' - local TLS_MODERN_MIN='TLSv1.2' - - _apply_tls_level "${TLS_MODERN_SUITE}" "${TLS_MODERN_IGNORE}" "${TLS_MODERN_MIN}" - - _notify 'inf' "TLS configured with 'modern' ciphers" - ;; - - ( "intermediate" ) - local TLS_INTERMEDIATE_SUITE='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-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA' - local TLS_INTERMEDIATE_IGNORE='!SSLv2,!SSLv3' - local TLS_INTERMEDIATE_MIN='TLSv1' - - _apply_tls_level "${TLS_INTERMEDIATE_SUITE}" "${TLS_INTERMEDIATE_IGNORE}" "${TLS_INTERMEDIATE_MIN}" - - # Lowers the minimum acceptable TLS version connection to `TLSv1` (from Debian upstream `TLSv1.2`) - # Lowers Security Level to `1` (from Debian upstream `2`, openssl release defaults to `1`) - # https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html - # https://wiki.debian.org/ContinuousIntegration/TriagingTips/openssl-1.1.1 - # https://dovecot.org/pipermail/dovecot/2020-October/120225.html - # TODO: This is a fix for Debian Bullseye Dovecot. Can remove when we only support TLS >=1.2. - # WARNING: This applies to all processes that use openssl and respect these settings. - sedfile -i -r \ - -e 's|^(MinProtocol).*|\1 = TLSv1|' \ - -e 's|^(CipherString).*|\1 = DEFAULT@SECLEVEL=1|' \ - /usr/lib/ssl/openssl.cnf - - _notify 'inf' "TLS configured with 'intermediate' ciphers" - ;; - - ( * ) - _notify 'err' "TLS_LEVEL not found [ in ${FUNCNAME[0]} ]" - ;; - - esac - - local SCOPE_SSL_TYPE="TLS Setup [SSL_TYPE=${SSL_TYPE}]" - # SSL certificate Configuration - # TODO: Refactor this feature, it's been extended multiple times for specific inputs/providers unnecessarily. - # NOTE: Some `SSL_TYPE` logic uses mounted certs/keys directly, some make an internal copy either retaining filename or renaming. - case "${SSL_TYPE}" in - ( "letsencrypt" ) - _notify 'inf' "Configuring SSL using 'letsencrypt'" - - # `docker-mailserver` will only use one certificate from an FQDN folder in `/etc/letsencrypt/live/`. - # We iterate the sequence [SSL_DOMAIN, HOSTNAME, DOMAINNAME] to find a matching FQDN folder. - # This same sequence is used for the Traefik `acme.json` certificate extraction process, which outputs the FQDN folder. - # - # eg: If HOSTNAME (mail.example.test) doesn't exist, try DOMAINNAME (example.test). - # SSL_DOMAIN if set will take priority and is generally expected to have a wildcard prefix. - # SSL_DOMAIN will have any wildcard prefix stripped for the output FQDN folder it is stored in. - # TODO: A wildcard cert needs to be provisioned via Traefik to validate if acme.json contains any other value for `main` or `sans` beyond the wildcard. - # - # NOTE: HOSTNAME is set via `helper-functions.sh`, it is not the original system HOSTNAME ENV anymore. - # TODO: SSL_DOMAIN is Traefik specific, it no longer seems relevant and should be considered for removal. - - _traefik_support - - # letsencrypt folders and files mounted in /etc/letsencrypt - local LETSENCRYPT_DOMAIN - local LETSENCRYPT_KEY - - # Identify a valid letsencrypt FQDN folder to use. - if [[ -n ${SSL_DOMAIN} ]] && [[ -e /etc/letsencrypt/live/$(_strip_wildcard_prefix "${SSL_DOMAIN}")/fullchain.pem ]] - then - LETSENCRYPT_DOMAIN=$(_strip_wildcard_prefix "${SSL_DOMAIN}") - elif [[ -e /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem ]] - then - LETSENCRYPT_DOMAIN=${HOSTNAME} - elif [[ -e /etc/letsencrypt/live/${DOMAINNAME}/fullchain.pem ]] - then - LETSENCRYPT_DOMAIN=${DOMAINNAME} - else - _notify 'err' "Cannot find a valid DOMAIN for '/etc/letsencrypt/live//', tried: '${SSL_DOMAIN}', '${HOSTNAME}', '${DOMAINNAME}'" - dms_panic__misconfigured 'LETSENCRYPT_DOMAIN' "${SCOPE_SSL_TYPE}" - return 1 - fi - - # Verify the FQDN folder also includes a valid private key (`privkey.pem` for Certbot, `key.pem` for extraction by Traefik) - if [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/privkey.pem ]] - then - LETSENCRYPT_KEY='privkey' - elif [[ -e /etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/key.pem ]] - then - LETSENCRYPT_KEY='key' - else - _notify 'err' "Cannot find key file ('privkey.pem' or 'key.pem') in '/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/'" - dms_panic__misconfigured 'LETSENCRYPT_KEY' "${SCOPE_SSL_TYPE}" - return 1 - fi - - # Update relevant config for Postfix and Dovecot - _notify 'inf' "Adding ${LETSENCRYPT_DOMAIN} SSL certificate to the postfix and dovecot configuration" - - # LetsEncrypt `fullchain.pem` and `privkey.pem` contents are detailed here from CertBot: - # https://certbot.eff.org/docs/using.html#where-are-my-certificates - # `key.pem` was added for `simp_le` support (2016): https://github.com/docker-mailserver/docker-mailserver/pull/288 - # `key.pem` is also a filename used by the `_extract_certs_from_acme` method (implemented for Traefik v2 only) - local PRIVATE_KEY="/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/${LETSENCRYPT_KEY}.pem" - local CERT_CHAIN="/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/fullchain.pem" - - _set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}" - - _notify 'inf' "SSL configured with 'letsencrypt' certificates" - ;; - - ( "custom" ) # (hard-coded path) Use a private key with full certificate chain all in a single PEM file. - _notify 'inf' "Adding ${HOSTNAME} SSL certificate" - - # NOTE: Dovecot works fine still as both values are bundled into the keychain - local COMBINED_PEM_NAME="${HOSTNAME}-full.pem" - local TMP_KEY_WITH_FULLCHAIN="${TMP_DMS_TLS_PATH}/${COMBINED_PEM_NAME}" - local KEY_WITH_FULLCHAIN="${DMS_TLS_PATH}/${COMBINED_PEM_NAME}" - - if [[ -f ${TMP_KEY_WITH_FULLCHAIN} ]] - then - cp "${TMP_KEY_WITH_FULLCHAIN}" "${KEY_WITH_FULLCHAIN}" - chmod 600 "${KEY_WITH_FULLCHAIN}" - - _set_certificate "${KEY_WITH_FULLCHAIN}" - - _notify 'inf' "SSL configured with 'CA signed/custom' certificates" - else - dms_panic__no_file "${TMP_KEY_WITH_FULLCHAIN}" "${SCOPE_SSL_TYPE}" - fi - ;; - - ( "manual" ) # (dynamic path via ENV) Use separate private key and cert/chain files (should be PEM encoded) - _notify 'inf' "Configuring certificates using key ${SSL_KEY_PATH} and cert ${SSL_CERT_PATH}" - - # Source files are copied internally to these destinations: - local PRIVATE_KEY="${DMS_TLS_PATH}/key" - local CERT_CHAIN="${DMS_TLS_PATH}/cert" - - # Fail early: - if [[ -z ${SSL_KEY_PATH} ]] && [[ -z ${SSL_CERT_PATH} ]] - then - dms_panic__no_env 'SSL_KEY_PATH or SSL_CERT_PATH' "${SCOPE_SSL_TYPE}" - fi - - if [[ -n ${SSL_ALT_KEY_PATH} ]] \ - && [[ -n ${SSL_ALT_CERT_PATH} ]] \ - && [[ ! -f ${SSL_ALT_KEY_PATH} ]] \ - && [[ ! -f ${SSL_ALT_CERT_PATH} ]] - then - dms_panic__no_file "(ALT) ${SSL_ALT_KEY_PATH} or ${SSL_ALT_CERT_PATH}" "${SCOPE_SSL_TYPE}" - fi - - if [[ -f ${SSL_KEY_PATH} ]] && [[ -f ${SSL_CERT_PATH} ]] - then - cp "${SSL_KEY_PATH}" "${PRIVATE_KEY}" - cp "${SSL_CERT_PATH}" "${CERT_CHAIN}" - chmod 600 "${PRIVATE_KEY}" - chmod 644 "${CERT_CHAIN}" - - _set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}" - - # Support for a fallback certificate, useful for hybrid/dual ECDSA + RSA certs - if [[ -n ${SSL_ALT_KEY_PATH} ]] && [[ -n ${SSL_ALT_CERT_PATH} ]] - then - _notify 'inf' "Configuring fallback certificates using key ${SSL_ALT_KEY_PATH} and cert ${SSL_ALT_CERT_PATH}" - - _set_alt_certificate "${SSL_ALT_KEY_PATH}" "${SSL_ALT_CERT_PATH}" - else - # If the Dovecot settings for alt cert has been enabled (doesn't start with `#`), - # but required ENV var is missing, reset to disabled state: - sed -i -r \ - -e 's|^(ssl_alt_key =).*|#\1 The plaintext authentication is always allowed (and SSL not required) for connections from localhost, as they’re assumed to be secure anyway. - # > This applies to all connections where the local and the remote IP addresses are equal. - # > Also IP ranges specified by login_trusted_networks setting are assumed to be secure. - # - # no => insecure auth allowed, yes (default) => plaintext auth only allowed over a secure connection (insecure connection acceptable for non-plaintext auth) - local DISABLE_PLAINTEXT_AUTH='no' - # no => disabled, yes => optional (secure connections not required), required (default) => mandatory (only secure connections allowed) - local DOVECOT_SSL_ENABLED='no' - sed -i -r "s|^#?(disable_plaintext_auth =).*|\1 ${DISABLE_PLAINTEXT_AUTH}|" /etc/dovecot/conf.d/10-auth.conf - sed -i -r "s|^(ssl =).*|\1 ${DOVECOT_SSL_ENABLED}|" "${DOVECOT_CONFIG_SSL}" - ;; - - ( 'snakeoil' ) # This is a temporary workaround for testing only, using the insecure snakeoil cert. - # mail_privacy.bats and mail_with_ldap.bats both attempt to make a starttls connection with openssl, - # failing if SSL/TLS is not available. - ;; - - ( * ) # Unknown option, panic. - dms_panic__invalid_value 'SSL_TYPE' "${SCOPE_TLS_LEVEL}" - ;; - - esac -} - function _setup_postfix_vhost { _notify 'task' "Setting up Postfix vhost" diff --git a/test/mail_ssl_letsencrypt.bats b/test/mail_ssl_letsencrypt.bats index f674b5d8..25d4b03f 100644 --- a/test/mail_ssl_letsencrypt.bats +++ b/test/mail_ssl_letsencrypt.bats @@ -93,7 +93,6 @@ function teardown() { _should_not_have_fqdn_in_cert 'mail.example.test' } - # When using `acme.json` (Traefik) - a wildcard cert `*.example.test` (SSL_DOMAIN) # should be extracted and be chosen over an existing FQDN `mail.example.test` (HOSTNAME): # _acme_wildcard should verify the FQDN `mail.example.test` is negotiated, not `example.test`. @@ -166,6 +165,7 @@ function teardown() { _should_extract_on_changes 'example.test' "${LOCAL_BASE_PATH}/wildcard/rsa.acme.json" _should_have_service_restart_count '2' + # note: https://github.com/docker-mailserver/docker-mailserver/pull/2404 solves this # TODO: Make this pass. # As the FQDN has changed since startup, the configs need to be updated accordingly. # This requires the `changedetector` service event to invoke the same function for TLS configuration diff --git a/test/mail_ssl_manual.bats b/test/mail_ssl_manual.bats index d7ba52f7..a74ef65e 100644 --- a/test/mail_ssl_manual.bats +++ b/test/mail_ssl_manual.bats @@ -22,20 +22,21 @@ function setup_file() { export SSL_ALT_KEY_PATH='/config/ssl/key.rsa.pem' export SSL_ALT_CERT_PATH='/config/ssl/cert.rsa.pem' - local DOMAIN='example.test' local PRIVATE_CONFIG + export DOMAIN_SSL_MANUAL='example.test' PRIVATE_CONFIG="$(duplicate_config_for_container .)" docker run -d --name mail_manual_ssl \ --volume "${PRIVATE_CONFIG}/:/tmp/docker-mailserver/" \ - --volume "$(pwd)/test/test-files/ssl/${DOMAIN}/with_ca/ecdsa/:/config/ssl/:ro" \ - --env DMS_DEBUG=0 \ + --volume "$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/:/config/ssl/:ro" \ + --env DMS_DEBUG=1 \ --env SSL_TYPE='manual' \ + --env TLS_LEVEL='modern' \ --env SSL_KEY_PATH="${SSL_KEY_PATH}" \ --env SSL_CERT_PATH="${SSL_CERT_PATH}" \ --env SSL_ALT_KEY_PATH="${SSL_ALT_KEY_PATH}" \ --env SSL_ALT_CERT_PATH="${SSL_ALT_CERT_PATH}" \ - --hostname "mail.${DOMAIN}" \ + --hostname "mail.${DOMAIN_SSL_MANUAL}" \ --tty \ "${NAME}" # Image name wait_for_finished_setup_in_container mail_manual_ssl @@ -109,6 +110,18 @@ function teardown_file() { assert_equal "${RESULT}" 'Verification: OK' } +@test "checking ssl: manual cert changes are picked up by check-for-changes" { + printf 'someThingsChangedHere' \ + >>"$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/key.ecdsa.pem" + sleep 10 + + run docker exec mail_manual_ssl /bin/bash -c "supervisorctl tail -3000 changedetector" + assert_output --partial 'Change detected' + assert_output --partial 'Manual certificates have changed' + + sed -i '/someThingsChangedHere/d' "$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/key.ecdsa.pem" +} + @test "last" { skip 'this test is only there to reliably mark the end for the teardown_file' }