From 362f4e3865baa984a7aec442f6bc7dd7dfb1debf Mon Sep 17 00:00:00 2001 From: mlan Date: Sun, 15 Nov 2020 20:27:46 +0100 Subject: [PATCH] - [docker](src/docker/bin/docker-config.sh) Generalized `dc_cond_chown()`. - [kopano](src/kopano) Updated documentation and bug fix of support for secure IMAPS, POP3S and ICALS. --- CHANGELOG.md | 2 ++ README.md | 40 +++++++++++++++++++++++++---- src/docker/bin/docker-config.sh | 30 ++++++++++++++++++---- src/docker/bin/docker-service.sh | 1 + src/kopano/entry.d/10-kopano-common | 9 +++++-- test/Makefile | 16 +++++------- 6 files changed, 77 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e739447..2ceb4f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - [test](test/Makefile) Arrange build tests. - [test](test/Makefile) Add SSL build tests. - [travis-ci](.travis.yml) Travis CI now run test-all. +- [docker](src/docker/bin/docker-config.sh) Generalized `dc_cond_chown()`. +- [kopano](src/kopano) Updated documentation and bug fix of support for secure IMAPS, POP3S and ICALS. # 1.2.2 diff --git a/README.md b/README.md index 802d90f..2174fd6 100644 --- a/README.md +++ b/README.md @@ -319,9 +319,36 @@ To enable secure access we need to explicitly define their listening ports. This If any of `IMAPS_LISTEN`, `POP3S_LISTEN` and `ICALS_LISTEN` are explicitly defined but there are no certificate files defined, a self-signed certificate will be generated when the container is created. +### SSL/LTS certificate and private key + +For most deployments a trusted SSL/TLS certificate is desired. During startup the `mlan/kopano` looks for [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) certificate and private key with these specific names: `/etc/kopano/ssl/cert.pem` and `/etc/kopano/ssl/priv_key.pem`. If found they will used by the secure protocols IMAPS, POP3S and ICALS. Moreover the file ownership will be changed if needed to make them readable by the Kopano services. + #### `SSL_CERTIFICATE_FILE` and `SSL_PRIVATE_KEY_FILE` -For most deployments a trusted TLS certificate is needed. When such are available, copy the [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) certificate and private key files to the container and use the envvars `SSL_CERTIFICATE_FILE` and `SSL_PRIVATE_KEY_FILE` to let the kopano-gateway and kopano-ical services find them. For example `SSL_CERTIFICATE_FILE=/etc/kopano/ssl/cert.pem` and `SSL_PRIVATE_KEY_FILE=/etc/kopano/ssl/priv_key.pem`. Note that these files need to be readable by the `kopano` user. +If you use other file names or directories, you let the Kopano services know by setting the variables `SSL_CERTIFICATE_FILE=/etc/kopano/ssl/cert.pem` and `SSL_PRIVATE_KEY_FILE=/etc/kopano/ssl/priv_key.pem` on the `docker run` command line or in the `docker-compose.yml` file. + +For testing purposes you can create a self-signed certificate using the `openssl` utility, see below. Note that this is not necessary since, when secure protocols are defined, a self-signed certificate and private key will be automatically be created during container startup if they are not found. + +```bash +openssl genrsa -out ssl/priv_key.pem +openssl req -x509 -utf8 -new -batch -subj "/CN=app" -key ssl/priv_key.pem -out ssl/cert.pem +``` + +One way to allow the container to read the certificate and private key is to bind mount the host directory holding the files to the container: + +```bash +docker run -d -name app -v $pwd/ssl:/etc/kopano/ssl mlan/kopano +``` + +A other way is to copy them to the container: + +```bash +docker create -name app mlan/kopano +docker cp ssl/. app:/etc/kopano/ssl +docker start app +``` + +If you copy the files to a running container you need to make sure that the user `kopano` can read them. ### Let’s Encrypt LTS certificates using [Traefik](https://docs.traefik.io/) @@ -329,19 +356,20 @@ For most deployments a trusted TLS certificate is needed. When such are availabl #### `ACME_FILE`, `ACME_POSTHOOK` -The `mlan/kopano` image looks for a file `ACME_FILE=/acme/acme.json` at container startup and every time this file changes certificates within this file are extracted. If the host or domain name of one of those certificates matches `HOSTNAME=$(hostname)` or `DOMAIN=${HOSTNAME#*.}` it will be used for TLS support. +The `mlan/kopano` image looks for a file `ACME_FILE=/acme/acme.json` at container startup and every time this file changes certificates within this file are extracted. If the host or domain name of one of those certificates matches `HOSTNAME=$(hostname)` or `DOMAIN=${HOSTNAME#*.}` it will be used by the secure protocols. Once the certificates and keys have been updated, we run the command in the environment variable `ACME_POSTHOOK="sv restart kopano-gateway kopano-ical"`. Kopano services needs to be restarted to update the LTS parameters. If such automatic reloading is not desired, set `ACME_POSTHOOK=` to empty. So reusing certificates from Traefik will work out of the box if the `/acme` directory in the Traefik container is also mounted in the `mlan/kopano` container. ```bash -docker run -d -name app -v proxy-acme:/acme:ro mlan/postfix +docker run -d -name proxy -v proxy-acme:/acme traefik +docker run -d -name app -v proxy-acme:/acme:ro mlan/kopano ``` Note, if the target certificate Common Name (CN) or Subject Alternate Name (SAN) is changed the container needs to be restarted. -Moreover, do not set any of `SSL_CERTIFICATE_FILE` and `SSL_PRIVATE_KEY_FILE` when using `ACME_FILE`. +Moreover, do not set any of `SSL_CERTIFICATE_FILE` and `SSL_PRIVATE_KEY_FILE` when using `ACME_FILE`. ## Logging `SYSLOG_LEVEL`, `LOG_LEVEL` @@ -408,7 +436,9 @@ Here some topics relevant for arranging a mail server are presented. ## Kopano WebApp HTTP access -The distribution installation of `kopano-webapp` only allow HTTPS access. The `mlan/kopano` image updates the configuration to `define("SECURE_COOKIES", false);` in `/etc/kopano/webapp/config.php` also allowing HTTP access. This can be useful when arranging the `mlan/kopano` container behind a reverse proxy, like [traefik](https://doc.traefik.io/traefik/), which then does the enforcement of HTTPS. +The distribution installation of `kopano-webapp` only allow HTTPS access. The `mlan/kopano` image updates the configuration to [`define("SECURE_COOKIES", false);`](https://documentation.kopano.io/webapp_admin_manual/config.html#secure-cookies) in `/etc/kopano/webapp/config.php` also allowing HTTP access. This can be useful when arranging the `mlan/kopano` container behind a reverse proxy, like [Traefik](https://doc.traefik.io/traefik/), which then does the enforcement of HTTPS. + +Note that prior to Kopano WebApp version 5.0.0 the corresponding parameter was `define("INSECURE_COOKIES", true);`, which provide the same functionality but with inverse logic. ## Mail client configuration diff --git a/src/docker/bin/docker-config.sh b/src/docker/bin/docker-config.sh index 471dba4..0cafd3a 100644 --- a/src/docker/bin/docker-config.sh +++ b/src/docker/bin/docker-config.sh @@ -141,14 +141,34 @@ dc_persist_mvdirs() { # # Conditionally change owner of files. +# -a all +# -r readable +# -w writable +# -x executable # -dc_chowncond() { +dc_cond_chown() { + dc_log 7 "Called with args: $@" + OPTIND=1 + local find_opts="! -perm -404" + while getopts ":arwx" opts; do + case "${opts}" in + a) find_opts="";; + r) find_opts="! -perm -404";; + w) find_opts="! -perm -606";; + x) find_opts="! -perm -505";; + esac + done + shift $((OPTIND -1)) local user=$1 - local dir=$2 + shift if id $user > /dev/null 2>&1; then - if [ -n "$(find $dir ! -user $user -print -exec chown -h $user: {} \;)" ]; then - dc_log 5 "Changed owner to $user for some files in $dir" - fi + for dir in $@; do + if [ -n "$(find $dir ! -user $user $find_opts -print -exec chown -h $user: {} \;)" ]; then + dc_log 5 "Changed owner to $user for some files in $dir" + fi + done + else + dc_log 3 "User $user is unknown." fi } diff --git a/src/docker/bin/docker-service.sh b/src/docker/bin/docker-service.sh index 171554a..79cef08 100755 --- a/src/docker/bin/docker-service.sh +++ b/src/docker/bin/docker-service.sh @@ -62,6 +62,7 @@ init_service() { local sourcefile= local sv_name cmd runsv_dir svlog_dir sv_log sv_down sv_force options dc_log 7 "Called with args $@" + OPTIND=1 while getopts ":dfhln:s:q" opts; do case "${opts}" in d) sv_down="down"; add_opt "down";; diff --git a/src/kopano/entry.d/10-kopano-common b/src/kopano/entry.d/10-kopano-common index c178dde..f15ed18 100755 --- a/src/kopano/entry.d/10-kopano-common +++ b/src/kopano/entry.d/10-kopano-common @@ -5,6 +5,9 @@ # Kopano now installs without any cfg files, so we just write custom values # into their target cfg file. # +# Variables defined in Dockerfile +# DOCKER_CONF_DIR1 DOCKER_CONF_DIR2 DOCKER_ACME_SSL_DIR DOCKER_APPL_SSL_DIR +# # # Configuration @@ -34,7 +37,7 @@ dagent_env_vars="LMTP_LISTEN SPAM_HEADER_NAME SPAM_HEADER_VALUE LOG_LEVEL" gateway_env_vars="BYPASS_AUTH DISABLE_PLAINTEXT_AUTH HTML_SAFETY_FILTER IMAP_CAPABILITY_IDLE IMAP_EXPUNGE_ON_DELETE IMAP_IGNORE_COMMAND_IDLE IMAP_LISTEN IMAP_MAX_FAIL_COMMANDS IMAP_MAX_MESSAGESIZE IMAP_ONLY_MAILFOLDERS IMAP_PUBLIC_FOLDERS IMAPS_LISTEN POP3_LISTEN POP3S_LISTEN DISABLE_PLAINTEXT_AUTH LOG_LEVEL" ical_env_vars="ICAL_LISTEN ICALS_LISTEN ENABLE_ICAL_GET LOG_LEVEL" ldap_env_vars="LDAP_URI LDAP_STARTTLS LDAP_BIND_USER LDAP_BIND_PASSWD LDAP_SEARCH_BASE LDAP_USER_TYPE_ATTRIBUTE_VALUE LDAP_GROUP_TYPE_ATTRIBUTE_VALUE LDAP_USER_SEARCH_FILTER" -search_env_vars="INDEX_PROCESSES INDEX_DRAFTS INDEX_JUNK SUGGESTIONS INDEX_ATTACHMENTS INDEX_ATTACHMENT_MAX_SIZE LOG_LEVEL LOG_METHOD" +search_env_vars="INDEX_PROCESSES INDEX_DRAFTS INDEX_JUNK SUGGESTIONS INDEX_ATTACHMENTS INDEX_ATTACHMENT_MAX_SIZE LOG_LEVEL" server_env_vars="MYSQL_HOST MYSQL_PORT MYSQL_DATABASE MYSQL_USER MYSQL_PASSWORD DISABLED_FEATURES USER_PLUGIN SEARCH_TIMEOUT LOG_LEVEL" spamd_env_vars="SPAM_DIR SPAM_DB HEADER_TAG LEARN_HAM HAM_DIR SA_GROUP LOG_LEVEL LOG_METHOD" spooler_env_vars="SMTP_SERVER SMTP_PORT LOG_LEVEL" @@ -100,6 +103,9 @@ kopano_export_tls_cert() { export SSL_CERTIFICATE_FILE=${SSL_CERTIFICATE_FILE-$DOCKER_APPL_SSL_CERT} export SSL_PRIVATE_KEY_FILE=${SSL_PRIVATE_KEY_FILE-$DOCKER_APPL_SSL_KEY} fi + if ([ -f "$SSL_CERTIFICATE_FILE" ] && [ -f "$SSL_PRIVATE_KEY_FILE" ]); then + dc_cond_chown $DOCKER_APPL_RUNAS $SSL_CERTIFICATE_FILE $SSL_PRIVATE_KEY_FILE + fi } # @@ -116,6 +122,5 @@ kopano_generate_tls_cert() { if ([ -z "$SSL_CERTIFICATE_FILE" ] && [ -n "$secure" ] && dc_is_installed openssl); then dc_log 4 "$secure, but no certs given, so generating self-signed cert for host $HOSTNAME" dc_tls_setup_selfsigned_cert $DOCKER_APPL_SSL_CERT $DOCKER_APPL_SSL_KEY - dc_chowncond $DOCKER_APPL_RUNAS $DOCKER_APPL_SSL_DIR fi } diff --git a/test/Makefile b/test/Makefile index 122713c..573fcf7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -10,7 +10,7 @@ _ip = $(shell docker inspect -f \ CURL_DBG ?= TST_NAME ?= test -SSL_KEYF ?= priv-key.pem +SSL_KEYF ?= priv_key.pem SSL_CRTF ?= cert.pem SSL_CRTD ?= 30 SSL_ACMF ?= acme.json @@ -70,10 +70,8 @@ APPS_ENV ?= $(APP_ENV) \ -e POP3S_LISTEN=*:995 \ -e ICALS_LISTEN=*:8443 APPF_ENV ?= $(APPS_ENV) \ --e SSL_CERTIFICATE_FILE=$(APP_SSLD)/$(SSL_CRTF) \ --e SSL_PRIVATE_KEY_FILE=$(APP_SSLD)/$(SSL_KEYF) +-v $(shell pwd)/$(TST_SSLD):/$(APP_SSLD) APPA_ENV ?= $(APPS_ENV) \ --e ACME_FILE=/$(TST_ACME) \ -v $(shell pwd)/$(TST_ACMD):/$(TST_ACMD) DB_NAME ?= db @@ -160,8 +158,6 @@ test-up_2: test-up-net test-up-auth_2 $(TST_CERT) # docker run -d $(DB_ENV) $(DB_VOL) $(DB_IMG) docker run -d $(APPF_ENV) $(APP_VOL) $(IMG_REPO):$(call _ver,$(IMG_VER),core) - docker cp $(TST_SSLD)/. $(APP_NAME):$(APP_SSLD) - docker exec -i $(APP_NAME) chown -R kopano: $(APP_SSLD) test-up_3: test-up-net test-up-auth_3 $(TST_ACME) # @@ -171,6 +167,7 @@ test-up_3: test-up-net test-up-auth_3 $(TST_ACME) # send: curl lmtp://app -> srv mysql://db & srv ldap://auth # recv: curl pop3s://app # recv: curl imaps://app + # web: curl http://app # # docker run -d $(DB_ENV) $(DB_VOL) $(DB_IMG) @@ -271,10 +268,11 @@ test-htop: test-debugtools test-tls: #--starttls imap docker run --rm -it $(NET_ENV) drwetter/testssl.sh $(APP_NAME):993 || true -test-cert-gen: $(TST_ACME) +test-ssl-gen: $(TST_ACME) -test-cert-rm: - rm $(TST_KEY) $(TST_CRT) $(TST_ACME) +test-ssl-rm: + rm -rf $(TST_SSLD) $(TST_ACMD) +# rm $(TST_KEY) $(TST_CRT) $(TST_ACME) $(TST_ACME): $(TST_ACMD) $(TST_CERT) bin/gen-acme-json.sh $(MAIL_US1)@$(MAIL_DOM) $(APP_FQDN) $(TST_KEY) $(TST_CERT) > $(TST_ACME)