Adds a new tls_external_cert_and_key config option for chatmail servers
that manage their own TLS certificates (e.g. via an external ACME client
or a load balancer).
A systemd path unit (tls-cert-reload.path) watches the certificate file
via inotify and automatically reloads dovecot and nginx when it changes.
Postfix reads certs per TLS handshake so needs no reload.
Also extracts openssl_selfsigned_args() so cert generation parameters
are shared between SelfSignedTlsDeployer and the e2e test.
Add Docker-based deployment: Dockerfile based on systemd image,
docker-compose.yaml, build script, entrypoint, external certificate
monitoring, CI workflow, and documentation.
This builds on the chatmaild/cmdeploy preparation in the previous
commit (j4n/docker-prep-chatmail) which added the env-var-driven
feature flags (CHATMAIL_NOSYSCTL, CHATMAIL_NOPORTCHECK, CHATMAIL_NOACME)
and @local deployment support needed by the container.
This is commit 2 of 3 to merge squashed changes on j4n/docker and docker
branches, original commits were beef0ec..606f36e
Architecture overview (mostly by original author Keonik1):
- Debian-systemd image wrapping the existing cmdeploy install
- Host networking to not manually expose the many ports needed
- Config via MAIL_DOMAIN env var or (new) mounted chatmail.ini
- New: cmdeploy stages: install at build, configure+activate at startup
- New: Monitoring service for external certs via systemd timer (chatmail-certmon)
- New: Image version tracking for automatic upgrade detection (cm + config hash)
- New: docker-compose.override.yaml pattern for user customizations
- New: GitHub Actions CI for ghcr.io image builds
Traefik reverse-proxy support is prepared but the specific files are
excluded from this PR and will be submitted separately.
TODO:
- [ ] Pull out CHATMAIL_NOACME as PR #855 introduced a proper mechanism
- [ ] Check if underlying image could be based on regular debian-slim
images with a step to enable systemd, similar to
https://github.com/alexdzyoba/docker-debian-systemd
Files added:
.dockerignore
.github/workflows/docker-build.yaml
docker-compose.yaml
docker-compose.override.yaml.example
docker/build.sh
docker/chatmail_relay.dockerfile
docker/files/chatmail-certmon.{service,sh,timer}
docker/files/entrypoint.sh
docker/files/setup_chatmail.service
docker/files/setup_chatmail_docker.sh
env.example
doc/source/docker.rst
Files modified:
.gitignore
doc/source/getting_started.rst
doc/source/index.rst
Co-authored-by: Keonik1 <keonik.dev@gmail.com>
Co-authored-by: missytake <missytake@systemli.org>
- chatmaild:
- basedeploy.py: Add has_systemd() guard. During Docker image builds
there's no running systemd, so deployers that query SystemdEnabled
facts would crash; this change might also be helpful for non-systemd
platforms.
- cmdeploy:
- cmdeploy.py:
- when deploying to @docker, auto-set CHATMAIL_NOPORTCHECK and
CHATMAIL_NOSYSCTL since neither makes sense inside a container
- --config default now reads CHATMAIL_INI env var, so Docker
entrypoints can point to a mounted ini without CLI flags.
- deployers.py:
- skip port check / CHATMAIL_NOPORTCHECK
- skip echobot systemd cleanup w/ has_systemd
- dovecot/deployer.py:
- Guard sysctl writes behind CHATMAIL_NOSYSCTL
- invert dovecot install check so it works without systemd
- sshexec.py: Add __call__ to LocalExec so cmdeploy status works with
@local target. Without it, cmdeploy status tried to call the
executor directly and got TypeError.
Consolidated from j4n/docker branch commits (selection):
- 8953fde feat(cmdeploy): read CHATMAIL_INI env var for default --config path
- 81d7782 fix(cmdeploy): add __call__ to LocalExec so status works with @local
- 8bba78e docker: disable port check if docker is running. fix#694
- 865b514 docker: replace config flags with env vars, drop docker param (instead of f26cb08)
Files: cmdeploy/src/cmdeploy/{basedeploy,cmdeploy,deployers,sshexec,dovecot/deployer}.py
Co-authored-by: Keonik1 <keonik.dev@gmail.com>
Co-authored-by: missytake <missytake@systemli.org>
- chatmaild:
- basedeploy.py: Add has_systemd() guard. During Docker image builds
there's no running systemd, so deployers that query SystemdEnabled
facts would crash; this change might also be helpful for non-systemd
platforms.
- cmdeploy:
- cmdeploy.py:
- when deploying to @docker, auto-set CHATMAIL_NOPORTCHECK and
CHATMAIL_NOSYSCTL since neither makes sense inside a container
- --config default now reads CHATMAIL_INI env var, so Docker
entrypoints can point to a mounted ini without CLI flags.
- deployers.py:
- skip port check / CHATMAIL_NOPORTCHECK
- skip echobot systemd cleanup w/ has_systemd
- dovecot/deployer.py:
- Guard sysctl writes behind CHATMAIL_NOSYSCTL
- invert dovecot install check so it works without systemd
- sshexec.py: Add __call__ to LocalExec so cmdeploy status works with
@local target. Without it, cmdeploy status tried to call the
executor directly and got TypeError.
Consolidated from j4n/docker branch commits (selection):
- 8953fde feat(cmdeploy): read CHATMAIL_INI env var for default --config path
- 81d7782 fix(cmdeploy): add __call__ to LocalExec so status works with @local
- 8bba78e docker: disable port check if docker is running. fix#694
- 865b514 docker: replace config flags with env vars, drop docker param (instead of f26cb08)
Files: cmdeploy/src/cmdeploy/{basedeploy,cmdeploy,deployers,sshexec,dovecot/deployer}.py
Co-authored-by: Keonik1 <keonik.dev@gmail.com>
Co-authored-by: missytake <missytake@systemli.org>
feat: support self-signed TLS via underscore domain convention
Domains starting with "_" (e.g. _chat.example.org) automatically use
self-signed TLS certificates instead of ACME/Let's Encrypt. The TLS
mode is derived from the domain name — no separate config option needed.
Internally, when config.tls_cert_mode is "self" (underscore domain):
- Generate self-signed certificates via openssl
- Set Postfix smtp_tls_security_level to "encrypt" (opportunistic TLS)
- Add smtp_tls_policy_map entry for underscore domains
- Skip ACME, MTA-STS and www CNAME checks in `cmdeploy dns`
- Serve /new via GET (not redirect to dcaccount:) with rate-limiting
(nginx limit_req, 2r/s burst=5)
- Return dclogin: URLs with ic=3 (AcceptInvalidCertificates) from /new
- Render QR codes client-side via JavaScript and qrcode-svg
- Use config.tls_cert_path/tls_key_path in Postfix, Dovecot and nginx
templates instead of hardcoded ACME paths
Ensure that the interface for mtail_address is available and fix a bug
in port checking where single services were always passing regardless of
the specified service name.
filtermail rate limiter is using leaky bucket
algorithm (GCRA).
Exceeting the limit requires sending
at least max_user_send_per_minute
messages to exhaust allowed burst,
and then sending messages faster
than the leak rate.
As we don't know how fast is the network
between the server and test runner,
try to send 3 times max_user_send_per_minute
messages to ensure the test does not
fail randomly.
The ! character in != is an invalid token in Dovecot's unified filter
language (2.3.12+). The parser expected a comparison operator (=, >, <)
and choked on !.