chore: Refactor `setup-stack.sh` case `SSL_TYPE=letsencrypt` (#2278)

Mostly cleans up the code and documents it better, although there are some minor fixes for handling `SSL_DOMAIN` ENV and additional logging added for spotting issues related to it in future when troubleshooting.

Commits are scoped with context messages for easing review if necessary. Overview of changes:

Traefik specific:

- Logic extracted out into it's own function.
- Conditional reworked to assist with debugging.
- `SSL_DOMAIN` must not be empty when attempting to extract.
- Added additional notes.

`SSL_TYPE=letsencrypt` case:

- Revised top note block.
- Correct handling for `SSL_DOMAIN`.
- Removed some unnecessary nesting.
- Less repetitive error message for `LETSENCRYPT_DOMAIN`.
- Added use of panics where appropriate (kept `return 1` so failures still exit functionality early).
- Improved inline docs.
This commit is contained in:
Brennan Kinney 2021-11-03 09:12:33 +13:00 committed by GitHub
parent ebb081c80f
commit bdb35dd19a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 74 additions and 44 deletions

View File

@ -938,6 +938,38 @@ function _setup_ssl
"${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" )
@ -986,68 +1018,66 @@ function _setup_ssl
( "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=""
local LETSENCRYPT_DOMAIN
local LETSENCRYPT_KEY
# 2020 feature intended for Traefik v2 support only:
# https://github.com/docker-mailserver/docker-mailserver/pull/1553
# Uses `key.pem` and `fullchain.pem`
if [[ -f /etc/letsencrypt/acme.json ]]
# Identify a valid letsencrypt FQDN folder to use.
if [[ -n ${SSL_DOMAIN} ]] && [[ -e /etc/letsencrypt/live/$(_strip_wildcard_prefix "${SSL_DOMAIN}")/fullchain.pem ]]
then
if ! _extract_certs_from_acme "${SSL_DOMAIN}"
then
if ! _extract_certs_from_acme "${HOSTNAME}"
then
_extract_certs_from_acme "${DOMAINNAME}"
fi
fi
fi
# first determine the letsencrypt domain by checking both the full hostname or just the domainname if a SAN is used in the cert
if [[ -e /etc/letsencrypt/live/${HOSTNAME}/fullchain.pem ]]
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 access '/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem' or '/etc/letsencrypt/live/${DOMAINNAME}/fullchain.pem'"
_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
# then determine the keyfile to use
if [[ -n ${LETSENCRYPT_DOMAIN} ]]
# 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
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 access '/etc/letsencrypt/live/${LETSENCRYPT_DOMAIN}/privkey.pem' nor 'key.pem'"
return 1
fi
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
# finally, make the changes to the postfix and dovecot configurations
if [[ -n ${LETSENCRYPT_KEY} ]]
then
_notify 'inf' "Adding ${LETSENCRYPT_DOMAIN} SSL certificate to the postfix and dovecot configuration"
# 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"
# 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}"
_set_certificate "${PRIVATE_KEY}" "${CERT_CHAIN}"
_notify 'inf' "SSL configured with 'letsencrypt' certificates"
fi
return 0
_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.