From 2f3cbfc144fa9f07a879223a058e9d3d5f74769d Mon Sep 17 00:00:00 2001 From: Andrew Cornford Date: Sat, 7 May 2022 23:28:32 +0100 Subject: [PATCH] feat: Support for Dovecot master accounts (#2535) Dovecot master accounts can now be configured in DMS via `setup.sh`. A master account is useful for administration purposes, or to perform mailbox backups of every user account over IMAP. Upstream Docs: https://doc.dovecot.org/configuration_manual/authentication/master_users/ Co-authored-by: Casper Co-authored-by: Brennan Kinney <5098581+polarathene@users.noreply.github.com> --- .gitignore | 1 + Dockerfile | 2 +- Makefile | 4 + .../advanced/dovecot-master-accounts.md | 21 ++++++ docs/mkdocs.yml | 1 + target/bin/adddovecotmasteruser | 59 +++++++++++++++ target/bin/deldovecotmasteruser | 75 +++++++++++++++++++ target/bin/listdovecotmasteruser | 21 ++++++ target/bin/setup | 18 ++++- target/bin/updatedovecotmasteruser | 33 ++++++++ target/dovecot/10-auth.conf | 2 + target/dovecot/auth-master.inc | 9 +++ target/scripts/helpers/accounts.sh | 51 ++++++++++++- target/scripts/helpers/ssl.sh | 1 + test/mail_ssl_letsencrypt.bats | 2 +- test/mail_with_imap.bats | 6 ++ 16 files changed, 302 insertions(+), 4 deletions(-) create mode 100755 docs/content/config/advanced/dovecot-master-accounts.md create mode 100755 target/bin/adddovecotmasteruser create mode 100755 target/bin/deldovecotmasteruser create mode 100755 target/bin/listdovecotmasteruser create mode 100755 target/bin/updatedovecotmasteruser create mode 100755 target/dovecot/auth-master.inc diff --git a/.gitignore b/.gitignore index 1999e03d..dadb085d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ tools/ test/config/empty/ test/config/postfix-accounts.cf +test/config/dovecot-masters.cf test/config/letsencrypt/mail.my-domain.com/combined.pem test/config/dovecot-lmtp/userdb test/config/key* diff --git a/Dockerfile b/Dockerfile index ad1b048f..0dc63b2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,7 +107,7 @@ RUN \ # --- Dovecot ----------------------------------- # ----------------------------------------------- -COPY target/dovecot/auth-passwdfile.inc target/dovecot/??-*.conf /etc/dovecot/conf.d/ +COPY target/dovecot/auth-passwdfile.inc target/dovecot/auth-master.inc target/dovecot/??-*.conf /etc/dovecot/conf.d/ COPY target/dovecot/sieve/ /etc/dovecot/sieve/ COPY target/dovecot/dovecot-purge.cron /etc/cron.d/dovecot-purge.disabled RUN chmod 0 /etc/cron.d/dovecot-purge.disabled diff --git a/Makefile b/Makefile index 6e436fcf..ee387313 100644 --- a/Makefile +++ b/Makefile @@ -29,12 +29,16 @@ clean: # ----------------------------------------------- generate-accounts: +# Normal mail accounts @ docker run --rm -e MAIL_USER=user1@localhost.localdomain -e MAIL_PASS=mypassword -t $(NAME) /bin/sh -c 'echo "$$MAIL_USER|$$(doveadm pw -s SHA512-CRYPT -u $$MAIL_USER -p $$MAIL_PASS)"' > test/config/postfix-accounts.cf @ docker run --rm -e MAIL_USER=user2@otherdomain.tld -e MAIL_PASS=mypassword -t $(NAME) /bin/sh -c 'echo "$$MAIL_USER|$$(doveadm pw -s SHA512-CRYPT -u $$MAIL_USER -p $$MAIL_PASS)"' >> test/config/postfix-accounts.cf @ docker run --rm -e MAIL_USER=user3@localhost.localdomain -e MAIL_PASS=mypassword -t $(NAME) /bin/sh -c 'echo "$$MAIL_USER|$$(doveadm pw -s SHA512-CRYPT -u $$MAIL_USER -p $$MAIL_PASS)|userdb_mail=mbox:~/mail:INBOX=~/inbox"' >> test/config/postfix-accounts.cf @ echo "# this is a test comment, please don't delete me :'(" >> test/config/postfix-accounts.cf @ echo " # this is also a test comment, :O" >> test/config/postfix-accounts.cf +# Dovecot master accounts + @ docker run --rm -e MASTER_USER=masterusername -e MASTER_PASS=masterpassword -t $(NAME) /bin/sh -c 'echo "$$MASTER_USER|$$(doveadm pw -s SHA512-CRYPT -u $$MASTER_USER -p $$MASTER_PASS)"' > test/config/dovecot-masters.cf + tests: @ NAME=$(NAME) ./test/bats/bin/bats --timing test/*.bats diff --git a/docs/content/config/advanced/dovecot-master-accounts.md b/docs/content/config/advanced/dovecot-master-accounts.md new file mode 100755 index 00000000..f5788cc4 --- /dev/null +++ b/docs/content/config/advanced/dovecot-master-accounts.md @@ -0,0 +1,21 @@ +--- +title: 'Advanced | Dovecot master accounts' +--- + +## Introduction + +A dovecot master account is able to login as any configured user. This is useful for administrative tasks like hot backups. + +## Configuration + +It is possible to create, update, delete and list dovecot master accounts using `setup.sh`. See `setup.sh help` for usage. + +This feature is presently [not supported with LDAP](https://github.com/docker-mailserver/docker-mailserver/pull/2535). + +## Logging in + +Once a master account is configured, it is possible to connect to any users mailbox using this account. Log in over POP3/IMAP using the following credential scheme: + +Username: `*` + +Password: `` \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 503e9a9e..bfd355d5 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -148,6 +148,7 @@ nav: - 'Kubernetes': config/advanced/kubernetes.md - 'IPv6': config/advanced/ipv6.md - 'Podman': config/advanced/podman.md + - 'Dovecot Master Accounts': config/advanced/dovecot-master-accounts.md - 'Examples': - 'Tutorials': - 'Basic Installation': examples/tutorials/basic-installation.md diff --git a/target/bin/adddovecotmasteruser b/target/bin/adddovecotmasteruser new file mode 100755 index 00000000..b4a3868f --- /dev/null +++ b/target/bin/adddovecotmasteruser @@ -0,0 +1,59 @@ +#! /bin/bash + +# shellcheck disable=SC2094 + +# shellcheck source=../scripts/helpers/index.sh +source /usr/local/bin/helpers/index.sh + +DATABASE=/tmp/docker-mailserver/dovecot-masters.cf + +function __usage +{ + printf "\e[35mADDDOVECOTMASTERUSER\e[31m(\e[93m8\e[31m) + +\e[38;5;214mNAME\e[39m + addmasteruser - add a dovecot master user (for POP3/IMAP administration) + +\e[38;5;214mSYNOPSIS\e[39m + ./setup.sh dovecot-master add [] + +\e[38;5;214mOPTIONS\e[39m + \e[94mGeneric Program Information\e[39m + help Print the usage information. + +\e[38;5;214mEXAMPLES\e[39m + \e[37m./setup.sh dovecot-master add test-user\e[39m + Add the dovecot master account 'test-user'. You will be prompted + to input a password afterwards since no password was supplied. + +\e[38;5;214mEXIT STATUS\e[39m + Exit status is 0 if command was successful. If wrong arguments are provided + or arguments contain errors, the script will exit early with exit status 1. + +" +} + +[[ ${1:-} == 'help' ]] && { __usage ; exit 0 ; } + +USERNAME="${1}" +shift +PASSWD="${*}" + +[[ -z ${USERNAME} ]] && { __usage ; _exit_with_error 'No username specified' ; } + +touch "${DATABASE}" +_create_lock # Protect config file with lock to avoid race conditions +if grep -qi "^$(_escape "${USERNAME}")|" "${DATABASE}" +then + _exit_with_error "User '${USERNAME}' already exists" +fi + +if [[ -z ${PASSWD} ]] +then + read -r -s -p "Enter Password: " PASSWD + echo + [[ -z ${PASSWD} ]] && _exit_with_error "Password must not be empty" +fi + +HASH="$(doveadm pw -s SHA512-CRYPT -u "${USERNAME}" -p "${PASSWD}")" +echo "${USERNAME}|${HASH}" >> "${DATABASE}" diff --git a/target/bin/deldovecotmasteruser b/target/bin/deldovecotmasteruser new file mode 100755 index 00000000..ac905304 --- /dev/null +++ b/target/bin/deldovecotmasteruser @@ -0,0 +1,75 @@ +#! /bin/bash + +# shellcheck disable=SC2094 +# ? This is done to ignore the message "Make sure not to read and write +# ? the same file in the same pipeline", which is a result of ${DATABASE} +# ? being used below. (This disables the message file-wide.) + +# shellcheck source=../scripts/helpers/index.sh +source /usr/local/bin/helpers/index.sh + +DATABASE=/tmp/docker-mailserver/dovecot-masters.cf + +function __usage +{ + echo -e "\e[35mDELDOVECOTMASTERUSER\e[31m(\e[93m8\e[31m) + +\e[38;5;214mNAME\e[39m + deldovecotmasteruser - delete a dovecot master user + +\e[38;5;214mSYNOPSIS\e[39m + ./setup.sh dovecot-master del { [\e[31m...\e[39m] \e[31m|\e[39m help } + +\e[38;5;214mDESCRIPTION\e[39m + Delete a dovecot master user. + +\e[38;5;214mOPTIONS\e[39m + -h + Show this help dialogue. + +\e[38;5;214mEXAMPLES\e[39m + \e[37m./setup.sh dovecot-master del administrator\e[39m + Delete the dovecot master user called 'administrator'. + + \e[37m./setup.sh dovecot-master del administrator admin\e[39m + Delete dovecot master users 'administrator' and 'admin'. + +\e[38;5;214mEXIT STATUS\e[39m + Exit status is 0 if command was successful, and 1 if there was an error. +" +} + +if [[ ${1} == 'help' ]] +then + __usage + exit 0 +fi + +shift $((OPTIND-1)) + +[[ -z ${*} ]] && { __usage ; _exit_with_error 'No user specified' ; } +[[ -s ${DATABASE} ]] || exit 0 + +_create_lock # Protect config file with lock to avoid race conditions + +for USER in "${@}" +do + ERROR=false + + # ${USER} must not contain /s and other syntactic characters + UNESCAPED_USER="${USER}" + USER=$(_escape "${USER}") + + if [[ -f ${DATABASE} ]] + then + if ! sedfile --strict -i "/^${USER}|/d" "${DATABASE}" + then + _log 'error' "'${UNESCAPED_USER}' couldn't be deleted in '${DATABASE}'" + ERROR=true + fi + fi + + ${ERROR} && _exit_with_error 'See the messages above.' +done + +exit 0 diff --git a/target/bin/listdovecotmasteruser b/target/bin/listdovecotmasteruser new file mode 100755 index 00000000..6dd33ae9 --- /dev/null +++ b/target/bin/listdovecotmasteruser @@ -0,0 +1,21 @@ +#! /bin/bash + +# shellcheck source=../scripts/helpers/index.sh +source /usr/local/bin/helpers/index.sh + +# suppress error output, e.g. when listmailuser runs in a fresh container (DMS not running) +# shellcheck source=/dev/null +source /etc/dms-settings 2>/dev/null + +DATABASE='/tmp/docker-mailserver/dovecot-masters.cf' + +[[ -f ${DATABASE} ]] || _exit_with_error "No 'dovecot-masters.cf' file" +[[ -s ${DATABASE} ]] || _exit_with_error "Empty 'dovecot-masters.cf' - no dovecot master accounts have been added" + +while read -r LINE +do + USER=$(echo "${LINE}" | cut -d'|' -f1) + echo "* ${USER}" +done < "${DATABASE}" + +exit 0 diff --git a/target/bin/setup b/target/bin/setup index 44d378cc..16177d94 100755 --- a/target/bin/setup +++ b/target/bin/setup @@ -25,7 +25,7 @@ ${ORANGE}NAME${RESET} ${ORANGE}SYNOPSIS${RESET} ./${SCRIPT:-${0}} [ OPTIONS${RED}...${RESET} ] COMMAND [ help ${RED}|${RESET} ARGUMENTS${RED}...${RESET} ] - COMMAND ${RED}:=${RESET} { email ${RED}|${RESET} alias ${RED}|${RESET} quota ${RED}|${RESET} config ${RED}|${RESET} relay ${RED}|${RESET} debug } SUBCOMMAND + COMMAND ${RED}:=${RESET} { email ${RED}|${RESET} alias ${RED}|${RESET} quota ${RED}|${RESET} dovecot-master ${RED}|${RESET} config ${RED}|${RESET} relay ${RED}|${RESET} debug } SUBCOMMAND ${ORANGE}DESCRIPTION${RESET} This is the main administration script that you use for all your interactions with @@ -59,6 +59,12 @@ ${RED}[${ORANGE}SUB${RED}]${ORANGE}COMMANDS${RESET} ${0} quota ${CYAN}set${RESET} [] ${0} quota ${CYAN}del${RESET} + ${LBLUE}COMMAND${RESET} dovecot-master ${RED}:=${RESET} + ${0} dovecot-master ${CYAN}add${RESET} [] + ${0} dovecot-master ${CYAN}update${RESET} [] + ${0} dovecot-master ${CYAN}del${RESET} [ OPTIONS${RED}...${RESET} ] [ ${RED}...${RESET} ] + ${0} dovecot-master ${CYAN}list${RESET} + ${LBLUE}COMMAND${RESET} config ${RED}:=${RESET} ${0} config ${CYAN}dkim${RESET} [ ARGUMENTS${RED}...${RESET} ] @@ -132,6 +138,16 @@ function _main esac ;; + ( dovecot-master ) + case ${2:-} in + ( add ) shift 2 ; adddovecotmasteruser "${@}" ;; + ( update ) shift 2 ; updatedovecotmasteruser "${@}" ;; + ( del ) shift 2 ; deldovecotmasteruser "${@}" ;; + ( list ) listdovecotmasteruser ;; + ( * ) _invalid_command "${1}" "${2}" ;; + esac + ;; + ( config ) case ${2:-} in ( dkim ) shift 2 ; open-dkim "${@}" ;; diff --git a/target/bin/updatedovecotmasteruser b/target/bin/updatedovecotmasteruser new file mode 100755 index 00000000..d228ec2d --- /dev/null +++ b/target/bin/updatedovecotmasteruser @@ -0,0 +1,33 @@ +#! /bin/bash + +# ? This is done to ignore the message "Make sure not to read and write +# ? the same file in the same pipeline", which is a result of ${DATABASE} +# ? being used below. (This disables the message file-wide.) +# shellcheck disable=SC2094 + +# shellcheck source=../scripts/helpers/index.sh +source /usr/local/bin/helpers/index.sh + +DATABASE=/tmp/docker-mailserver/dovecot-masters.cf + +USER="${1}" +shift +PASSWD="${*}" + +function __usage { echo 'Usage: updatedovecotmasteruser [PASSWORD]' ; } + +[[ -z ${USER} ]] && { __usage ; _exit_with_error 'No username specified' ; } + +if [[ -z ${PASSWD} ]] +then + read -r -s -p 'Enter Password: ' PASSWD + echo + [[ -z ${PASSWD} ]] && _exit_with_error 'Password must not be empty' +fi + +HASH="$(doveadm pw -s SHA512-CRYPT -u "${USER}" -p "${PASSWD}")" + +touch "${DATABASE}" +_create_lock # Protect config file with lock to avoid race conditions +grep -qi "^$(_escape "${USER}")|" "${DATABASE}" 2>/dev/null || _exit_with_error "Master user \"${USER}\" does not exist" +sed -i "s ^""${USER}""|.* ""${USER}""|""${HASH}"" " "${DATABASE}" diff --git a/target/dovecot/10-auth.conf b/target/dovecot/10-auth.conf index 942b0857..f71289e9 100644 --- a/target/dovecot/10-auth.conf +++ b/target/dovecot/10-auth.conf @@ -126,3 +126,5 @@ auth_mechanisms = plain login #!include auth-checkpassword.conf.ext #!include auth-vpopmail.conf.ext #!include auth-static.conf.ext + +#!include auth-master.inc diff --git a/target/dovecot/auth-master.inc b/target/dovecot/auth-master.inc new file mode 100755 index 00000000..b71967cb --- /dev/null +++ b/target/dovecot/auth-master.inc @@ -0,0 +1,9 @@ +auth_master_user_separator = * +passdb { + driver = passwd-file + args = scheme=SHA512-CRYPT username_format=%u /etc/dovecot/masterdb + master = yes + result_success = continue + #auth_bind = yes +} + diff --git a/target/scripts/helpers/accounts.sh b/target/scripts/helpers/accounts.sh index 56cf5a3e..e0a0b229 100644 --- a/target/scripts/helpers/accounts.sh +++ b/target/scripts/helpers/accounts.sh @@ -7,13 +7,17 @@ # Only an issue when $myhostname is an exact match (eg: bare domain FQDN). DOVECOT_USERDB_FILE=/etc/dovecot/userdb +DOVECOT_MASTERDB_FILE=/etc/dovecot/masterdb function _create_accounts { : >/etc/postfix/vmailbox : >"${DOVECOT_USERDB_FILE}" - if [[ -f /tmp/docker-mailserver/postfix-accounts.cf ]] && [[ ${ENABLE_LDAP} -ne 1 ]] + [[ ${ENABLE_LDAP} -eq 1 ]] && return 0 + + _create_masters + if [[ -f /tmp/docker-mailserver/postfix-accounts.cf ]] then _log 'trace' "Checking file line endings" sed -i 's|\r||g' /tmp/docker-mailserver/postfix-accounts.cf @@ -164,3 +168,48 @@ function _create_dovecot_alias_dummy_accounts done < /tmp/docker-mailserver/postfix-virtual.cf fi } + +# Support Dovecot master user: https://doc.dovecot.org/configuration_manual/authentication/master_users/ +# Supporting LDAP users requires `auth_bind = yes` in `dovecot-ldap.conf.ext`, see docker-mailserver/docker-mailserver/pull/2535 for details +function _create_masters +{ + : >"${DOVECOT_MASTERDB_FILE}" + + if [[ -f /tmp/docker-mailserver/dovecot-masters.cf ]] + then + _log 'trace' "Checking file line endings" + sed -i 's|\r||g' /tmp/docker-mailserver/dovecot-masters.cf + + _log 'trace' "Regenerating dovecot masters list" + + # checking that /tmp/docker-mailserver/dovecot-masters.cf ends with a newline + # shellcheck disable=SC1003 + sed -i -e '$a\' /tmp/docker-mailserver/dovecot-masters.cf + + chown dovecot:dovecot "${DOVECOT_MASTERDB_FILE}" + chmod 640 "${DOVECOT_MASTERDB_FILE}" + + sed -i -e '/\!include auth-master\.inc/s/^#//' /etc/dovecot/conf.d/10-auth.conf + + # creating users ; 'pass' is encrypted + # comments and empty lines are ignored + local LOGIN PASS + while IFS=$'|' read -r LOGIN PASS + do + _log 'debug' "Creating master user '${LOGIN}'" + + local DOVECOT_MASTERDB_LINE + + # Dovecot's masterdb has the following format + # user:password + DOVECOT_MASTERDB_LINE="${LOGIN}:${PASS}" + if grep -qF "${DOVECOT_MASTERDB_LINE}" "${DOVECOT_MASTERDB_FILE}" + then + _log 'warn' "Login '${LOGIN}' will not be added to '${DOVECOT_MASTERDB_FILE}' twice" + else + echo "${DOVECOT_MASTERDB_LINE}" >>"${DOVECOT_MASTERDB_FILE}" + fi + + done < <(grep -v "^\s*$\|^\s*\#" /tmp/docker-mailserver/dovecot-masters.cf) + fi +} diff --git a/target/scripts/helpers/ssl.sh b/target/scripts/helpers/ssl.sh index 28a907b8..95f10d25 100644 --- a/target/scripts/helpers/ssl.sh +++ b/target/scripts/helpers/ssl.sh @@ -483,6 +483,7 @@ function _monitored_files_checksums "${DMS_DIR}/postfix-virtual.cf" "${DMS_DIR}/postfix-aliases.cf" "${DMS_DIR}/dovecot-quotas.cf" + "${DMS_DIR}/dovecot-masters.cf" ) if [[ ${SSL_TYPE:-} == 'manual' ]] diff --git a/test/mail_ssl_letsencrypt.bats b/test/mail_ssl_letsencrypt.bats index 594fad96..0f509192 100644 --- a/test/mail_ssl_letsencrypt.bats +++ b/test/mail_ssl_letsencrypt.bats @@ -298,7 +298,7 @@ function _should_be_equal_in_content() { function _get_service_logs() { local SERVICE=${1:-'mailserver'} - local CMD_LOGS=(docker exec "${TEST_NAME}" "supervisorctl tail -2000 ${SERVICE}") + local CMD_LOGS=(docker exec "${TEST_NAME}" "supervisorctl tail -2200 ${SERVICE}") # As the `mailserver` service logs are not stored in a file but output to stdout/stderr, # The `supervisorctl tail` command won't work; we must instead query via `docker logs`: diff --git a/test/mail_with_imap.bats b/test/mail_with_imap.bats index 74ae5cd6..601efc14 100644 --- a/test/mail_with_imap.bats +++ b/test/mail_with_imap.bats @@ -40,3 +40,9 @@ teardown_file() { run docker exec mail_with_imap /bin/sh -c "nc -w 5 0.0.0.0 25 < /tmp/docker-mailserver-test/auth/smtp-auth-login.txt | grep 'Authentication successful'" assert_success } + +# master account +@test "checking dovecot: master account can login" { + run docker exec mail_with_imap bash -c "testsaslauthd -u user1@localhost.localdomain*masterusername -p masterpassword" + assert_success +}