mirror of
https://github.com/docker-mailserver/docker-mailserver.git
synced 2024-01-19 02:48:50 +00:00
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.
This commit is contained in:
parent
54f2181379
commit
ec8b99335e
|
@ -96,7 +96,7 @@ Set this to `yes` to enable authentication binds ([more details in the dovecot d
|
||||||
|
|
||||||
### `SASLAUTHD_LDAP_FILTER`
|
### `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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -415,7 +415,7 @@ Note: activate this only if you are confident in your bayes database for identif
|
||||||
##### FETCHMAIL_PARALLEL
|
##### FETCHMAIL_PARALLEL
|
||||||
|
|
||||||
**0** => `fetchmail` runs with a single config file `/etc/fetchmailrc`
|
**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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ _obtain_hostname_and_domainname
|
||||||
PM_ADDRESS="${POSTMASTER_ADDRESS:=postmaster@${DOMAINNAME}}"
|
PM_ADDRESS="${POSTMASTER_ADDRESS:=postmaster@${DOMAINNAME}}"
|
||||||
_notify 'inf' "${LOG_DATE} Using postmaster address ${PM_ADDRESS}"
|
_notify 'inf' "${LOG_DATE} Using postmaster address ${PM_ADDRESS}"
|
||||||
|
|
||||||
|
REGEX_NEVER_MATCH="(?\!)"
|
||||||
|
|
||||||
# Change detection delayed during startup to avoid conflicting writes
|
# Change detection delayed during startup to avoid conflicting writes
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
|
@ -65,10 +67,24 @@ do
|
||||||
# Also note that changes are performed in place and are not atomic
|
# 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
|
# 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.
|
# `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
|
# 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.
|
# extracted for `docker-mailserver` services to adjust to.
|
||||||
if [[ ${CHANGED} =~ /etc/letsencrypt/acme.json ]]
|
elif [[ ${CHANGED} =~ /etc/letsencrypt/acme.json ]]
|
||||||
then
|
then
|
||||||
_notify 'inf' "'/etc/letsencrypt/acme.json' has changed, extracting certs.."
|
_notify 'inf' "'/etc/letsencrypt/acme.json' has changed, extracting certs.."
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
#! /bin/bash
|
#! /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`,
|
# These helpers are used by `setup-stack.sh` and `check-for-changes.sh`,
|
||||||
# not by anything within `helper-functions.sh` itself:
|
# not by anything within `helper-functions.sh` itself:
|
||||||
# shellcheck source=target/scripts/helpers/index.sh
|
# 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)
|
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
|
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
|
# ? --------------------------------------------- BIN HELPER
|
||||||
|
|
||||||
function errex
|
function errex
|
||||||
|
@ -151,47 +159,6 @@ function _sanitize_ipv4_to_subnet_cidr
|
||||||
}
|
}
|
||||||
export -f _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
|
# ? --------------------------------------------- Notifications
|
||||||
|
|
||||||
function _notify
|
function _notify
|
||||||
|
@ -218,43 +185,6 @@ function _notify
|
||||||
}
|
}
|
||||||
export -f _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
|
# ? --------------------------------------------- General
|
||||||
|
|
||||||
# Outputs the DNS label count (delimited by `.`) for the given input string.
|
# Outputs the DNS label count (delimited by `.`) for the given input string.
|
||||||
|
|
|
@ -11,5 +11,6 @@ function _import_scripts
|
||||||
. "${PATH_TO_SCRIPTS}/aliases.sh"
|
. "${PATH_TO_SCRIPTS}/aliases.sh"
|
||||||
. "${PATH_TO_SCRIPTS}/relay.sh"
|
. "${PATH_TO_SCRIPTS}/relay.sh"
|
||||||
. "${PATH_TO_SCRIPTS}/sasl.sh"
|
. "${PATH_TO_SCRIPTS}/sasl.sh"
|
||||||
|
. "${PATH_TO_SCRIPTS}/ssl.sh"
|
||||||
}
|
}
|
||||||
_import_scripts
|
_import_scripts
|
||||||
|
|
499
target/scripts/helpers/ssl.sh
Normal file
499
target/scripts/helpers/ssl.sh
Normal file
|
@ -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/<DOMAIN>/', 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 </path/to/alternative/key.pem|' \
|
||||||
|
-e 's|^(ssl_alt_cert =).*|#\1 </path/to/alternative/cert.pem|' \
|
||||||
|
"${DOVECOT_CONFIG_SSL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_notify 'inf' "SSL configured with 'Manual' certificates"
|
||||||
|
else
|
||||||
|
dms_panic__no_file "${SSL_KEY_PATH} or ${SSL_CERT_PATH}" "${SCOPE_SSL_TYPE}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
( "self-signed" ) # (hard-coded path) Use separate private key and cert/chain files (should be PEM encoded), expects self-signed CA
|
||||||
|
_notify 'inf' "Adding ${HOSTNAME} SSL certificate"
|
||||||
|
|
||||||
|
local KEY_NAME="${HOSTNAME}-key.pem"
|
||||||
|
local CERT_NAME="${HOSTNAME}-cert.pem"
|
||||||
|
|
||||||
|
# Self-Signed source files:
|
||||||
|
local SS_KEY="${TMP_DMS_TLS_PATH}/${KEY_NAME}"
|
||||||
|
local SS_CERT="${TMP_DMS_TLS_PATH}/${CERT_NAME}"
|
||||||
|
local SS_CA_CERT="${TMP_DMS_TLS_PATH}/demoCA/cacert.pem"
|
||||||
|
|
||||||
|
# Source files are copied internally to these destinations:
|
||||||
|
local PRIVATE_KEY="${DMS_TLS_PATH}/${KEY_NAME}"
|
||||||
|
local CERT_CHAIN="${DMS_TLS_PATH}/${CERT_NAME}"
|
||||||
|
local CA_CERT="${DMS_TLS_PATH}/cacert.pem"
|
||||||
|
|
||||||
|
if [[ -f ${SS_KEY} ]] \
|
||||||
|
&& [[ -f ${SS_CERT} ]] \
|
||||||
|
&& [[ -f ${SS_CA_CERT} ]]
|
||||||
|
then
|
||||||
|
cp "${SS_KEY}" "${PRIVATE_KEY}"
|
||||||
|
cp "${SS_CERT}" "${CERT_CHAIN}"
|
||||||
|
chmod 600 "${PRIVATE_KEY}"
|
||||||
|
chmod 644 "${CERT_CHAIN}"
|
||||||
|
|
||||||
|
_set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}"
|
||||||
|
|
||||||
|
cp "${SS_CA_CERT}" "${CA_CERT}"
|
||||||
|
chmod 644 "${CA_CERT}"
|
||||||
|
|
||||||
|
# Have Postfix trust the self-signed CA (which is not installed within the OS trust store)
|
||||||
|
sedfile -i -r "s|^#?(smtpd?_tls_CAfile =).*|\1 ${CA_CERT}|" "${POSTFIX_CONFIG_MAIN}"
|
||||||
|
# Part of the original `self-signed` support, unclear why this symlink was required?
|
||||||
|
# May have been to support the now removed `Courier` (Dovecot replaced it):
|
||||||
|
# https://github.com/docker-mailserver/docker-mailserver/commit/1fb3aeede8ac9707cc9ea11d603e3a7b33b5f8d5
|
||||||
|
# smtp_tls_CApath and smtpd_tls_CApath both point to /etc/ssl/certs
|
||||||
|
local PRIVATE_CA="/etc/ssl/certs/cacert-${HOSTNAME}.pem"
|
||||||
|
ln -s "${CA_CERT}" "${PRIVATE_CA}"
|
||||||
|
|
||||||
|
_notify 'inf' "SSL configured with 'self-signed' certificates"
|
||||||
|
else
|
||||||
|
dms_panic__no_file "${SS_KEY} or ${SS_CERT}" "${SCOPE_SSL_TYPE}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
( '' ) # No SSL/TLS certificate used/required, plaintext auth permitted over insecure connections
|
||||||
|
_notify 'warn' "(INSECURE!) SSL configured with plain text access. DO NOT USE FOR PRODUCTION DEPLOYMENT."
|
||||||
|
# Untested. Not officially supported.
|
||||||
|
|
||||||
|
# Postfix configuration:
|
||||||
|
# smtp_tls_security_level (default: 'may', amavis 'none' x2) | http://www.postfix.org/postconf.5.html#smtp_tls_security_level
|
||||||
|
# '_setup_postfix_relay_hosts' also adds 'smtp_tls_security_level = encrypt'
|
||||||
|
# smtpd_tls_security_level (default: 'may', port 587 'encrypt') | http://www.postfix.org/postconf.5.html#smtpd_tls_security_level
|
||||||
|
#
|
||||||
|
# smtpd_tls_auth_only (default not applied, 'no', implicitly 'yes' if security_level is 'encrypt')
|
||||||
|
# | http://www.postfix.org/postconf.5.html#smtpd_tls_auth_only | http://www.postfix.org/TLS_README.html#server_tls_auth
|
||||||
|
#
|
||||||
|
# smtp_tls_wrappermode (default: not applied, 'no') | http://www.postfix.org/postconf.5.html#smtp_tls_wrappermode
|
||||||
|
# smtpd_tls_wrappermode (default: 'yes' for service port 'smtps') | http://www.postfix.org/postconf.5.html#smtpd_tls_wrappermode
|
||||||
|
# NOTE: Enabling wrappermode requires a security_level of 'encrypt' or stronger. Port 465 presently does not meet this condition.
|
||||||
|
#
|
||||||
|
# Postfix main.cf (base config):
|
||||||
|
sedfile -i -r \
|
||||||
|
-e "s|^#?(smtpd?_tls_security_level).*|\1 = none|" \
|
||||||
|
-e "s|^#?(smtpd_tls_auth_only).*|\1 = no|" \
|
||||||
|
"${POSTFIX_CONFIG_MAIN}"
|
||||||
|
#
|
||||||
|
# Postfix master.cf (per connection overrides):
|
||||||
|
# Disables implicit TLS on port 465 for inbound (smtpd) and outbound (smtp) traffic. Treats it as equivalent to port 25 SMTP with explicit STARTTLS.
|
||||||
|
# Inbound 465 (aka service port aliases: submissions / smtps) for Postfix to receive over implicit TLS (eg from MUA or functioning as a relay host).
|
||||||
|
# Outbound 465 as alternative to port 587 when sending to another MTA (with authentication), such as a relay service (eg SendGrid).
|
||||||
|
sedfile -i -r \
|
||||||
|
-e "/smtpd?_tls_security_level/s|=.*|=none|" \
|
||||||
|
-e '/smtpd?_tls_wrappermode/s|yes|no|' \
|
||||||
|
-e '/smtpd_tls_auth_only/s|yes|no|' \
|
||||||
|
"${POSTFIX_CONFIG_MASTER}"
|
||||||
|
|
||||||
|
# Dovecot configuration:
|
||||||
|
# https://doc.dovecot.org/configuration_manual/dovecot_ssl_configuration/
|
||||||
|
# > 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
|
|
@ -691,414 +691,6 @@ function _setup_dkim
|
||||||
fi
|
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/<DOMAIN>/', 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 </path/to/alternative/key.pem|' \
|
|
||||||
-e 's|^(ssl_alt_cert =).*|#\1 </path/to/alternative/cert.pem|' \
|
|
||||||
"${DOVECOT_CONFIG_SSL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_notify 'inf' "SSL configured with 'Manual' certificates"
|
|
||||||
else
|
|
||||||
dms_panic__no_file "${SSL_KEY_PATH} or ${SSL_CERT_PATH}" "${SCOPE_SSL_TYPE}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
( "self-signed" ) # (hard-coded path) Use separate private key and cert/chain files (should be PEM encoded), expects self-signed CA
|
|
||||||
_notify 'inf' "Adding ${HOSTNAME} SSL certificate"
|
|
||||||
|
|
||||||
local KEY_NAME="${HOSTNAME}-key.pem"
|
|
||||||
local CERT_NAME="${HOSTNAME}-cert.pem"
|
|
||||||
|
|
||||||
# Self-Signed source files:
|
|
||||||
local SS_KEY="${TMP_DMS_TLS_PATH}/${KEY_NAME}"
|
|
||||||
local SS_CERT="${TMP_DMS_TLS_PATH}/${CERT_NAME}"
|
|
||||||
local SS_CA_CERT="${TMP_DMS_TLS_PATH}/demoCA/cacert.pem"
|
|
||||||
|
|
||||||
# Source files are copied internally to these destinations:
|
|
||||||
local PRIVATE_KEY="${DMS_TLS_PATH}/${KEY_NAME}"
|
|
||||||
local CERT_CHAIN="${DMS_TLS_PATH}/${CERT_NAME}"
|
|
||||||
local CA_CERT="${DMS_TLS_PATH}/cacert.pem"
|
|
||||||
|
|
||||||
if [[ -f ${SS_KEY} ]] \
|
|
||||||
&& [[ -f ${SS_CERT} ]] \
|
|
||||||
&& [[ -f ${SS_CA_CERT} ]]
|
|
||||||
then
|
|
||||||
cp "${SS_KEY}" "${PRIVATE_KEY}"
|
|
||||||
cp "${SS_CERT}" "${CERT_CHAIN}"
|
|
||||||
chmod 600 "${PRIVATE_KEY}"
|
|
||||||
chmod 644 "${CERT_CHAIN}"
|
|
||||||
|
|
||||||
_set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}"
|
|
||||||
|
|
||||||
cp "${SS_CA_CERT}" "${CA_CERT}"
|
|
||||||
chmod 644 "${CA_CERT}"
|
|
||||||
|
|
||||||
# Have Postfix trust the self-signed CA (which is not installed within the OS trust store)
|
|
||||||
sedfile -i -r "s|^#?(smtpd?_tls_CAfile =).*|\1 ${CA_CERT}|" "${POSTFIX_CONFIG_MAIN}"
|
|
||||||
# Part of the original `self-signed` support, unclear why this symlink was required?
|
|
||||||
# May have been to support the now removed `Courier` (Dovecot replaced it):
|
|
||||||
# https://github.com/docker-mailserver/docker-mailserver/commit/1fb3aeede8ac9707cc9ea11d603e3a7b33b5f8d5
|
|
||||||
# smtp_tls_CApath and smtpd_tls_CApath both point to /etc/ssl/certs
|
|
||||||
local PRIVATE_CA="/etc/ssl/certs/cacert-${HOSTNAME}.pem"
|
|
||||||
ln -s "${CA_CERT}" "${PRIVATE_CA}"
|
|
||||||
|
|
||||||
_notify 'inf' "SSL configured with 'self-signed' certificates"
|
|
||||||
else
|
|
||||||
dms_panic__no_file "${SS_KEY} or ${SS_CERT}" "${SCOPE_SSL_TYPE}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
( '' ) # No SSL/TLS certificate used/required, plaintext auth permitted over insecure connections
|
|
||||||
_notify 'warn' "(INSECURE!) SSL configured with plain text access. DO NOT USE FOR PRODUCTION DEPLOYMENT."
|
|
||||||
# Untested. Not officially supported.
|
|
||||||
|
|
||||||
# Postfix configuration:
|
|
||||||
# smtp_tls_security_level (default: 'may', amavis 'none' x2) | http://www.postfix.org/postconf.5.html#smtp_tls_security_level
|
|
||||||
# '_setup_postfix_relay_hosts' also adds 'smtp_tls_security_level = encrypt'
|
|
||||||
# smtpd_tls_security_level (default: 'may', port 587 'encrypt') | http://www.postfix.org/postconf.5.html#smtpd_tls_security_level
|
|
||||||
#
|
|
||||||
# smtpd_tls_auth_only (default not applied, 'no', implicitly 'yes' if security_level is 'encrypt')
|
|
||||||
# | http://www.postfix.org/postconf.5.html#smtpd_tls_auth_only | http://www.postfix.org/TLS_README.html#server_tls_auth
|
|
||||||
#
|
|
||||||
# smtp_tls_wrappermode (default: not applied, 'no') | http://www.postfix.org/postconf.5.html#smtp_tls_wrappermode
|
|
||||||
# smtpd_tls_wrappermode (default: 'yes' for service port 'smtps') | http://www.postfix.org/postconf.5.html#smtpd_tls_wrappermode
|
|
||||||
# NOTE: Enabling wrappermode requires a security_level of 'encrypt' or stronger. Port 465 presently does not meet this condition.
|
|
||||||
#
|
|
||||||
# Postfix main.cf (base config):
|
|
||||||
sedfile -i -r \
|
|
||||||
-e "s|^#?(smtpd?_tls_security_level).*|\1 = none|" \
|
|
||||||
-e "s|^#?(smtpd_tls_auth_only).*|\1 = no|" \
|
|
||||||
"${POSTFIX_CONFIG_MAIN}"
|
|
||||||
#
|
|
||||||
# Postfix master.cf (per connection overrides):
|
|
||||||
# Disables implicit TLS on port 465 for inbound (smtpd) and outbound (smtp) traffic. Treats it as equivalent to port 25 SMTP with explicit STARTTLS.
|
|
||||||
# Inbound 465 (aka service port aliases: submissions / smtps) for Postfix to receive over implicit TLS (eg from MUA or functioning as a relay host).
|
|
||||||
# Outbound 465 as alternative to port 587 when sending to another MTA (with authentication), such as a relay service (eg SendGrid).
|
|
||||||
sedfile -i -r \
|
|
||||||
-e "/smtpd?_tls_security_level/s|=.*|=none|" \
|
|
||||||
-e '/smtpd?_tls_wrappermode/s|yes|no|' \
|
|
||||||
-e '/smtpd_tls_auth_only/s|yes|no|' \
|
|
||||||
"${POSTFIX_CONFIG_MASTER}"
|
|
||||||
|
|
||||||
# Dovecot configuration:
|
|
||||||
# https://doc.dovecot.org/configuration_manual/dovecot_ssl_configuration/
|
|
||||||
# > 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
|
function _setup_postfix_vhost
|
||||||
{
|
{
|
||||||
_notify 'task' "Setting up Postfix vhost"
|
_notify 'task' "Setting up Postfix vhost"
|
||||||
|
|
|
@ -93,7 +93,6 @@ function teardown() {
|
||||||
_should_not_have_fqdn_in_cert 'mail.example.test'
|
_should_not_have_fqdn_in_cert 'mail.example.test'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# When using `acme.json` (Traefik) - a wildcard cert `*.example.test` (SSL_DOMAIN)
|
# 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):
|
# 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`.
|
# _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_extract_on_changes 'example.test' "${LOCAL_BASE_PATH}/wildcard/rsa.acme.json"
|
||||||
_should_have_service_restart_count '2'
|
_should_have_service_restart_count '2'
|
||||||
|
|
||||||
|
# note: https://github.com/docker-mailserver/docker-mailserver/pull/2404 solves this
|
||||||
# TODO: Make this pass.
|
# TODO: Make this pass.
|
||||||
# As the FQDN has changed since startup, the configs need to be updated accordingly.
|
# 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
|
# This requires the `changedetector` service event to invoke the same function for TLS configuration
|
||||||
|
|
|
@ -22,20 +22,21 @@ 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 DOMAIN='example.test'
|
|
||||||
local PRIVATE_CONFIG
|
local PRIVATE_CONFIG
|
||||||
|
export DOMAIN_SSL_MANUAL='example.test'
|
||||||
PRIVATE_CONFIG="$(duplicate_config_for_container .)"
|
PRIVATE_CONFIG="$(duplicate_config_for_container .)"
|
||||||
|
|
||||||
docker run -d --name mail_manual_ssl \
|
docker run -d --name mail_manual_ssl \
|
||||||
--volume "${PRIVATE_CONFIG}/:/tmp/docker-mailserver/" \
|
--volume "${PRIVATE_CONFIG}/:/tmp/docker-mailserver/" \
|
||||||
--volume "$(pwd)/test/test-files/ssl/${DOMAIN}/with_ca/ecdsa/:/config/ssl/:ro" \
|
--volume "$(pwd)/test/test-files/ssl/${DOMAIN_SSL_MANUAL}/with_ca/ecdsa/:/config/ssl/:ro" \
|
||||||
--env DMS_DEBUG=0 \
|
--env DMS_DEBUG=1 \
|
||||||
--env SSL_TYPE='manual' \
|
--env SSL_TYPE='manual' \
|
||||||
|
--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}" \
|
--hostname "mail.${DOMAIN_SSL_MANUAL}" \
|
||||||
--tty \
|
--tty \
|
||||||
"${NAME}" # Image name
|
"${NAME}" # Image name
|
||||||
wait_for_finished_setup_in_container mail_manual_ssl
|
wait_for_finished_setup_in_container mail_manual_ssl
|
||||||
|
@ -109,6 +110,18 @@ function teardown_file() {
|
||||||
assert_equal "${RESULT}" 'Verification: OK'
|
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" {
|
@test "last" {
|
||||||
skip 'this test is only there to reliably mark the end for the teardown_file'
|
skip 'this test is only there to reliably mark the end for the teardown_file'
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue