Compare commits

..

1 Commits

Author SHA1 Message Date
missytake
ca70537453 added full path to tox 2023-10-24 21:53:23 +02:00
14 changed files with 55 additions and 185 deletions

2
.gitignore vendored
View File

@@ -159,5 +159,3 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
chatmail.zone

View File

@@ -1,61 +1,23 @@
# Chatmail instances optimized for Delta Chat apps # Chat Mail server configuration
This repository helps to setup a ready-to-use chatmail instance This repository setups a ready-to-go chatmail instance
comprised of a minimal setup of the battle-tested comprised of a minimal setup of the battle-tested
[postfix smtp](https://www.postfix.org) and [dovecot imap](https://www.dovecot.org) services. [postfix smtp server](https://www.postfix.org) and [dovecot imap server](https://www.dovecot.org).
The setup is designed and optimized for providing chatmail accounts ## Getting started
for use by [Delta Chat apps](https://delta.chat).
Chatmail accounts are automatically created by a first login, 1. prepare your local system:
after which the initially specified password is required for using them.
## Getting Started deploying your own chatmail instance
1. Prepare your local (presumably Linux) system:
scripts/init.sh scripts/init.sh
2. Setup a domain with `A` and `AAAA` records for your chatmail server. 2. set environment variable to the chatmail domain you want to setup:
3. Set environment variable to the chatmail domain you want to setup:
export CHATMAIL_DOMAIN=c1.testrun.org # replace with your host export CHATMAIL_DOMAIN=c1.testrun.org # replace with your host
4. Deploy the chat mail instance to your chatmail server: 3. run the deploy of the chat mail instance:
scripts/deploy.sh scripts/deploy.sh
This script uses `pyinfra` and `ssh` to setup packages and configure
the chatmail instance on your remote server.
5. Run `scripts/generate-dns-zone.sh` and
transfer the generated DNS records at your DNS provider
6. Start a Delta Chat app and create a new account
by typing an e-mail address with an arbitrary username
and `@<your-chatmail-domain>` appended.
Use an at least 10-character random password.
### Ports
Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
Dovecot listens on ports 143(imap) and 993 (imaps).
Delta Chat will, however, discover all ports and configurations
automatically by reading the `autoconfig.xml` file from the chatmail instance.
## Emergency Commands to disable automatic account creation
If you need to stop account creation,
e.g. because some script is wildly creating accounts, run:
touch /etc/chatmail-nocreate
While this file is present, account creation will be blocked.
## Running tests and benchmarks (offline and online) ## Running tests and benchmarks (offline and online)
@@ -70,26 +32,28 @@ While this file is present, account creation will be blocked.
scripts/bench.sh scripts/bench.sh
## Running tests (offline and online)
## Development Background for chatmail instances ```
## Dovecot/Postfix configuration
This repository drives the development of "chatmail instances", ### Ports
comprised of minimal setups of
- [postfix smtp server](https://www.postfix.org) Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
- [dovecot imap server](https://www.dovecot.org) Dovecot listens on ports 143(imap) and 993 (imaps).
as well as two custom services that are integrated with these two: ## DNS
- `chatmaild/src/chatmaild/dictproxy.py` implements For DKIM you must add a DNS entry as found in /etc/opendkim/selector.txt on your chatmail instance.
create-on-login account creation semantics and is used The above `scripts/deploy.sh` prints out the DKIM selector and DNS entry you
by Dovecot during login authentication and by Postfix need to setup with your DNS provider.
which in turn uses Dovecot SASL to authenticate users
to send mails for them.
- `chatmaild/src/chatmaild/filtermail.py` prevents
unencrypted e-mail from leaving the chatmail instance
and is integrated into postfix's outbound mail pipelines.
## Emergency Commands
If you need to stop account creation,
e.g. because some script is wildly creating accounts,
just run `touch /tmp/nocreate`.
You can remove the file
as soon as the attacker was banned
by different means.

View File

@@ -21,27 +21,15 @@ def encrypt_password(password: str):
return "{SHA512-CRYPT}" + passhash return "{SHA512-CRYPT}" + passhash
def check_password(password) -> bool: def create_user(db, user, password):
"""Check password policy"""
if len(password) < 10:
return False
return True
def create_user(db, user, encrypted_password):
if os.path.exists(NOCREATE_FILE): if os.path.exists(NOCREATE_FILE):
logging.warning( logging.warning(
f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation." f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation."
) )
return return
with db.write_transaction() as conn: with db.write_transaction() as conn:
conn.create_user(user, encrypted_password) conn.create_user(user, password)
return dict( return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
home=f"/home/vmail/{user}",
uid="vmail",
gid="vmail",
password=encrypted_password,
)
def get_user_data(db, user): def get_user_data(db, user):
@@ -60,9 +48,6 @@ def lookup_userdb(db, user):
def lookup_passdb(db, user, password): def lookup_passdb(db, user, password):
userdata = get_user_data(db, user) userdata = get_user_data(db, user)
if not userdata: if not userdata:
if not check_password(password):
logging.warning("Attempt to create an account with a weak password.")
return
return create_user(db, user, encrypt_password(password)) return create_user(db, user, encrypt_password(password))
userdata["password"] = userdata["password"].strip() userdata["password"] = userdata["password"].strip()
return userdata return userdata

View File

@@ -4,8 +4,8 @@ Chat Mail pyinfra deploy.
import importlib.resources import importlib.resources
from pathlib import Path from pathlib import Path
from pyinfra import host from pyinfra import host, logger
from pyinfra.operations import apt, files, server, systemd from pyinfra.operations import apt, files, server, systemd, python
from pyinfra.facts.files import File from pyinfra.facts.files import File
from .acmetool import deploy_acmetool from .acmetool import deploy_acmetool
@@ -70,36 +70,6 @@ def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
mode="644", mode="644",
config={"domain_name": domain, "opendkim_selector": dkim_selector}, config={"domain_name": domain, "opendkim_selector": dkim_selector},
) )
need_restart |= main_config.changed
files.directory(
name="Add opendkim directory to /etc",
path="/etc/opendkim",
user="opendkim",
group="opendkim",
mode="750",
present=True,
)
keytable = files.template(
src=importlib.resources.files(__package__).joinpath("opendkim/KeyTable"),
dest="/etc/dkimkeys/KeyTable",
user="opendkim",
group="opendkim",
mode="644",
config={"domain_name": domain, "opendkim_selector": dkim_selector},
)
need_restart |= keytable.changed
signing_table = files.template(
src=importlib.resources.files(__package__).joinpath("opendkim/SigningTable"),
dest="/etc/dkimkeys/SigningTable",
user="opendkim",
group="opendkim",
mode="644",
config={"domain_name": domain, "opendkim_selector": dkim_selector},
)
need_restart |= signing_table.changed
files.directory( files.directory(
name="Add opendkim socket directory to /var/spool/postfix", name="Add opendkim socket directory to /var/spool/postfix",
@@ -120,6 +90,8 @@ def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
_sudo_user="opendkim", _sudo_user="opendkim",
) )
need_restart |= main_config.changed
return need_restart return need_restart
@@ -183,17 +155,6 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
mode="644", mode="644",
) )
# as per https://doc.dovecot.org/configuration_manual/os/
# it is recommended to set the following inotify limits
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
server.sysctl(
name=f"Change {key}",
key=key,
value=65535,
persist=True,
)
return need_restart return need_restart
@@ -331,3 +292,14 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
enabled=True, enabled=True,
restarted=journald_conf, restarted=journald_conf,
) )
def callback():
result = server.shell(
commands=[
f"""sed 's/\tIN/ 600 IN/;s/\t(//;s/\"$//;s/^\t \"//g; s/ ).*//' """
f"""/etc/dkimkeys/{dkim_selector}.txt | tr --delete '\n'"""
]
)
logger.info(f"Add this TXT entry into DNS zone: {result.stdout}")
python.call(name="Print TXT entry for DKIM", function=callback)

View File

@@ -6,7 +6,7 @@ from deploy_chatmail import deploy_chatmail
def main(): def main():
mail_domain = os.getenv("CHATMAIL_DOMAIN") mail_domain = os.getenv("CHATMAIL_DOMAIN")
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain) mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "dkim") dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023")
assert mail_domain assert mail_domain
assert mail_server assert mail_server

View File

@@ -118,24 +118,6 @@ service auth-worker {
user = vmail user = vmail
} }
service imap-login {
# High-security mode.
# Each process serves a single connection and exits afterwards.
# This is the default, but we set it explicitly to be sure.
# See <https://doc.dovecot.org/admin_manual/login_processes/#high-security-mode> for details.
service_count = 1
# Inrease the number of simultaneous connections.
#
# As of Dovecot 2.3.19.1 the default is 100 processes.
# Combined with `service_count = 1` it means only 100 connections
# can be handled simultaneously.
process_limit = 10000
# Avoid startup latency for new connections.
process_min_avail = 10
}
ssl = required ssl = required
ssl_cert = </var/lib/acme/live/{{ config.hostname }}/fullchain ssl_cert = </var/lib/acme/live/{{ config.hostname }}/fullchain
ssl_key = </var/lib/acme/live/{{ config.hostname }}/privkey ssl_key = </var/lib/acme/live/{{ config.hostname }}/privkey

View File

@@ -1 +0,0 @@
dkim._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/dkim.private

View File

@@ -1 +0,0 @@
*@{{ config.domain_name }} {{ config.opendkim_selector }}._domainkey.{{ config.domain_name }}

View File

@@ -1,4 +1,7 @@
# OpenDKIM configuration. # This is a basic configuration for signing and verifying. It can easily be
# adapted to suit a basic installation. See opendkim.conf(5) and
# /usr/share/doc/opendkim/examples/opendkim.conf.sample for complete
# documentation of available configuration parameters.
Syslog yes Syslog yes
SyslogSuccess yes SyslogSuccess yes
@@ -18,9 +21,7 @@ OversignHeaders From
# setup options can be found in /usr/share/doc/opendkim/README.opendkim. # setup options can be found in /usr/share/doc/opendkim/README.opendkim.
Domain {{ config.domain_name }} Domain {{ config.domain_name }}
Selector {{ config.opendkim_selector }} Selector {{ config.opendkim_selector }}
KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private
KeyTable /etc/dkimkeys/KeyTable
SigningTable /etc/dkimkeys/SigningTable
# In Debian, opendkim runs as user "opendkim". A umask of 007 is required when # In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
# using a local socket with MTAs that access the socket as a non-privileged # using a local socket with MTAs that access the socket as a non-privileged

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
set -e set -e
venv/bin/pytest online-tests/benchmark.py -vrx online-tests/venv/bin/pytest online-tests/benchmark.py -vrx

View File

@@ -1,15 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
export CHATMAIL_DOMAIN
echo ----------------------------------------- chatmaild/venv/bin/python3 -m build -n --sdist chatmaild --outdir dist
echo deploying to $CHATMAIL_DOMAIN
echo -----------------------------------------
echo WARNING: in five seconds deploy to $CHATMAIL_DOMAIN starts deploy-chatmail/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" \
sleep 5
venv/bin/python3 -m build -n --sdist chatmaild --outdir dist
venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" \
deploy-chatmail/src/deploy_chatmail/deploy.py deploy-chatmail/src/deploy_chatmail/deploy.py
rm -r dist/ rm -r dist/

View File

@@ -1,20 +0,0 @@
#!/bin/sh
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
: ${CHATMAIL_SSH:=$CHATMAIL_DOMAIN}
set -e
SSH="ssh root@$CHATMAIL_SSH"
EMAIL="root@$CHATMAIL_DOMAIN"
ACME_ACCOUNT_URL="$($SSH -- acmetool account-url)"
cat <<EOF
$CHATMAIL_DOMAIN. MX 10 $CHATMAIL_DOMAIN.
$CHATMAIL_DOMAIN. TXT "v=spf1 a:$CHATMAIL_DOMAIN -all"
_dmarc.$CHATMAIL_DOMAIN. TXT "v=DMARC1;p=reject;rua=mailto:$EMAIL;ruf=mailto:$EMAIL;fo=1;adkim=r;aspf=r"
_submission._tcp.$CHATMAIL_DOMAIN. SRV 0 1 587 $CHATMAIL_DOMAIN.
_submissions._tcp.$CHATMAIL_DOMAIN. SRV 0 1 465 $CHATMAIL_DOMAIN.
_imap._tcp.$CHATMAIL_DOMAIN. SRV 0 1 143 $CHATMAIL_DOMAIN.
_imaps._tcp.$CHATMAIL_DOMAIN. SRV 0 1 993 $CHATMAIL_DOMAIN.
$CHATMAIL_DOMAIN. IN CAA 0 issue "letsencrypt.org; accounturi=$ACME_ACCOUNT_URL"
EOF
$SSH opendkim-genzone -F | sed 's/^;.*$//;/^$/d'

View File

@@ -18,7 +18,7 @@ def test_basic(db):
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate" chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE): if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE):
os.remove(chatmaild.dictproxy.NOCREATE_FILE) os.remove(chatmaild.dictproxy.NOCREATE_FILE)
lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u") lookup_passdb(db, "link2xt@c1.testrun.org", "asdf")
data = get_user_data(db, "link2xt@c1.testrun.org") data = get_user_data(db, "link2xt@c1.testrun.org")
assert data assert data
@@ -37,7 +37,7 @@ def test_nocreate_file(db):
with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f: with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f:
f.write("") f.write("")
assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE) assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE)
lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik") lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
assert not get_user_data(db, "newuser1@something.org") assert not get_user_data(db, "newuser1@something.org")
os.remove(chatmaild.dictproxy.NOCREATE_FILE) os.remove(chatmaild.dictproxy.NOCREATE_FILE)

View File

@@ -23,11 +23,6 @@ def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
with pytest.raises(imap_or_smtp.AuthError): with pytest.raises(imap_or_smtp.AuthError):
imap_or_smtp.login(user, password + "wrong") imap_or_smtp.login(user, password + "wrong")
lp.sec(f"creating users with a short password is not allowed")
user, _password = gencreds()
with pytest.raises(imap_or_smtp.AuthError):
imap_or_smtp.login(user, "admin")
def test_login_same_password(imap_or_smtp, gencreds): def test_login_same_password(imap_or_smtp, gencreds):
"""Test two different users logging in with the same password """Test two different users logging in with the same password