diff --git a/.gitignore b/.gitignore index c6260e93..ed1cb451 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ chatmail.zone /custom/ docker-compose.yaml .env +/traefik/data/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4194d5e4..21dc2ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Add configuration parameters ([#614](https://github.com/chatmail/relay/pull/614)): + - `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`) - `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`) - `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`) diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index cc49e6d5..fdb64484 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -44,6 +44,9 @@ class Config: ) self.mtail_address = params.get("mtail_address") self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true" + self.use_foreign_cert_manager = ( + params.get("use_foreign_cert_manager", "false").lower() == "true" + ) self.change_kernel_settings = ( params.get("change_kernel_settings", "true").lower() == "true" ) diff --git a/chatmaild/src/chatmaild/ini/chatmail.ini.f b/chatmaild/src/chatmaild/ini/chatmail.ini.f index c04f6ef4..98dce681 100644 --- a/chatmaild/src/chatmaild/ini/chatmail.ini.f +++ b/chatmaild/src/chatmaild/ini/chatmail.ini.f @@ -60,6 +60,9 @@ postfix_reinject_port_incoming = 10026 # if set to "True" IPv6 is disabled disable_ipv6 = False +# if you set "True", acmetool will not be installed and you will have to manage certificates yourself. +use_foreign_cert_manager = False + # # Kernel settings # diff --git a/cmdeploy/src/cmdeploy/__init__.py b/cmdeploy/src/cmdeploy/__init__.py index 81b32b7a..569bc6bd 100644 --- a/cmdeploy/src/cmdeploy/__init__.py +++ b/cmdeploy/src/cmdeploy/__init__.py @@ -726,10 +726,11 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None: deploy_iroh_relay(config) # Deploy acmetool to have TLS certificates. - tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"] - deploy_acmetool( - domains=tls_domains, - ) + if not config.use_foreign_cert_manager: + tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"] + deploy_acmetool( + domains=tls_domains, + ) apt.packages( # required for setfacl for echobot diff --git a/docker/docker-compose-traefik.yaml b/docker/docker-compose-traefik.yaml new file mode 100644 index 00000000..94a0b360 --- /dev/null +++ b/docker/docker-compose-traefik.yaml @@ -0,0 +1,136 @@ +services: + chatmail: + build: + context: ./docker + dockerfile: chatmail_relay.dockerfile + tags: + - chatmail-relay:latest + image: chatmail-relay:latest + restart: unless-stopped + container_name: chatmail + depends_on: + - traefik-certs-dumper + cgroup: host # required for systemd + tty: true # required for logs + tmpfs: # required for systemd + - /tmp + - /run + - /run/lock + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + environment: #all possible variables you can check inside README and /chatmaild/src/chatmaild/ini/chatmail.ini.f + MAIL_DOMAIN: $MAIL_DOMAIN + # MAX_MESSAGE_SIZE: "50M" + # DEBUG_COMMANDS_ENABLED: "true" + # FORCE_REINIT_INI_FILE: "true" + # RECREATE_VENV: "false" + USE_FOREIGN_CERT_MANAGER: "true" + CHANGE_KERNEL_SETTINGS: "false" + PATH_TO_SSL: "${CERTS_ROOT_DIR_CONTAINER}/${MAIL_DOMAIN}" + ENABLE_CERTS_MONITORING: "true" + # CERTS_MONITORING_TIMEOUT: 60 + # IS_DEVELOPMENT_INSTANCE: "true" + ports: + - "25:25" + - "587:587" + - "143:143" + - "465:465" + - "993:993" + volumes: + ## system + - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd + - ./:/opt/chatmail + - ${CERTS_ROOT_DIR_HOST}:${CERTS_ROOT_DIR_CONTAINER}:ro + + ## data + - ./data/chatmail:/home + # - ./data/chatmail-dkimkeys:/etc/dkimkeys + # - ./data/chatmail-echobot:/run/echobot + # - ./data/chatmail-acme:/var/lib/acme + + ## custom resources + # - ./custom/www/src/index.md:/opt/chatmail/www/src/index.md + + ## debug + # - ./docker/files/setup_chatmail_docker.sh:/setup_chatmail_docker.sh + # - ./docker/files/entrypoint.sh:/entrypoint.sh + # - ./docker/files/update_ini.sh:/update_ini.sh + + labels: + - traefik.enable=true + - traefik.http.services.chatmail-relay.loadbalancer.server.scheme=https + - traefik.http.services.chatmail-relay.loadbalancer.server.port=443 + - traefik.http.services.chatmail-relay.loadbalancer.serverstransport=insecure@file + - traefik.http.routers.chatmail-relay.rule=Host(`${MAIL_DOMAIN}`) || Host(`mta-sts.${MAIL_DOMAIN}`) || Host(`www.${MAIL_DOMAIN}`) + - traefik.http.routers.chatmail-relay.service=chatmail-relay + - traefik.http.routers.chatmail-relay.tls=true + - traefik.http.routers.chatmail-relay.tls.certresolver=letsEncrypt + + traefik_init: + image: alpine:latest + restart: on-failure + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + working_dir: /app + entrypoint: sh -c ' + touch acme.json && + chown 0:0 ./acme.json && + chmod 600 ./acme.json' + volumes: + - ./traefik/data:/app + + traefik: + image: traefik:v3.3 + container_name: traefik + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + command: + - "--configFile=/config.yaml" + - "--certificatesresolvers.letsEncrypt.acme.email=${ACME_EMAIL}" + # ports: + # - "80:80" + # - "443:443" + network_mode: host + depends_on: + traefik_init: + condition: service_completed_successfully + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./traefik/config.yaml:/config.yaml + - ./traefik/data/acme.json:/acme.json + - ./traefik/dynamic-configs:/dynamic/conf + + traefik-certs-dumper: + image: ldez/traefik-certs-dumper:v2.10.0 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + depends_on: + - traefik + entrypoint: sh -c ' + apk add openssl && + while ! [ -e /data/acme.json ] + || ! [ `jq ".[] | .Certificates | length" /data/acme.json | jq -s "add" ` != 0 ]; do + sleep 1 + ; done + && traefik-certs-dumper file --version v3 --watch --domain-subdir=true + --source /data/acme.json --dest /data/letsencrypt/certs --post-hook "sh /post-hook.sh"' + environment: + CERTS_DIR: /data/letsencrypt/certs + volumes: + - ./traefik/data/letsencrypt:/data/letsencrypt + - ./traefik/data/acme.json:/data/acme.json + - ./traefik/post-hook.sh:/post-hook.sh diff --git a/docker/example.env b/docker/example.env index 48655812..fdaa193b 100644 --- a/docker/example.env +++ b/docker/example.env @@ -1 +1,5 @@ MAIL_DOMAIN="chat.example.com" +ACME_EMAIL="my.email@gmail.com" + +CERTS_ROOT_DIR_HOST="./traefik/data/letsencrypt/certs" +CERTS_ROOT_DIR_CONTAINER="/var/lib/acme/live" diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh index bce52a56..00efc219 100755 --- a/docker/files/entrypoint.sh +++ b/docker/files/entrypoint.sh @@ -3,6 +3,19 @@ set -eo pipefail unlink /etc/nginx/sites-enabled/default || true +if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then + if [ ! -f "$PATH_TO_SSL/fullchain" ]; then + echo "Error: file '$PATH_TO_SSL/fullchain' does not exist. Exiting..." > /dev/stderr + sleep 2 + exit 1 + fi + if [ ! -f "$PATH_TO_SSL/privkey" ]; then + echo "Error: file '$PATH_TO_SSL/privkey' does not exist. Exiting..." > /dev/stderr + sleep 2 + exit 1 + fi +fi + SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}" env_vars=$(printenv | cut -d= -f1 | xargs) diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 971d4461..3819e220 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -32,11 +32,25 @@ Please substitute it with your own domain. ``` ## Installation +When installing via Docker, there are several options: -1. Copy the file `./docker/docker-compose-default.yaml` to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and won’t cause conflicts when updating the git repository. +- Use the built-in nginx and acmetool in Chatmail container to host the chat and manage certificates. +- Use third-party tools for certificate management. + +For the third-party certificate manager example, traefik will be used, but you can use whatever is more convenient for you. + +1. Copy the file `./docker/docker-compose-default.yaml` or `./docker/docker-compose-traefik.yaml` and rename it to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and won’t cause conflicts when updating the git repository. ```shell cp ./docker/docker-compose-default.yaml docker-compose.yaml +## or +# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml +``` + +2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`. + +```shell +cp ./docker/example.env .env ``` 3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index e90cf889..31bd814d 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -29,10 +29,22 @@ Please substitute it with your own domain. ``` ## Installation +При установке через docker есть несколько вариантов: +- использовать встроенный в chatmail контейнер nginx и acmetool для хостинга чата и управления сертификатами. +- использовать сторонние инструменты для менеджмента сертификатов -1. Скопировать файл `./docker/docker-compose-default.yaml` в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория. +В качестве примера для стороннего менеджера сертификатов будет использоваться traefik, но вы можете использовать то что удобнее вам. + +1. Скопировать файл `./docker/docker-compose-default.yaml` или `./docker/docker-compose-traefik.yaml` и переименовать в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория. ```shell cp ./docker/docker-compose-default.yaml docker-compose.yaml +## or +# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml +``` + +2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используятся в `docker-compose.yaml`. +```shell +cp ./docker/example.env .env ``` 3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. diff --git a/traefik/config.yaml b/traefik/config.yaml new file mode 100644 index 00000000..ff55284d --- /dev/null +++ b/traefik/config.yaml @@ -0,0 +1,33 @@ +log: + level: TRACE + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + permanent: true + websecure: + address: ":443" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + file: + directory: /dynamic/conf + watch: true + +serverstransport: + insecureskipverify: true + +certificatesResolvers: + letsEncrypt: + acme: + storage: /acme.json + caServer: "https://acme-v02.api.letsencrypt.org/directory" + tlschallenge: true + httpChallenge: + entryPoint: web diff --git a/traefik/dynamic-configs/insecure.yaml b/traefik/dynamic-configs/insecure.yaml new file mode 100644 index 00000000..acafed2e --- /dev/null +++ b/traefik/dynamic-configs/insecure.yaml @@ -0,0 +1,4 @@ +http: + serversTransports: + insecure: + insecureSkipVerify: true diff --git a/traefik/post-hook.sh b/traefik/post-hook.sh new file mode 100755 index 00000000..377e00fc --- /dev/null +++ b/traefik/post-hook.sh @@ -0,0 +1,15 @@ +CERTS_DIR=${CERTS_DIR:-"/data/letsencrypt/certs"} + +echo "CERTS_DIR: $CERTS_DIR" + +for dir in "$CERTS_DIR"/*/; do + echo "Processing: $dir" + cd "$dir" + if [ -f "certificate.crt" ]; then + ln -sf certificate.crt fullchain + fi + if [ -f "privatekey.key" ]; then + ln -sf privatekey.key privkey + fi + cd - +done