mirror of
https://github.com/chatmail/relay.git
synced 2026-08-11 11:00:52 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02c7d3da58 | |||
| 14f829003d | |||
| 5da9aaeb37 | |||
| d068052f0c | |||
| fa5afadaaa | |||
| eb862f1645 | |||
| 6f3039509c | |||
| 504fb249a9 |
@@ -20,6 +20,8 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
no-dns:
|
no-dns:
|
||||||
name: LXC deploy and test
|
name: LXC deploy and test
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main
|
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main
|
||||||
with:
|
with:
|
||||||
cmlxc_version: main
|
cmlxc_version: main
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ jobs:
|
|||||||
|
|
||||||
lxc-test:
|
lxc-test:
|
||||||
name: LXC deploy and test
|
name: LXC deploy and test
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main
|
uses: chatmail/cmlxc/.github/workflows/lxc-test.yml@main
|
||||||
with:
|
with:
|
||||||
cmlxc_version: main
|
cmlxc_version: main
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.CHATMAIL_STAGING_SSHKEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.CHATMAIL_STAGING_SSHKEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -rILvh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/doc/build/ "${{ secrets.USERNAME }}@chatmail.at:/var/www/html/staging.chatmail.at/doc/relay/${STEPS_PREPARE_OUTPUTS_PRID}/"
|
rsync -rILvh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/doc/build/ "${{ secrets.USERNAME }}@chatmail.at:${STEPS_PREPARE_OUTPUTS_PRID}/"
|
||||||
env:
|
env:
|
||||||
STEPS_PREPARE_OUTPUTS_PRID: ${{ steps.prepare.outputs.prid }}
|
STEPS_PREPARE_OUTPUTS_PRID: ${{ steps.prepare.outputs.prid }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
# Chatmail relays for end-to-end encrypted email
|
# Chatmail relays for end-to-end encrypted email
|
||||||
|
|
||||||
Chatmail relay servers are interoperable Mail Transport Agents (MTAs) designed for:
|
Chatmail relay servers are interoperable Mail Transport Agents (MTAs) designed for:
|
||||||
|
|
||||||
- **Zero State:** no private data or metadata collected, messages are auto-deleted, low disk usage
|
- **Zero State:** no private data or metadata collected, messages are auto-deleted, low disk usage
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Chatmail relay servers are interoperable Mail Transport Agents (MTAs) designed f
|
|||||||
- **Reliable Federation and Decentralization:** No spam or IP reputation checks, federating
|
- **Reliable Federation and Decentralization:** No spam or IP reputation checks, federating
|
||||||
depends on established IETF standards and protocols.
|
depends on established IETF standards and protocols.
|
||||||
|
|
||||||
This repository contains everything needed to setup a ready-to-use chatmail relay on an ssh-reachable host.
|
This repository contains everything needed to setup a ready-to-use chatmail relay on an ssh-reachable host.
|
||||||
For getting started and more information please refer to the web version of this repositories' documentation at
|
For getting started and more information please refer to the web version of this repositories' documentation at
|
||||||
|
|
||||||
[https://chatmail.at/doc/relay](https://chatmail.at/doc/relay)
|
[https://chatmail.at/doc/relay](https://chatmail.at/doc/relay)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ version = "0.3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iniconfig",
|
"iniconfig",
|
||||||
"filelock",
|
"filelock",
|
||||||
|
"psutil",
|
||||||
"requests",
|
"requests",
|
||||||
"crypt-r >= 3.13.1 ; python_version >= '3.13'",
|
"crypt-r >= 3.13.1 ; python_version >= '3.13'",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from random import randint
|
|
||||||
|
|
||||||
import iniconfig
|
import iniconfig
|
||||||
|
|
||||||
@@ -42,11 +41,6 @@ class Config:
|
|||||||
self.username_max_length = int(params.pop("username_max_length", 9))
|
self.username_max_length = int(params.pop("username_max_length", 9))
|
||||||
self.password_min_length = int(params.pop("password_min_length", 9))
|
self.password_min_length = int(params.pop("password_min_length", 9))
|
||||||
self.www_folder = params.pop("www_folder", "")
|
self.www_folder = params.pop("www_folder", "")
|
||||||
|
|
||||||
self.imap_port = int(params.pop("imap_port", 143))
|
|
||||||
self.imaps_port = int(params.pop("imaps_port", 993))
|
|
||||||
self.smtp_port = int(params.pop("smtp_port", 587))
|
|
||||||
self.smtps_port = int(params.pop("smtps_port", 465))
|
|
||||||
self.filtermail_smtp_port = int(params.pop("filtermail_smtp_port", "10080"))
|
self.filtermail_smtp_port = int(params.pop("filtermail_smtp_port", "10080"))
|
||||||
self.filtermail_smtp_port_incoming = int(
|
self.filtermail_smtp_port_incoming = int(
|
||||||
params.pop("filtermail_smtp_port_incoming", "10081")
|
params.pop("filtermail_smtp_port_incoming", "10081")
|
||||||
@@ -81,6 +75,16 @@ class Config:
|
|||||||
self.privacy_pdo = params.pop("privacy_pdo", None)
|
self.privacy_pdo = params.pop("privacy_pdo", None)
|
||||||
self.privacy_supervisor = params.pop("privacy_supervisor", None)
|
self.privacy_supervisor = params.pop("privacy_supervisor", None)
|
||||||
|
|
||||||
|
self.max_load_1m = float(params.pop("max_load_1m", 5))
|
||||||
|
self.min_available_memory_mb = parse_size_mb(
|
||||||
|
params.pop("min_available_memory", "200M")
|
||||||
|
)
|
||||||
|
self.min_free_disk_space_mb = parse_size_mb(
|
||||||
|
params.pop("min_free_disk_space", "1G")
|
||||||
|
)
|
||||||
|
self.max_imap_connections = int(params.pop("max_imap_connections", 10000))
|
||||||
|
self.max_smtp_connections = int(params.pop("max_smtp_connections", 1000))
|
||||||
|
|
||||||
# TLS certificate management.
|
# TLS certificate management.
|
||||||
# If tls_external_cert_and_key is set, use externally managed certs.
|
# If tls_external_cert_and_key is set, use externally managed certs.
|
||||||
# Otherwise derived from the domain name:
|
# Otherwise derived from the domain name:
|
||||||
@@ -144,15 +148,8 @@ def parse_size_mb(limit):
|
|||||||
|
|
||||||
def write_initial_config(inipath, mail_domain, overrides):
|
def write_initial_config(inipath, mail_domain, overrides):
|
||||||
"""Write out default config file, using the specified config value overrides."""
|
"""Write out default config file, using the specified config value overrides."""
|
||||||
content = get_default_config_content(mail_domain, **overrides).splitlines()
|
content = get_default_config_content(mail_domain, **overrides)
|
||||||
used_ports = [25, 53, 80, 143, 402, 443, 465, 587, 993, 3340, 3903, 3904, 8443, 10080, 10081, 10082, 10083, 10025, 10026]
|
inipath.write_text(content)
|
||||||
for config_key in ["smtp_port", "imap_port", "smtps_port", "imaps_port"]:
|
|
||||||
value = randint(1024, 65536)
|
|
||||||
while value in used_ports:
|
|
||||||
value = randint(65535)
|
|
||||||
used_ports.append(value)
|
|
||||||
content.append(f"{config_key} = {value}")
|
|
||||||
inipath.write_text("\n".join(content))
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_config_content(mail_domain, **overrides):
|
def get_default_config_content(mail_domain, **overrides):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ except ImportError:
|
|||||||
from .config import Config, read_config
|
from .config import Config, read_config
|
||||||
from .dictproxy import DictProxy
|
from .dictproxy import DictProxy
|
||||||
from .migrate_db import migrate_from_db_to_maildir
|
from .migrate_db import migrate_from_db_to_maildir
|
||||||
|
from .syslimits import has_sufficient_resources
|
||||||
|
|
||||||
NOCREATE_FILE = "/etc/chatmail-nocreate"
|
NOCREATE_FILE = "/etc/chatmail-nocreate"
|
||||||
VALID_LOCALPART_RE = re.compile(r"^[a-z0-9._-]+$")
|
VALID_LOCALPART_RE = re.compile(r"^[a-z0-9._-]+$")
|
||||||
@@ -147,6 +148,8 @@ class AuthDictProxy(DictProxy):
|
|||||||
return userdata
|
return userdata
|
||||||
if not is_allowed_to_create(self.config, addr, cleartext_password):
|
if not is_allowed_to_create(self.config, addr, cleartext_password):
|
||||||
return
|
return
|
||||||
|
if not has_sufficient_resources(self.config):
|
||||||
|
return
|
||||||
|
|
||||||
lock = filelock.FileLock(str(user.password_path) + ".lock", timeout=5)
|
lock = filelock.FileLock(str(user.password_path) + ".lock", timeout=5)
|
||||||
with lock:
|
with lock:
|
||||||
|
|||||||
@@ -42,6 +42,33 @@ mail_domain = {mail_domain}
|
|||||||
# minimum length a password must have
|
# minimum length a password must have
|
||||||
#password_min_length = 9
|
#password_min_length = 9
|
||||||
|
|
||||||
|
#
|
||||||
|
# System resource limits
|
||||||
|
#
|
||||||
|
|
||||||
|
# The following three limits refuse creation of new addresses
|
||||||
|
# while existing addresses keep working.
|
||||||
|
# Rejections are logged by the doveauth service.
|
||||||
|
|
||||||
|
# Maximum 1-minute load average, as reported by "uptime";
|
||||||
|
# it counts processes waiting for disk I/O as well as for CPU.
|
||||||
|
#max_load_1m = 5
|
||||||
|
|
||||||
|
# Minimum memory available without swapping.
|
||||||
|
#min_available_memory = 200M
|
||||||
|
|
||||||
|
# Minimum free disk space on the file system holding the mailboxes.
|
||||||
|
#min_free_disk_space = 1G
|
||||||
|
|
||||||
|
# Maximum number of concurrent IMAP connections
|
||||||
|
# (the Dovecot imap process limit).
|
||||||
|
#max_imap_connections = 10000
|
||||||
|
|
||||||
|
# Maximum number of concurrent SMTP connections
|
||||||
|
# on each of the submission and smtps ports (the Postfix process limit).
|
||||||
|
# A single client IP may use up to a fifth of this.
|
||||||
|
#max_smtp_connections = 1000
|
||||||
|
|
||||||
# Use externally managed TLS certificates instead of built-in acmetool.
|
# Use externally managed TLS certificates instead of built-in acmetool.
|
||||||
# Paths refer to files on the deployment server (not the build machine).
|
# Paths refer to files on the deployment server (not the build machine).
|
||||||
# Both files must already exist before running cmdeploy.
|
# Both files must already exist before running cmdeploy.
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Detect whether the system is at its limits."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
MB = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def read_value(getter):
|
||||||
|
try:
|
||||||
|
return getter()
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning("ignoring unreadable system limit: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_sufficient_resources(config):
|
||||||
|
"""Return False if load, memory or disk exceeds a configured limit."""
|
||||||
|
load = read_value(lambda: psutil.getloadavg()[0])
|
||||||
|
mem = read_value(lambda: psutil.virtual_memory().available // MB)
|
||||||
|
disk = read_value(lambda: psutil.disk_usage(str(config.mailboxes_dir)).free // MB)
|
||||||
|
if load is not None and load > config.max_load_1m:
|
||||||
|
msg = f"load avg {load:.2f} > {config.max_load_1m:.2f}"
|
||||||
|
elif mem is not None and mem < config.min_available_memory_mb:
|
||||||
|
msg = f"available memory {mem}MB < {config.min_available_memory_mb}MB"
|
||||||
|
elif disk is not None and disk < config.min_free_disk_space_mb:
|
||||||
|
msg = f"free disk {disk}MB < {config.min_free_disk_space_mb}MB"
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
logging.warning("registration rejected: %s", msg)
|
||||||
|
return False
|
||||||
@@ -20,6 +20,10 @@ def make_config(tmp_path):
|
|||||||
basedir.mkdir(parents=True, exist_ok=True)
|
basedir.mkdir(parents=True, exist_ok=True)
|
||||||
overrides = settings.copy() if settings else {}
|
overrides = settings.copy() if settings else {}
|
||||||
overrides["mailboxes_dir"] = str(basedir)
|
overrides["mailboxes_dir"] = str(basedir)
|
||||||
|
# permissive resource limits so tests never depend on host load/memory/disk
|
||||||
|
overrides.setdefault("max_load_1m", "99999")
|
||||||
|
overrides.setdefault("min_available_memory", "0")
|
||||||
|
overrides.setdefault("min_free_disk_space", "0")
|
||||||
write_initial_config(inipath, mail_domain, overrides=overrides)
|
write_initial_config(inipath, mail_domain, overrides=overrides)
|
||||||
return read_config(inipath)
|
return read_config(inipath)
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ def test_read_config_basic_using_defaults(tmp_path, maildomain):
|
|||||||
assert example_config.username_min_length == 9
|
assert example_config.username_min_length == 9
|
||||||
assert example_config.username_max_length == 9
|
assert example_config.username_max_length == 9
|
||||||
assert example_config.password_min_length == 9
|
assert example_config.password_min_length == 9
|
||||||
|
assert example_config.max_imap_connections == 10000
|
||||||
|
assert example_config.max_smtp_connections == 1000
|
||||||
assert example_config._unused_keys == []
|
assert example_config._unused_keys == []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -202,3 +202,17 @@ def test_50_concurrent_lookups_different_accounts(gencreds, dictproxy):
|
|||||||
res = results.get()
|
res = results.get()
|
||||||
if res is not None:
|
if res is not None:
|
||||||
pytest.fail(f"concurrent lookup failed\n{res}")
|
pytest.fail(f"concurrent lookup failed\n{res}")
|
||||||
|
|
||||||
|
|
||||||
|
def test_insufficient_resources_block_creation_not_existing_logins(
|
||||||
|
dictproxy, gencreds, monkeypatch
|
||||||
|
):
|
||||||
|
addr, password = gencreds()
|
||||||
|
assert dictproxy.lookup_passdb(addr, password)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
chatmaild.doveauth, "has_sufficient_resources", lambda config: False
|
||||||
|
)
|
||||||
|
newaddr, newpassword = gencreds()
|
||||||
|
assert not dictproxy.lookup_passdb(newaddr, newpassword)
|
||||||
|
assert dictproxy.lookup_passdb(addr, password)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import shutil
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
from chatmaild.syslimits import has_sufficient_resources
|
||||||
|
|
||||||
|
PERMISSIVE = {
|
||||||
|
"max_load_1m": "99999",
|
||||||
|
"min_available_memory": "0",
|
||||||
|
"min_free_disk_space": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_rejects_constrained_system(make_config, caplog):
|
||||||
|
assert has_sufficient_resources(make_config("chat.example.org", PERMISSIVE))
|
||||||
|
for settings in (
|
||||||
|
{"max_load_1m": "-1.0"},
|
||||||
|
{"min_available_memory": "99999999G"},
|
||||||
|
{"min_free_disk_space": "99999999G"},
|
||||||
|
):
|
||||||
|
config = make_config("chat.example.org", PERMISSIVE | settings)
|
||||||
|
caplog.clear()
|
||||||
|
assert not has_sufficient_resources(config), settings
|
||||||
|
assert "registration rejected" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_unreadable_disk_does_not_reject(make_config, caplog):
|
||||||
|
config = make_config(
|
||||||
|
"chat.example.org", PERMISSIVE | {"min_free_disk_space": "99999999G"}
|
||||||
|
)
|
||||||
|
shutil.rmtree(config.mailboxes_dir)
|
||||||
|
assert has_sufficient_resources(config)
|
||||||
|
assert "ignoring" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_unreadable_value_keeps_other_checks(make_config, monkeypatch, caplog):
|
||||||
|
def raise_error(*args):
|
||||||
|
raise psutil.Error("dud")
|
||||||
|
|
||||||
|
monkeypatch.setattr(psutil, "getloadavg", raise_error)
|
||||||
|
config = make_config(
|
||||||
|
"chat.example.org", PERMISSIVE | {"min_free_disk_space": "99999999G"}
|
||||||
|
)
|
||||||
|
assert not has_sufficient_resources(config)
|
||||||
|
assert "ignoring" in caplog.text
|
||||||
@@ -32,6 +32,7 @@ from .external.deployer import ExternalTlsDeployer
|
|||||||
from .filtermail.deployer import FiltermailDeployer
|
from .filtermail.deployer import FiltermailDeployer
|
||||||
from .mtail.deployer import MtailDeployer
|
from .mtail.deployer import MtailDeployer
|
||||||
from .nginx.deployer import NginxDeployer
|
from .nginx.deployer import NginxDeployer
|
||||||
|
from .websockify.deployer import WebsockifyDeployer
|
||||||
from .opendkim.deployer import OpendkimDeployer
|
from .opendkim.deployer import OpendkimDeployer
|
||||||
from .postfix.deployer import PostfixDeployer
|
from .postfix.deployer import PostfixDeployer
|
||||||
from .selfsigned.deployer import SelfSignedTlsDeployer
|
from .selfsigned.deployer import SelfSignedTlsDeployer
|
||||||
@@ -138,9 +139,22 @@ class UnboundDeployer(Deployer):
|
|||||||
# On an IPv4-only system, if unbound is started but not configured,
|
# On an IPv4-only system, if unbound is started but not configured,
|
||||||
# it causes subsequent steps to fail to resolve hosts.
|
# it causes subsequent steps to fail to resolve hosts.
|
||||||
with blocked_service_startup():
|
with blocked_service_startup():
|
||||||
|
# dns-root-data is an optional package
|
||||||
|
# that contains /usr/share/dns/root.key
|
||||||
|
#
|
||||||
|
# This file is copied into /var/lib/unbound/root.key
|
||||||
|
# at the start of "unbound" systemd unit
|
||||||
|
# by /usr/libexec/unbound-helper shell script
|
||||||
|
# from the "unbound" package as of version 1.17.1-2+deb12u4
|
||||||
|
#
|
||||||
|
# The same /var/lib/unbound/root.key can be retrieved directly
|
||||||
|
# following the procedure from
|
||||||
|
# <https://www.rfc-editor.org/info/rfc7958/#section-3.1>
|
||||||
|
# with "unbound-anchor -a /var/lib/unbound/root.key"
|
||||||
|
# We don't install and use "unbound-anchor".
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install unbound",
|
name="Install unbound",
|
||||||
packages=["unbound", "unbound-anchor", "dnsutils"],
|
packages=["unbound", "dns-root-data", "dnsutils"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure(self):
|
def configure(self):
|
||||||
@@ -166,12 +180,6 @@ class UnboundDeployer(Deployer):
|
|||||||
dest="/etc/resolv.conf",
|
dest="/etc/resolv.conf",
|
||||||
force=True,
|
force=True,
|
||||||
)
|
)
|
||||||
server.shell(
|
|
||||||
name="Generate root keys for validating DNSSEC",
|
|
||||||
commands=[
|
|
||||||
"unbound-anchor -a /var/lib/unbound/root.key || true",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.ensure_directory(
|
self.ensure_directory(
|
||||||
path="/etc/unbound/unbound.conf.d",
|
path="/etc/unbound/unbound.conf.d",
|
||||||
)
|
)
|
||||||
@@ -246,6 +254,13 @@ class LegacyRemoveDeployer(Deployer):
|
|||||||
def install(self):
|
def install(self):
|
||||||
apt.packages(name="Remove rspamd", packages="rspamd", present=False)
|
apt.packages(name="Remove rspamd", packages="rspamd", present=False)
|
||||||
|
|
||||||
|
# unbound-anchor was used to download /var/lib/unbound/root.key
|
||||||
|
# It is replaced by dns-root-data which contains /usr/share/dns/root.key.
|
||||||
|
# unbound systemd unit copies /usr/share/dns/root.key
|
||||||
|
# into /var/lib/unbound/root.key automatically on start
|
||||||
|
# as long as /usr/share/dns/root.key is present.
|
||||||
|
apt.packages(name="Remove unbound-anchor", packages="unbound-anchor", present=False)
|
||||||
|
|
||||||
# remove historic expunge script
|
# remove historic expunge script
|
||||||
# which is now implemented through a systemd timer (chatmail-expire)
|
# which is now implemented through a systemd timer (chatmail-expire)
|
||||||
self.remove_file("/etc/cron.d/expunge")
|
self.remove_file("/etc/cron.d/expunge")
|
||||||
@@ -356,6 +371,8 @@ class ChatmailVenvDeployer(Deployer):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.units = (
|
self.units = (
|
||||||
|
# doveauth must restart when chatmaild/ini file changes
|
||||||
|
"doveauth",
|
||||||
"chatmail-metadata",
|
"chatmail-metadata",
|
||||||
"lastlogin",
|
"lastlogin",
|
||||||
"chatmail-expire",
|
"chatmail-expire",
|
||||||
@@ -496,15 +513,15 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
|||||||
if config.tls_cert_mode == "acme":
|
if config.tls_cert_mode == "acme":
|
||||||
port_services.append(("acmetool", 402))
|
port_services.append(("acmetool", 402))
|
||||||
port_services += [
|
port_services += [
|
||||||
(["imap-login", "dovecot", "nginx"], config.imap_port),
|
(["imap-login", "dovecot"], 143),
|
||||||
# acmetool previously listened on port 80,
|
# acmetool previously listened on port 80,
|
||||||
# so don't complain during upgrade that moved it to port 402
|
# so don't complain during upgrade that moved it to port 402
|
||||||
# and gave the port to nginx.
|
# and gave the port to nginx.
|
||||||
(["acmetool", "nginx"], 80),
|
(["acmetool", "nginx"], 80),
|
||||||
("nginx", 443),
|
("nginx", 443),
|
||||||
(["master", "smtpd", "nginx"], config.smtp_port),
|
(["master", "smtpd"], 465),
|
||||||
(["master", "smtpd", "nginx"], config.smtps_port),
|
(["master", "smtpd"], 587),
|
||||||
(["imap-login", "dovecot", "nginx"], config.imaps_port),
|
(["imap-login", "dovecot"], 993),
|
||||||
("iroh-relay", 3340),
|
("iroh-relay", 3340),
|
||||||
("mtail", 3903),
|
("mtail", 3903),
|
||||||
("stats", 3904),
|
("stats", 3904),
|
||||||
@@ -549,6 +566,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
|
|||||||
PostfixDeployer(config, disable_mail),
|
PostfixDeployer(config, disable_mail),
|
||||||
FcgiwrapDeployer(),
|
FcgiwrapDeployer(),
|
||||||
NginxDeployer(config),
|
NginxDeployer(config),
|
||||||
|
WebsockifyDeployer(config),
|
||||||
MtailDeployer(config.mtail_address),
|
MtailDeployer(config.mtail_address),
|
||||||
GithashDeployer(),
|
GithashDeployer(),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class DovecotDeployer(Deployer):
|
|||||||
def __init__(self, config, disable_mail):
|
def __init__(self, config, disable_mail):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.disable_mail = disable_mail
|
self.disable_mail = disable_mail
|
||||||
self.units = ["doveauth"]
|
self.units = []
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
arch = host.get_fact(Arch)
|
arch = host.get_fact(Arch)
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ default_client_limit = 20000
|
|||||||
# the following warning will be logged:
|
# the following warning will be logged:
|
||||||
# Warning: service(imap): process_limit (1024) reached, client connections are being dropped
|
# Warning: service(imap): process_limit (1024) reached, client connections are being dropped
|
||||||
service imap {
|
service imap {
|
||||||
process_limit = 50000
|
process_limit = {{ config.max_imap_connections }}
|
||||||
}
|
}
|
||||||
|
|
||||||
mail_server_admin = mailto:root@{{ config.mail_domain }}
|
{% if config.privacy_mail %}
|
||||||
mail_server_comment = Chatmail server
|
# Advertised to clients as IMAP METADATA /shared/admin (RFC 5464).
|
||||||
|
# The privacy_mail contact from chatmail.ini is used because it is
|
||||||
|
# the only address set by an operator.
|
||||||
|
mail_server_admin = mailto:{{ config.privacy_mail }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
# `zlib` enables compressing messages stored in the maildir.
|
# `zlib` enables compressing messages stored in the maildir.
|
||||||
# See
|
# See
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ counter created_nonci_accounts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# doveauth refusing new addresses because a chatmail.ini
|
||||||
|
# system resource limit is exceeded.
|
||||||
|
counter rejected_registrations
|
||||||
|
/registration rejected: / {
|
||||||
|
rejected_registrations++
|
||||||
|
}
|
||||||
|
|
||||||
counter postfix_timeouts
|
counter postfix_timeouts
|
||||||
/timeout after DATA/ {
|
/timeout after DATA/ {
|
||||||
postfix_timeouts++
|
postfix_timeouts++
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
<displayShortName>{{ config.mail_domain }}</displayShortName>
|
<displayShortName>{{ config.mail_domain }}</displayShortName>
|
||||||
<incomingServer type="imap">
|
<incomingServer type="imap">
|
||||||
<hostname>{{ config.mail_domain }}</hostname>
|
<hostname>{{ config.mail_domain }}</hostname>
|
||||||
<port>{{ config.imaps_port }}</port>
|
<port>993</port>
|
||||||
<socketType>SSL</socketType>
|
<socketType>SSL</socketType>
|
||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
</incomingServer>
|
</incomingServer>
|
||||||
<incomingServer type="imap">
|
<incomingServer type="imap">
|
||||||
<hostname>{{ config.mail_domain }}</hostname>
|
<hostname>{{ config.mail_domain }}</hostname>
|
||||||
<port>{{ config.imap_port }}</port>
|
<port>143</port>
|
||||||
<socketType>STARTTLS</socketType>
|
<socketType>STARTTLS</socketType>
|
||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
@@ -28,14 +28,14 @@
|
|||||||
</incomingServer>
|
</incomingServer>
|
||||||
<outgoingServer type="smtp">
|
<outgoingServer type="smtp">
|
||||||
<hostname>{{ config.mail_domain }}</hostname>
|
<hostname>{{ config.mail_domain }}</hostname>
|
||||||
<port>{{ config.smtps_port }}</port>
|
<port>465</port>
|
||||||
<socketType>SSL</socketType>
|
<socketType>SSL</socketType>
|
||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
</outgoingServer>
|
</outgoingServer>
|
||||||
<outgoingServer type="smtp">
|
<outgoingServer type="smtp">
|
||||||
<hostname>{{ config.mail_domain }}</hostname>
|
<hostname>{{ config.mail_domain }}</hostname>
|
||||||
<port>{{ config.smtp_port }}</port>
|
<port>587</port>
|
||||||
<socketType>STARTTLS</socketType>
|
<socketType>STARTTLS</socketType>
|
||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
|
|||||||
@@ -31,26 +31,6 @@ stream {
|
|||||||
~\bimap\b 127.0.0.1:993;
|
~\bimap\b 127.0.0.1:993;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
|
||||||
listen {{ config.smtp_port }};
|
|
||||||
proxy_pass 127.0.0.1:587;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen {{ config.imap_port }};
|
|
||||||
proxy_pass 127.0.0.1:143;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen {{ config.smtps_port }};
|
|
||||||
proxy_pass 127.0.0.1:465;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen {{ config.imaps_port }};
|
|
||||||
proxy_pass 127.0.0.1:993;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443;
|
listen 443;
|
||||||
{% if not disable_ipv6 %}
|
{% if not disable_ipv6 %}
|
||||||
@@ -148,7 +128,10 @@ http {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
|
|
||||||
location /relay/probe {
|
# Endpoints Iroh uses for net_report probes and that clients
|
||||||
|
# probe to tell whether this relay works. Both paths are served
|
||||||
|
# by iroh-relay 0.35 and by the 1.0 line.
|
||||||
|
location /ping {
|
||||||
proxy_pass http://127.0.0.1:3340;
|
proxy_pass http://127.0.0.1:3340;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
}
|
}
|
||||||
@@ -157,6 +140,20 @@ http {
|
|||||||
proxy_pass http://127.0.0.1:3340;
|
proxy_pass http://127.0.0.1:3340;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /imap {
|
||||||
|
proxy_pass http://127.0.0.1:8143;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /smtp {
|
||||||
|
proxy_pass http://127.0.0.1:8587;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Redirect www. to non-www
|
# Redirect www. to non-www
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ smtp inet n - y - - smtpd
|
|||||||
-o smtpd_tls_mandatory_protocols=>=TLSv1.2
|
-o smtpd_tls_mandatory_protocols=>=TLSv1.2
|
||||||
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port_incoming }}
|
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port_incoming }}
|
||||||
-o smtpd_relay_restrictions=reject_unauth_destination
|
-o smtpd_relay_restrictions=reject_unauth_destination
|
||||||
submission inet n - y - 5000 smtpd
|
submission inet n - y - {{ config.max_smtp_connections }} smtpd
|
||||||
-o syslog_name=postfix/submission
|
-o syslog_name=postfix/submission
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
-o smtpd_tls_mandatory_protocols=>=TLSv1.3
|
-o smtpd_tls_mandatory_protocols=>=TLSv1.3
|
||||||
@@ -32,9 +32,9 @@ submission inet n - y - 5000 smtpd
|
|||||||
-o smtpd_sender_restrictions=$mua_sender_restrictions
|
-o smtpd_sender_restrictions=$mua_sender_restrictions
|
||||||
-o smtpd_recipient_restrictions=
|
-o smtpd_recipient_restrictions=
|
||||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||||
-o smtpd_client_connection_count_limit=1000
|
-o smtpd_client_connection_count_limit={{ config.max_smtp_connections // 5 }}
|
||||||
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
|
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
|
||||||
smtps inet n - y - 5000 smtpd
|
smtps inet n - y - {{ config.max_smtp_connections }} smtpd
|
||||||
-o syslog_name=postfix/smtps
|
-o syslog_name=postfix/smtps
|
||||||
-o smtpd_tls_wrappermode=yes
|
-o smtpd_tls_wrappermode=yes
|
||||||
-o smtpd_tls_security_level=encrypt
|
-o smtpd_tls_security_level=encrypt
|
||||||
@@ -48,7 +48,7 @@ smtps inet n - y - 5000 smtpd
|
|||||||
-o smtpd_sender_restrictions=$mua_sender_restrictions
|
-o smtpd_sender_restrictions=$mua_sender_restrictions
|
||||||
-o smtpd_recipient_restrictions=
|
-o smtpd_recipient_restrictions=
|
||||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||||
-o smtpd_client_connection_count_limit=1000
|
-o smtpd_client_connection_count_limit={{ config.max_smtp_connections // 5 }}
|
||||||
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
|
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
|
||||||
#628 inet n - y - - qmqpd
|
#628 inet n - y - - qmqpd
|
||||||
pickup unix n - y 60 1 pickup
|
pickup unix n - y 60 1 pickup
|
||||||
@@ -76,7 +76,6 @@ lmtp unix - - y - - lmtp
|
|||||||
anvil unix - - y - 1 anvil
|
anvil unix - - y - 1 anvil
|
||||||
scache unix - - y - 1 scache
|
scache unix - - y - 1 scache
|
||||||
postlog unix-dgram n - n - 1 postlogd
|
postlog unix-dgram n - n - 1 postlogd
|
||||||
filter unix - n n - - lmtp
|
|
||||||
# Local SMTP server for reinjecting outgoing filtered mail.
|
# Local SMTP server for reinjecting outgoing filtered mail.
|
||||||
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
||||||
-o syslog_name=postfix/reinject
|
-o syslog_name=postfix/reinject
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from pyinfra.operations import apt, server
|
||||||
|
|
||||||
|
from cmdeploy.basedeploy import Deployer
|
||||||
|
|
||||||
|
|
||||||
|
class WebsockifyDeployer(Deployer):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
apt.packages(name="Install websockify", packages=["websockify"])
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.ensure_systemd_unit("websockify/websockify-imap.service")
|
||||||
|
self.ensure_systemd_unit("websockify/websockify-submission.service")
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
self.ensure_service("websockify-imap.service")
|
||||||
|
self.ensure_service("websockify-submission.service")
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=WebSocket proxy for IMAP
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/websockify 127.0.0.1:8143 localhost:143
|
||||||
|
DynamicUser=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=WebSocket proxy for SMTP
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/websockify 127.0.0.1:8587 localhost:587
|
||||||
|
DynamicUser=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -199,6 +199,93 @@ creating addresses, login with ssh to the deployment machine and run:
|
|||||||
Chatmail address creation will be denied while this file is present.
|
Chatmail address creation will be denied while this file is present.
|
||||||
|
|
||||||
|
|
||||||
|
.. _system-limits:
|
||||||
|
|
||||||
|
Configurable System Limits
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Limits for auto-rejecting address creation
|
||||||
|
..........................................
|
||||||
|
|
||||||
|
A relay refuses creation of new addresses
|
||||||
|
when the machine runs low on resources,
|
||||||
|
but existing addresses keep working.
|
||||||
|
|
||||||
|
Three ``chatmail.ini`` settings control this,
|
||||||
|
shown here with their defaults::
|
||||||
|
|
||||||
|
max_load_1m = 5
|
||||||
|
min_available_memory = 200M
|
||||||
|
min_free_disk_space = 1G
|
||||||
|
|
||||||
|
- ``max_load_1m`` is the maximum 1-minute load average,
|
||||||
|
as reported by ``uptime``;
|
||||||
|
it counts processes waiting for disk I/O as well as for CPU.
|
||||||
|
It is deliberately not scaled by the number of CPUs
|
||||||
|
because I/O rather than CPU is what typically limits a relay.
|
||||||
|
|
||||||
|
- ``min_available_memory`` is the minimum memory available without swapping.
|
||||||
|
|
||||||
|
- ``min_free_disk_space`` is the minimum free disk space
|
||||||
|
on the file system holding the mailboxes.
|
||||||
|
|
||||||
|
The defaults suit the small machine described in
|
||||||
|
`Minimal requirements and prerequisites`_.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you run a bigger machine,
|
||||||
|
raise ``max_load_1m`` after watching ``uptime`` under typical load.
|
||||||
|
|
||||||
|
Rejections are logged by the ``doveauth`` service,
|
||||||
|
so you can check whether a limit is set too tightly::
|
||||||
|
|
||||||
|
journalctl -u doveauth --grep 'registration rejected'
|
||||||
|
|
||||||
|
If ``mtail_address`` is set, rejections are also counted
|
||||||
|
in the ``rejected_registrations`` metric.
|
||||||
|
|
||||||
|
|
||||||
|
Overall IMAP and SMTP connection limits
|
||||||
|
.......................................
|
||||||
|
|
||||||
|
Two further settings bound how many connections
|
||||||
|
the relay accepts at all, again shown with their defaults::
|
||||||
|
|
||||||
|
max_imap_connections = 10000
|
||||||
|
max_smtp_connections = 1000
|
||||||
|
|
||||||
|
``max_imap_connections`` becomes the Dovecot imap process limit,
|
||||||
|
and ``max_smtp_connections`` the Postfix process limit
|
||||||
|
on each of the submission and smtps ports.
|
||||||
|
A single client IP may use up to a fifth of ``max_smtp_connections``.
|
||||||
|
Each connection costs memory,
|
||||||
|
so these limits defend the relay against running out of RAM.
|
||||||
|
|
||||||
|
Unless ``imap_compress`` is enabled,
|
||||||
|
an IMAP connection that is idle for ``imap_hibernate_timeout``
|
||||||
|
is handed over to the ``imap-hibernate`` process
|
||||||
|
and does not count towards ``max_imap_connections``,
|
||||||
|
which is why a relay can serve far more IMAP clients
|
||||||
|
than this setting suggests.
|
||||||
|
|
||||||
|
If you run a large relay with 10k or 100k's of addresses,
|
||||||
|
check current connection counts before upgrading
|
||||||
|
and set the limits accordingly.
|
||||||
|
|
||||||
|
To see how close a running relay is to these two limits,
|
||||||
|
copy ``scripts/check-connections.sh`` from the relay repository
|
||||||
|
onto the relay and run it there::
|
||||||
|
|
||||||
|
imap 5 ports 143,993 (max_imap_connections)
|
||||||
|
5 dovecot sessions, 0 of them in an active imap process
|
||||||
|
submission 0 ports 465,587 (max_smtp_connections per port)
|
||||||
|
incoming 0 port 25 (from other relays, no chatmail.ini limit)
|
||||||
|
|
||||||
|
It counts established sockets with ``ss``
|
||||||
|
and cross-checks the IMAP number against ``doveadm who``.
|
||||||
|
|
||||||
|
|
||||||
Running a relay with self-signed certificates
|
Running a relay with self-signed certificates
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
|||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Show current IMAP and SMTP connections on a chatmail relay,
|
||||||
|
# to compare against the max_imap_connections and max_smtp_connections
|
||||||
|
# settings in chatmail.ini. Run this on the relay itself.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Count established TCP connections whose local port is one of the arguments.
|
||||||
|
established() {
|
||||||
|
filter=$(printf 'sport = :%s or ' "$@")
|
||||||
|
ss -Htn state established "( ${filter% or } )" | wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
# doveadm prints a header line and then one line per logged-in user,
|
||||||
|
# with that user's number of connections in the second column.
|
||||||
|
sessions=$(doveadm who | awk 'NR > 1 { n += $2 } END { print n + 0 }')
|
||||||
|
|
||||||
|
# Unless imap_compress is enabled, connections idle for
|
||||||
|
# imap_hibernate_timeout are handed over to the imap-hibernate
|
||||||
|
# process, so they cost no imap process while idle.
|
||||||
|
active=$(pgrep -x imap | wc -l)
|
||||||
|
|
||||||
|
printf 'imap %6d ports 143,993 (max_imap_connections)\n' "$(established 143 993)"
|
||||||
|
printf ' %6d dovecot sessions, %d of them in an active imap process\n' "$sessions" "$active"
|
||||||
|
printf 'submission %6d ports 465,587 (max_smtp_connections per port)\n' "$(established 465 587)"
|
||||||
|
printf 'incoming %6d port 25 (from other relays, no chatmail.ini limit)\n' "$(established 25)"
|
||||||
Reference in New Issue
Block a user