Compare commits

..

31 Commits

Author SHA1 Message Date
missytake
03a367a8b2 DKIM: only use random DKIM selector on staging.testrun.org for now 2024-01-24 13:21:14 +01:00
missytake
c4af0e49ad rspamd: fix DKIM signing 2024-01-24 13:21:14 +01:00
missytake
d760bc617e CI: activate for generate-dkim-selector branch 2024-01-24 13:21:14 +01:00
missytake
82c317c1d5 DKIM: generate dkim_selector with cmdeploy init 2024-01-24 13:21:14 +01:00
link2xt
a6b333672d Revert "Pin deltachat-rpc-server version"
This reverts commit 3940b9256d.

1.133.2 release has OpenSSL 3.2 downgraded to 3.1 and pass the tests.
2024-01-24 03:53:23 +00:00
link2xt
29857143c9 Dovecot: setup METADATA
There is no dictionary to set additional attributes,
but admin email can already be retrieved:

? GETMETADATA "" (/shared/admin)
* METADATA "" (/shared/admin {27}
mailto:root@c20.testrun.org)
? OK Getmetadata completed (0.001 + 0.000 secs).
2024-01-24 01:55:13 +00:00
missytake
d1460e7a1a tests: other bots could be in passthrough_recipients 2024-01-24 02:36:27 +01:00
missytake
87ab7e83d5 config: add xstore and groupsbot to default passthrough_recipients 2024-01-24 02:36:27 +01:00
link2xt
9f31357a9c Remove postscreen-related entries from Postfix master.cf
All these entries are related to `postscreen` service
which is currently not enabled.

For documentation see https://www.postfix.org/POSTSCREEN_README.html

If we later want to enable it, we can readd uncommented entries
and document it.
2024-01-24 02:08:30 +01:00
link2xt
c94ef0379a Update pip and setuptools in scripts/initenv.sh
This is to support Debian 11 which ships setuptools
that do not support `-e` without setup.py
2024-01-24 01:31:48 +01:00
link2xt
bc66325d71 Cleanup Received headers after filtermail as well 2024-01-23 21:27:23 +00:00
link2xt
27f44ae911 Cleanup Received headers only on outgoing mail 2024-01-23 20:28:34 +00:00
link2xt
3940b9256d Pin deltachat-rpc-server version 2024-01-22 14:44:39 +00:00
link2xt
4886ff9b86 Do not use redirect on /cgi-bin/newemail.py
Delta Chat does not follow redirects,
so it breaks old QR codes printed on paper
and published on various web pages.
2024-01-21 13:20:00 +00:00
missytake
38a9fc3d6e CI: fix GH action description 2024-01-19 20:36:49 +01:00
missytake
e676545f7a CI: DEFAULT_DNS_ZONE doesn't need to be secret 2024-01-19 20:36:49 +01:00
missytake
ef95627138 CI: don't reset staging.testrun.org VPS on every CI run 2024-01-19 20:36:49 +01:00
missytake
bfaedb5cf1 CI: save /var/lib/rspamd/dkim from getting wiped 2024-01-19 20:36:49 +01:00
missytake
ea8d53aa9b CI: test DNS entries after online tests, less flaky 2024-01-19 20:36:49 +01:00
missytake
be7a000de6 CI: try cmdeploy dns 3 times as it is a bit flaky 2024-01-19 20:36:49 +01:00
missytake
ad3cf9ecaa CI: enable tests with 2 chatmail servers, with nine.testrun.org for now 2024-01-19 20:36:49 +01:00
missytake
691324a3e8 DNS: revert hardcoded DNS server for reverse DNS checks 2024-01-19 20:36:49 +01:00
missytake
23a9f893b4 CI: save /var/lib/acme from getting wiped 2024-01-19 20:36:49 +01:00
missytake
3ea826aecb CI: don't deploy to nine.testrun.org automatically 2024-01-19 20:36:49 +01:00
missytake
532d094a08 CI: check whether cmdeploy dns --zonefile works 2024-01-19 20:36:49 +01:00
missytake
0cea5840df CI: don't reset staging.testrun.org after each run 2024-01-19 20:36:49 +01:00
missytake
45686778ea unbound: ensure systemd service can be started after root keys were generated 2024-01-19 20:36:49 +01:00
missytake
45108d9c93 CI: deploy on staging.testrun.org and if it works, on nine.testrun.org 2024-01-19 20:36:49 +01:00
missytake
3665d957a7 tests: fix tests for new fastCGI route and DKIM responses 2024-01-17 11:23:04 +01:00
link2xt
86940b2ee1 Stop requesting DMARC reports
Nobody reads these XML reports
and we know our DKIM is valid
when `cmdeploy dns` is happy.
2024-01-17 01:37:54 +00:00
link2xt
24fb9eb65b Nicer /new URL for new accounts and redirect GET requests
If user types in https://nine.testrun.org/new manually
in the browser, at least Firefox and Brave suggest
to open the app after following the redirect.
2024-01-15 13:06:29 +00:00
18 changed files with 165 additions and 28 deletions

View File

@@ -0,0 +1,20 @@
;; Zone file for staging.testrun.org
$ORIGIN staging.testrun.org.
$TTL 300
@ IN SOA ns.testrun.org. root.nine.testrun.org (
2023010101 ; Serial
7200 ; Refresh
3600 ; Retry
1209600 ; Expire
3600 ; Negative response caching TTL
)
;; Nameservers.
@ IN NS ns.testrun.org.
;; DNS records.
@ IN A 37.27.37.98
mta-sts.staging.testrun.org. CNAME staging.testrun.org.
www.staging.testrun.org. CNAME staging.testrun.org.

73
.github/workflows/test-and-deploy.yaml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: deploy on staging.testrun.org, and run tests
on:
push:
branches:
- main
- staging-ci
- generate-dkim-selector
jobs:
deploy:
name: deploy on staging.testrun.org, and run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: prepare SSH
run: |
mkdir ~/.ssh
echo "${{ secrets.STAGING_SSH_KEY }}" >> ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan staging.testrun.org > ~/.ssh/known_hosts
# rsync -avz root@staging.testrun.org:/var/lib/acme . || true
# rsync -avz root@staging.testrun.org:/var/lib/rspamd/dkim . || true
#- name: rebuild staging.testrun.org to have a clean VPS
# run: |
# curl -X POST \
# -H "Authorization: Bearer ${{ secrets.HETZNER_API_TOKEN }}" \
# -H "Content-Type: application/json" \
# -d '{"image":"debian-12"}' \
# "https://api.hetzner.cloud/v1/servers/${{ secrets.STAGING_SERVER_ID }}/actions/rebuild"
- run: scripts/initenv.sh
- name: append venv/bin to PATH
run: echo venv/bin >>$GITHUB_PATH
- name: run formatting checks
run: cmdeploy fmt -v
- name: run deploy-chatmail offline tests
run: pytest --pyargs cmdeploy
#- name: upload TLS cert after rebuilding
# run: |
# echo " --- wait until staging.testrun.org VPS is rebuilt --- "
# rm ~/.ssh/known_hosts
# while ! ssh -o ConnectTimeout=180 -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org id -u ; do sleep 1 ; done
# ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org id -u
# rsync -avz acme root@staging.testrun.org:/var/lib/ || true
# rsync -avz dkim root@staging.testrun.org:/var/lib/rspamd/ || true
- run: cmdeploy init staging.testrun.org
- run: cmdeploy run
- name: set DNS entries
run: |
#ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org chown _rspamd:_rspamd -R /var/lib/rspamd/dkim
cmdeploy dns --zonefile staging-generated.zone
cat staging-generated.zone >> .github/workflows/staging.testrun.org-default.zone
cat .github/workflows/staging.testrun.org-default.zone
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging.testrun.org.zone
ssh root@ns.testrun.org nsd-checkzone staging.testrun.org /etc/nsd/staging.testrun.org.zone
ssh root@ns.testrun.org systemctl reload nsd
- name: cmdeploy test
run: CHATMAIL_DOMAIN2=nine.testrun.org cmdeploy test --slow
- name: cmdeploy dns (try 3 times)
run: cmdeploy dns || cmdeploy dns || cmdeploy dns

View File

@@ -1,4 +1,5 @@
import iniconfig
from datetime import datetime
def read_config(inipath):
@@ -24,6 +25,7 @@ class Config:
self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo")
self.privacy_supervisor = params.get("privacy_supervisor")
self.dkim_selector = params.get("dkim_selector")
def _getbytefile(self):
return open(self._inipath, "rb")
@@ -33,8 +35,16 @@ def write_initial_config(inipath, mail_domain):
from importlib.resources import files
inidir = files(__package__).joinpath("ini")
selector = "dkim"
if mail_domain == "staging.testrun.org":
selector = datetime.now().strftime("%Y%m%d%H%M")
content = (
inidir.joinpath("chatmail.ini.f").read_text().format(mail_domain=mail_domain)
inidir.joinpath("chatmail.ini.f")
.read_text()
.format(
mail_domain=mail_domain,
dkim_selector=selector,
)
)
if mail_domain.endswith(".testrun.org"):
override_inipath = inidir.joinpath("override-testrun.ini")

View File

@@ -33,7 +33,9 @@ password_min_length = 9
passthrough_senders =
# list of e-mail recipients for which to accept outbound un-encrypted mails
passthrough_recipients =
passthrough_recipients = xstore@testrun.org groupsbot@hispanilandia.net
dkim_selector = {dkim_selector}
#
# Deployment Details

View File

@@ -1,7 +1,7 @@
[privacy]
passthrough_recipients = privacy@testrun.org
passthrough_recipients = privacy@testrun.org xstore@testrun.org groupsbot@hispanilandia.net
privacy_postal =
Merlinux GmbH, Represented by the managing director H. Krekel,

View File

@@ -28,5 +28,5 @@ def test_read_config_testrun(make_config):
assert config.username_min_length == 9
assert config.username_max_length == 9
assert config.password_min_length == 9
assert config.passthrough_recipients == ["privacy@testrun.org"]
assert "privacy@testrun.org" in config.passthrough_recipients
assert config.passthrough_senders == []

View File

@@ -442,7 +442,10 @@ def deploy_chatmail(config_path: Path) -> None:
)
server.shell(
name="Generate root keys for validating DNSSEC",
commands=["unbound-anchor -a /var/lib/unbound/root.key || true"],
commands=[
"unbound-anchor -a /var/lib/unbound/root.key || true",
"systemctl reset-failed unbound.service",
],
)
systemd.service(
name="Start and enable unbound",
@@ -492,7 +495,7 @@ def deploy_chatmail(config_path: Path) -> None:
nginx_need_restart = _configure_nginx(mail_domain)
remove_opendkim()
rspamd_need_restart = _configure_rspamd("dkim", mail_domain)
rspamd_need_restart = _configure_rspamd(config.dkim_selector, mail_domain)
systemd.service(
name="Start and enable rspamd",

View File

@@ -7,7 +7,7 @@ _imap._tcp.{chatmail_domain}. SRV 0 1 143 {chatmail_domain}.
_imaps._tcp.{chatmail_domain}. SRV 0 1 993 {chatmail_domain}.
{chatmail_domain}. CAA 128 issue "letsencrypt.org;accounturi={acme_account_url}"
{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=s;aspf=s"
_dmarc.{chatmail_domain}. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
_mta-sts.{chatmail_domain}. TXT "v=STSv1; id={sts_id}"
mta-sts.{chatmail_domain}. CNAME {chatmail_domain}.
www.{chatmail_domain}. CNAME {chatmail_domain}.

View File

@@ -82,7 +82,8 @@ def dns_cmd_options(parser):
def dns_cmd(args, out):
"""Generate dns zone file."""
show_dns(args, out)
exit_code = show_dns(args, out)
exit(exit_code)
def status_cmd(args, out):

View File

@@ -47,9 +47,11 @@ class DNS:
return result == f"{mail_domain}."
def show_dns(args, out):
def show_dns(args, out) -> int:
"""Check existing DNS records, optionally write them to zone file, return exit code 0 or 1."""
template = importlib.resources.files(__package__).joinpath("chatmail.zone.f")
mail_domain = args.config.mail_domain
selector = args.config.dkim_selector
ssh = f"ssh root@{mail_domain}"
dns = DNS(out, mail_domain)
@@ -60,8 +62,8 @@ def show_dns(args, out):
continue
line = line.replace("\t", " ")
lines.append(line)
lines[0] = f"dkim._domainkey.{mail_domain}. IN TXT " + lines[0].strip(
"dkim._domainkey IN TXT "
lines[0] = f"{selector}._domainkey.{mail_domain}. IN TXT " + lines[0].strip(
f"{selector}._domainkey IN TXT "
)
return "\n".join(lines)
@@ -70,9 +72,11 @@ def show_dns(args, out):
acme_account_url = out.shell_output(f"{ssh} -- acmetool account-url")
except subprocess.CalledProcessError:
print("Please run `cmdeploy run` first.")
return
return 1
dkim_entry = read_dkim_entries(
out.shell_output(f"{ssh} -- cat /var/lib/rspamd/dkim/{mail_domain}.dkim.zone")
out.shell_output(
f"{ssh} -- cat /var/lib/rspamd/dkim/{mail_domain}.{selector}.zone"
)
)
ipv6 = dns.get_ipv6()
@@ -99,7 +103,7 @@ def show_dns(args, out):
with open(args.zonefile, "w+") as zf:
zf.write(zonefile)
print(f"DNS records successfully written to: {args.zonefile}")
return
return 0
except TypeError:
pass
started_dkim_parsing = False
@@ -153,6 +157,7 @@ def show_dns(args, out):
else:
to_print.append(dkim_entry)
exit_code = 0
if to_print:
to_print.insert(
0, "You should configure the following DNS entries at your provider:\n"
@@ -161,6 +166,7 @@ def show_dns(args, out):
"\nIf you already configured the DNS entries, wait a bit until the DNS entries propagate to the Internet."
)
print("\n".join(to_print))
exit_code = 1
else:
out.green("Great! All your DNS entries are correct.")
@@ -180,6 +186,8 @@ def show_dns(args, out):
print(
"You can do so at your hosting provider (maybe this isn't your DNS provider)."
)
exit_code = 1
return exit_code
def check_necessary_dns(out, mail_domain):

View File

@@ -13,13 +13,15 @@ auth_cache_size = 100M
mail_debug = yes
{% endif %}
mail_server_admin = mailto:root@{{ config.mail_domain }}
mail_server_comment = Chatmail server
mail_plugins = quota
# these are the capabilities Delta Chat cares about actually
# so let's keep the network overhead per login small
# https://github.com/deltachat/deltachat-core-rust/blob/master/src/imap/capabilities.rs
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY METADATA
# Authentication for system users.
@@ -73,6 +75,7 @@ mail_privileged_group = vmail
# <https://datatracker.ietf.org/doc/html/rfc4978.html>
protocol imap {
mail_plugins = $mail_plugins imap_zlib imap_quota
imap_metadata = yes
}
protocol lmtp {

View File

@@ -58,8 +58,19 @@ http {
}
# Old URL for compatibility with e.g. printed QR codes.
#
# Copy-paste instead of redirect to /new
# because Delta Chat core does not follow redirects.
#
# Redirects are only for browsers.
location /cgi-bin/newemail.py {
return 301 /new;
if ($request_method = GET) {
return 301 dcaccount:https://{{ config.domain_name }}/new;
}
fastcgi_pass unix:/run/fcgiwrap.socket;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py;
}
}

View File

@@ -48,5 +48,3 @@ virtual_mailbox_domains = {{ config.mail_domain }}
smtpd_milters = inet:127.0.0.1:11332
non_smtpd_milters = $smtpd_milters
header_checks = regexp:/etc/postfix/submission_header_cleanup

View File

@@ -14,10 +14,6 @@ smtp inet n - y - - smtpd -v
{% else %}
smtp inet n - y - - smtpd
{% endif %}
#smtp inet n - y - 1 postscreen
#smtpd pass - - y - - smtpd
#dnsblog unix - - y - 0 dnsblog
#tlsproxy unix - - y - 0 tlsproxy
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
@@ -34,6 +30,7 @@ submission inet n - y - - smtpd
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_client_connection_count_limit=1000
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
-o cleanup_service_name=authclean
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
@@ -50,6 +47,7 @@ smtps inet n - y - - smtpd
-o smtpd_client_connection_count_limit=1000
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
-o cleanup_service_name=authclean
#628 inet n - y - - qmqpd
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
@@ -80,3 +78,13 @@ filter unix - n n - - lmtp
# Local SMTP server for reinjecting filered mail.
localhost:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
-o syslog_name=postfix/reinject
-o cleanup_service_name=authclean
# Cleanup `Received` headers for authenticated mail
# to avoid leaking client IP.
#
# We do not do this for received mails
# as this will break DKIM signatures
# if `Received` header is signed.
authclean unix n - - - 0 cleanup
-o header_checks=regexp:/etc/postfix/submission_header_cleanup

View File

@@ -1,9 +1,9 @@
selector = {{ config.dkim_selector }}
selector = "{{ config.dkim_selector }}"
use_esld = false # don't cut c1.testrun.org down to testrun.org
domain = {
{{ config.mail_domain }} {
selectors [
selector = {{ config.dkim_selector }}
selector = "{{ config.dkim_selector }}"
path = {{ config.dkim_key_path }}
]
}

View File

@@ -9,7 +9,7 @@ def test_gen_qr_png_data(maildomain):
def test_fastcgi_working(maildomain, chatmail_config):
url = f"https://{maildomain}/cgi-bin/newemail.py"
url = f"https://{maildomain}/new"
print(url)
res = requests.post(url)
assert maildomain in res.json().get("email")
@@ -18,7 +18,7 @@ def test_fastcgi_working(maildomain, chatmail_config):
def test_newemail_configure(maildomain, rpc):
"""Test configuring accounts by scanning a QR code works."""
url = f"DCACCOUNT:https://{maildomain}/cgi-bin/newemail.py"
url = f"DCACCOUNT:https://{maildomain}/new"
for i in range(3):
account_id = rpc.add_account()
rpc.set_config_from_qr(account_id, url)

View File

@@ -48,7 +48,7 @@ def test_reject_missing_dkim(cmsetup, maildata, from_addr):
recipient = cmsetup.gen_users(1)[0]
msg = maildata("plain.eml", from_addr=from_addr, to_addr=recipient.addr).as_string()
with smtplib.SMTP(cmsetup.maildomain, 25) as s:
with pytest.raises(smtplib.SMTPDataError, match="Spam message rejected"):
with pytest.raises(smtplib.SMTPDataError, match="missing DKIM signature"):
s.sendmail(from_addr=from_addr, to_addrs=recipient.addr, msg=msg)

View File

@@ -1,6 +1,6 @@
#!/bin/bash
set -e
python3 -m venv venv
python3 -m venv --upgrade-deps venv
venv/bin/pip install -e chatmaild
venv/bin/pip install -e cmdeploy