Compare commits

..

6 Commits

Author SHA1 Message Date
missytake
3a32817de8 support CHATMAIL_SERVER in generate-dns-zone.sh
Revert "generate-dns-zone.sh doesn't need to support CHATMAIL_SERVER env var for now, let's assume A/AAAA point to the chatmail server, too"

This reverts commit 51ebd74e700eb65594c7b42dd2179141504cf666.
2023-11-25 00:59:30 +01:00
missytake
c6dd4f9b21 generate-dns-zone.sh doesn't need to support CHATMAIL_SERVER env var for now, let's assume A/AAAA point to the chatmail server, too 2023-11-25 00:59:30 +01:00
missytake
a420e37612 MTA-STS: the HTTPS route needs to be mta-sts.@ not _mta-sts 2023-11-25 00:59:07 +01:00
missytake
5429f3e379 fix: hetzner doesn't accept whitespace in TXT and CAA records apparently 2023-11-25 00:58:42 +01:00
missytake
d2c98e9afc DNS: distinguish between mail_server and mail_domain 2023-11-25 00:56:28 +01:00
missytake
658d6923ae Added MTA-STS records and .well-known file 2023-11-25 00:54:39 +01:00
10 changed files with 80 additions and 139 deletions

View File

@@ -6,14 +6,11 @@ build-backend = "setuptools.build_meta"
name = "chatmaild"
version = "0.1"
dependencies = [
"aiosmtpd",
"flask",
"gunicorn",
"aiosmtpd"
]
[project.scripts]
doveauth = "chatmaild.doveauth:main"
doveauth-http = "chatmaild.web:main"
filtermail = "chatmaild.filtermail:main"
[tool.pytest.ini_options]

View File

@@ -1,10 +0,0 @@
[Unit]
Description=HTTP endpoint for creating chatmail accounts
[Service]
ExecStart=/usr/local/bin/gunicorn --timeout 60 -b :3691 -w 1 chatmaild.web:main
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -3,6 +3,7 @@ import os
import time
import sys
import json
import crypt
from socketserver import (
UnixStreamServer,
StreamRequestHandler,
@@ -11,7 +12,39 @@ from socketserver import (
import pwd
from .database import Database
from .util import is_allowed_to_create, encrypt_password, get_mail_domain
NOCREATE_FILE = "/etc/chatmail-nocreate"
def encrypt_password(password: str):
# https://doc.dovecot.org/configuration_manual/authentication/password_schemes/
passhash = crypt.crypt(password, crypt.METHOD_SHA512)
return "{SHA512-CRYPT}" + passhash
def is_allowed_to_create(user, cleartext_password) -> bool:
"""Return True if user and password are admissable."""
if os.path.exists(NOCREATE_FILE):
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
return False
if len(cleartext_password) < 10:
logging.warning("Password needs to be at least 10 characters long")
return False
parts = user.split("@")
if len(parts) != 2:
logging.warning(f"user {user!r} is not a proper e-mail address")
return False
localpart, domain = parts
if domain == "nine.testrun.org":
# nine.testrun.org policy, username has to be exactly nine chars
if len(localpart) != 9:
logging.warning(f"localpart {localpart!r} has not exactly nine chars")
return False
return True
def get_user_data(db, user):
@@ -90,7 +123,8 @@ def main():
socket = sys.argv[1]
passwd_entry = pwd.getpwnam(sys.argv[2])
db = Database(sys.argv[3])
mail_domain = get_mail_domain()
with open("/etc/mailname", "r") as fp:
mail_domain = fp.read().strip()
class Handler(StreamRequestHandler):
def handle(self):

View File

@@ -1,56 +0,0 @@
import base64
import random
import logging
import os
import crypt
NOCREATE_FILE = "/etc/chatmail-nocreate"
def is_allowed_to_create(user, cleartext_password) -> bool:
"""Return True if user and password are admissable."""
if os.path.exists(NOCREATE_FILE):
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
return False
if len(cleartext_password) < 10:
logging.warning("Password needs to be at least 10 characters long")
return False
parts = user.split("@")
if len(parts) != 2:
logging.warning(f"user {user!r} is not a proper e-mail address")
return False
localpart, domain = parts
if domain == "nine.testrun.org":
# nine.testrun.org policy, username has to be exactly nine chars
if len(localpart) != 9:
logging.warning(f"localpart {localpart!r} has not exactly nine chars")
return False
return True
def gen_password():
with open("/dev/urandom", "rb") as f:
s = f.read(21)
return base64.b64encode(s).decode("ascii")[:12]
def encrypt_password(password: str):
# https://doc.dovecot.org/configuration_manual/authentication/password_schemes/
passhash = crypt.crypt(password, crypt.METHOD_SHA512)
return "{SHA512-CRYPT}" + passhash
def get_mail_domain():
with open("/etc/mailname", "r") as fp:
return fp.read().strip()
def get_valid_email_addr(length=9, chars="2345789acdefghjkmnpqrstuvwxyz"):
localpart = "".join(random.choice(chars) for i in range(length))
mail_domain = get_mail_domain()
return f"{localpart}@{mail_domain}"

View File

@@ -1,48 +0,0 @@
from flask import Flask, jsonify, request
import time
import os
from database import Database
from util import gen_password, get_valid_email_addr, encrypt_password
from doveauth import get_user_data
def create_app_from_db_path(db_path=None):
db = Database(db_path)
return create_app_from_db(db)
def create_app_from_db(db):
app = Flask("chatmaild-http")
app.db = db
@app.route("/", methods=["POST"])
def new_email():
for i in range(10):
addr = get_valid_email_addr()
if not get_user_data(db, addr):
cleartext_password = gen_password()
encrypted_password = encrypt_password(cleartext_password)
q = """INSERT INTO users (addr, password, last_login)
VALUES (?, ?, ?)"""
with db.write_transaction() as conn:
conn.execute(q, (addr, encrypted_password, int(time.time())))
return jsonify(
email=addr,
password=cleartext_password,
)
return jsonify(
type="error",
status_code=409,
reason="all 10 email addresses we tried are taken"
)
return app
def main():
"""(debugging-only!) serve http account creation Web API on localhost"""
db_path = os.getenv("CHATMAIL_DATABASE", "/home/vmail/passdb.sqlite")
app = create_app_from_db_path(db_path)
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=3691)

View File

@@ -46,7 +46,6 @@ def _install_chatmaild() -> None:
for fn in (
"doveauth",
"doveauth-http",
"filtermail",
):
files.put(
@@ -246,7 +245,7 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
return need_restart
def _configure_nginx(domain: str, debug: bool = False) -> bool:
def _configure_nginx(domain: str, mail_server: str) -> bool:
"""Configures nginx HTTP server."""
need_restart = False
@@ -276,7 +275,7 @@ def _configure_nginx(domain: str, debug: bool = False) -> bool:
user="root",
group="root",
mode="644",
config={"domain_name": domain},
config={"mail_server": mail_server},
)
need_restart |= mta_sts_config.changed
@@ -334,7 +333,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
dovecot_need_restart = _configure_dovecot(mail_server, debug=debug)
postfix_need_restart = _configure_postfix(mail_domain, debug=debug)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
nginx_need_restart = _configure_nginx(mail_domain)
nginx_need_restart = _configure_nginx(mail_domain, mail_server)
mta_sts_need_restart = _install_mta_sts_daemon()
# deploy web pages and info if we have them

View File

@@ -1,4 +1,4 @@
version: STSv1
mode: enforce
mx: {{ config.domain_name }}
mx: {{ config.mail_server }}
max_age: 2419200

View File

@@ -20,8 +20,6 @@ http {
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_certificate /var/lib/acme/live/{{ config.domain_name }}/fullchain;
ssl_certificate_key /var/lib/acme/live/{{ config.domain_name }}/privkey;
gzip on;
@@ -30,6 +28,8 @@ http {
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /var/lib/acme/live/{{ config.domain_name }}/fullchain;
ssl_certificate_key /var/lib/acme/live/{{ config.domain_name }}/privkey;
root /var/www/html;
@@ -42,10 +42,28 @@ http {
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
location /new_email {
proxy_pass http://localhost:3691/;
}
root /var/www/html;
index index.html index.htm;
server_name mta-sts.{{ config.domain_name }};
ssl_certificate /var/lib/acme/live/mta-sts.{{ config.domain_name }}/fullchain;
ssl_certificate_key /var/lib/acme/live/mta-sts.{{ config.domain_name }}/privkey;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
}

View File

@@ -20,7 +20,7 @@ Domain {{ config.domain_name }}
Selector {{ config.opendkim_selector }}
KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private
KeyTable /etc/dkimkeys/KeyTable
SigningTable refile:/etc/dkimkeys/SigningTable
SigningTable /etc/dkimkeys/SigningTable
# 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

View File

@@ -1,5 +1,6 @@
#!/bin/sh
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
: ${CHATMAIL_SERVER:=$CHATMAIL_DOMAIN}
: ${CHATMAIL_SSH:=$CHATMAIL_DOMAIN}
set -e
@@ -8,16 +9,22 @@ 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"
$CHATMAIL_DOMAIN. MX 10 $CHATMAIL_SERVER.
$CHATMAIL_DOMAIN. TXT "v=spf1 a:$CHATMAIL_SERVER -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.
_submission._tcp.$CHATMAIL_SERVER. SRV 0 1 587 $CHATMAIL_SERVER.
_submissions._tcp.$CHATMAIL_SERVER. SRV 0 1 465 $CHATMAIL_SERVER.
_imap._tcp.$CHATMAIL_SERVER. SRV 0 1 143 $CHATMAIL_SERVER.
_imaps._tcp.$CHATMAIL_SERVER. SRV 0 1 993 $CHATMAIL_SERVER.
$CHATMAIL_DOMAIN. IN CAA 128 issue "letsencrypt.org;accounturi=$ACME_ACCOUNT_URL"
_mta-sts.$CHATMAIL_DOMAIN. IN TXT "v=STSv1; id=$(date -u '+%Y%m%d%H%M')"
mta-sts.$CHATMAIL_DOMAIN. IN CNAME $CHATMAIL_DOMAIN.
_smtp._tls.$CHATMAIL_DOMAIN. IN TXT "v=TLSRPTv1;rua=mailto:$EMAIL"
mta-sts.$CHATMAIL_SERVER. IN CNAME $CHATMAIL_SERVER.
_smtp._tls.$CHATMAIL_SERVER. IN TXT "v=TLSRPTv1;rua=mailto:$EMAIL"
EOF
if [ "$CHATMAIL_DOMAIN" != "$CHATMAIL_SERVER" ]; then
cat <<EOF
mta-sts.$CHATMAIL_DOMAIN. IN CNAME mta-sts.$CHATMAIL_SERVER.
_smtp._tls.$CHATMAIL_DOMAIN. IN CNAME _smtp._tls.$CHATMAIL_SERVER.
EOF
fi
$SSH opendkim-genzone -F | sed 's/^;.*$//;/^$/d'