Compare commits

..

13 Commits

Author SHA1 Message Date
holger krekel
6d3ffd8f4e add plan as discussed with alex and nami 2023-10-13 15:44:06 +00:00
holger krekel
a24f1e8393 create venv in chatmail-pyinfra 2023-10-13 15:44:06 +00:00
holger krekel
f84692a07a fix/rename 2023-10-13 15:44:06 +00:00
holger krekel
4badc7c8d6 (nami, hpk) draft repackaging goal 2023-10-13 15:44:06 +00:00
link2xt
4b82fd6f77 Add init.sh and deploy.sh scripts (#2) 2023-10-13 16:14:02 +02:00
link2xt
9da375cf5c README: add --ssh-user root 2023-10-13 14:06:30 +00:00
missytake
8086e2ee2f removed inventory, doesn't work anymore anyway 2023-10-13 14:28:08 +02:00
holger krekel
1b88e0d9d0 add a functional online test for login ok/failure 2023-10-13 14:23:14 +02:00
holger krekel
db6df34703 python tests work 2023-10-13 14:23:11 +02:00
holger krekel
a907da9907 wip 2023-10-13 14:22:08 +02:00
holger krekel
14649c37fd this seems to work 2023-10-13 14:22:08 +02:00
holger krekel
28fe373489 (nami, hpk) some random WIP-changes to make dovecot auth work and get us an understanding 2023-10-13 14:22:08 +02:00
missytake
1942ad3cef ensure environment variables are set 2023-10-13 11:17:55 +02:00
22 changed files with 166 additions and 54 deletions

View File

@@ -4,7 +4,58 @@ This package deploys Postfix and Dovecot servers, including OpenDKIM for DKIM si
Postfix uses Dovecot for authentication as described in <https://www.postfix.org/SASL_README.html#server_dovecot> Postfix uses Dovecot for authentication as described in <https://www.postfix.org/SASL_README.html#server_dovecot>
## Ports ## Getting started
prepare:
pip install -e chatmail-infra
then run with pyinfra command line tool:
CHATMAIL_DOMAIN=c1.testrun.org pyinfra --ssh-user root c1.testrun.org deploy.py
## Structure (wip)
```
# package doveauth tool and deploy chatmail server to a envvar-specified ssh-reachable host
deploy.py
# chatmail pyinfra deploy package
chatmail-pyinfra
pyproject.toml
chatmail/__init__ ...
# tests against the deployed system
tests/test_online_test.py
# doveauth tool used by dovecot's auth mechanism on the host system
doveauth
README.md
pyproject.toml
doveauth.py
doveauth.lua
test_doveauth.py
# lmtp server to block (outgoing) unencrypted messages
filtermail
README.md
pyproject.toml
....
# scripts for setup/development/deployment
scripts/
init.sh # create venv/other perequires
deploy.sh # run pyinfra based deploy of everything
```
## Dovecot/Postfix configuration
### Ports
Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions). Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
Dovecot listens on ports 143(imap) and 993 (imaps). Dovecot listens on ports 143(imap) and 993 (imaps).
@@ -12,9 +63,3 @@ Dovecot listens on ports 143(imap) and 993 (imaps).
## DNS ## DNS
For DKIM you must add a DNS entry as in /etc/opendkim/selector.txt (where selector is the opendkim_selector configured in the chatmail inventory). For DKIM you must add a DNS entry as in /etc/opendkim/selector.txt (where selector is the opendkim_selector configured in the chatmail inventory).
## Run with pyinfra
```
CHATMAIL_DOMAIN=c1.testrun.org pyinfra c1.testrun.org deploy.py
```

View File

@@ -1,11 +0,0 @@
chatmail = [
(
"c1.testrun.org",
{
"ssh_user": "root",
"domain": "c1.testrun.org",
"dkim_selector": "2023",
},
),
]

View File

@@ -8,7 +8,9 @@ end
-- call out to python program to actually manage authentication for dovecot -- call out to python program to actually manage authentication for dovecot
function chatctl_verify(user, password) function chatctl_verify(user, password)
local handle = io.popen("python doveauth.py hexauth "..escape(user).." "..escape(password)) local cmd = "python3 /home/vmail/chatctl hexauth "..escape(user).." "..escape(password)
print("executing: "..cmd)
local handle = io.popen(cmd)
local result = handle:read("*a") local result = handle:read("*a")
handle:close() handle:close()
return split_chatctl(result) return split_chatctl(result)
@@ -16,32 +18,27 @@ end
function chatctl_lookup(user) function chatctl_lookup(user)
assert(user) assert(user)
local handle = io.popen("python doveauth.py hexlookup "..escape(user)) local handle = io.popen("python3 /home/vmail/chatctl hexlookup "..escape(user))
local result = handle:read("*a") local result = handle:read("*a")
handle:close() handle:close()
return split_chatctl(result) return split_chatctl(result)
end end
function get_extra_dovecot_output(res) function get_extra_dovecot_output(res)
return {homedir=res.homedir, uid=res.uid, gid=res.gid} return {home=res.home, uid=res.uid, gid=res.gid}
end end
function auth_passdb_verify(request, password) function auth_password_verify(request, password)
local res = chatctl_verify(request.user, password) local res = chatctl_verify(request.user, password)
-- request:log_error("auth_password_verify "..request.user.." "..password)
if res.status == "ok" then if res.status == "ok" then
local extra = get_extra_dovecot_output(res)
return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res) return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res)
end end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "" return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
end end
function auth_passdb_lookup(request)
local res = chatctl_lookup(request.user)
if res.status == "ok" then
return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "no such user"
end
function auth_userdb_lookup(request) function auth_userdb_lookup(request)
local res = chatctl_lookup(request.user) local res = chatctl_lookup(request.user)

View File

@@ -4,21 +4,17 @@ import sys
def get_user_data(user): def get_user_data(user):
if user == b"link2xt@instant2.testrun.org": if user == "link2xt@c1.testrun.org":
return dict( return dict(
homedir="/home/vmail/link2xt",
uid="vmail", uid="vmail",
gid="vmail", gid="vmail",
password=b"Ahyei6ie", password="Ahyei6ie",
) )
return {} return {}
def create_user(user, password): def create_user(user, password):
assert isinstance(password, bytes) return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
return dict(
homedir=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password
)
def verify_user(user, password): def verify_user(user, password):
@@ -51,11 +47,11 @@ def dump_result(res):
if __name__ == "__main__": if __name__ == "__main__":
if sys.argv[1] == "hexauth": if sys.argv[1] == "hexauth":
login = base64.b16decode(sys.argv[2]) login = base64.b16decode(sys.argv[2]).decode()
password = base64.b16decode(sys.argv[3]) password = base64.b16decode(sys.argv[3]).decode()
res = verify_user(login, password) res = verify_user(login, password)
dump_result(res) dump_result(res)
elif sys.argv[1] == "hexlookup": elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2]) login = base64.b16decode(sys.argv[2]).decode()
res = lookup_user(login) res = lookup_user(login)
dump_result(res) dump_result(res)

View File

@@ -4,6 +4,11 @@ protocols = imap lmtp
auth_mechanisms = plain auth_mechanisms = plain
auth_verbose = yes
auth_debug = yes
auth_debug_passwords = yes
auth_verbose_passwords = plain
# Authentication for system users. # Authentication for system users.
passdb { passdb {
driver = lua driver = lua

View File

@@ -15,19 +15,19 @@ dovecot = {
-- Tests for testing the lua<->python interaction -- Tests for testing the lua<->python interaction
function test_passdb_verify_ok(user, password) function test_password_verify_ok(user, password)
local res, extra = auth_passdb_verify({user=user}, password) local res, extra = auth_password_verify({user=user}, password)
assert(res==dovecot.auth.PASSDB_RESULT_OK) assert(res==dovecot.auth.PASSDB_RESULT_OK)
assert(extra.uid == "vmail") assert(extra.uid == "vmail")
assert(extra.gid == "vmail") assert(extra.gid == "vmail")
-- assert(extra.homedir == "/home/vmail/link2xt") -- assert(extra.homedir == "/home/vmail/link2xt")
print("OK test_passdb_verify_ok "..user.." "..password) print("OK test_password_verify_ok "..user.." "..password)
end end
function test_passdb_verify_mismatch(user, password) function test_password_verify_mismatch(user, password)
local res = auth_passdb_verify({user=user}, password) local res = auth_password_verify({user=user}, password)
assert(res == dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH) assert(res == dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
print("OK test_passdb_verify_mismatch "..user.." "..password) print("OK test_password_verify_mismatch "..user.." "..password)
end end
function test_userdb_lookup_ok(user) function test_userdb_lookup_ok(user)
@@ -67,10 +67,12 @@ function test_split_chatctl()
end end
test_split_chatctl() test_split_chatctl()
test_passdb_verify_ok("link2xt@instant2.testrun.org", "Ahyei6ie") test_password_verify_ok("link2xt@c1.testrun.org", "Ahyei6ie")
test_passdb_verify_mismatch("link2xt@instant2.testrun.org", "Aqwlek") test_password_verify_mismatch("link2xt@c1.testrun.org", "Aqwlek")
test_userdb_lookup_ok("link2xt@instant2.testrun.org") test_userdb_lookup_ok("link2xt@c1.testrun.org")
test_userdb_lookup_mismatch("wlekqjlew@xyz.org") test_userdb_lookup_mismatch("wlekqjlew@xyz.org")
test_passdb_lookup_ok("link2xt@instant2.testrun.org")
test_passdb_lookup_mismatch("llqkwjelqwe@xyz.org") -- probably not needed by dovecot?
-- test_passdb_lookup_ok("link2xt@c1.testrun.org")
-- test_passdb_lookup_mismatch("llqkwjelqwe@xyz.org")

View File

@@ -5,15 +5,15 @@ from doveauth import get_user_data, verify_user
def test_basic(): def test_basic():
data = get_user_data(b"link2xt@instant2.testrun.org") data = get_user_data("link2xt@c1.testrun.org")
assert data assert data
@pytest.mark.xfail(reason="no persistence yet") @pytest.mark.xfail(reason="no persistence yet")
def test_verify_or_create(): def test_verify_or_create():
res = verify_user(b"newuser1@something.org", b"kajdlkajsldk12l3kj1983") res = verify_user("newuser1@something.org", "kajdlkajsldk12l3kj1983")
assert res["status"] == "ok" assert res["status"] == "ok"
res = verify_user(b"newuser1@something.org", b"kajdlqweqwe") res = verify_user("newuser1@something.org", "kajdlqweqwe")
assert res["status"] == "fail" assert res["status"] == "fail"

View File

@@ -0,0 +1,28 @@
import pytest
import imaplib
@pytest.fixture
def conn():
return connect("c1.testrun.org")
def login(conn, user, password):
print("trying to login", user, password)
conn.login(user, password)
def connect(host):
print(f"connecting to {host}")
conn = imaplib.IMAP4_SSL(host)
return conn
def test_login_ok(conn):
login(conn, "link2xt@c1.testrun.org", "Ahyei6ie")
def test_login_fail(conn):
with pytest.raises(imaplib.IMAP4.error) as excinfo:
login(conn, "link2xt@c1.testrun.org", "qweqwe")
assert "AUTHENTICATIONFAILED" in str(excinfo)

View File

@@ -8,7 +8,11 @@ def main():
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain) mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023") dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023")
assert mail_domain
assert mail_server
assert dkim_selector
deploy_chatmail(mail_domain, mail_server, dkim_selector) deploy_chatmail(mail_domain, mail_server, dkim_selector)
main() main()

39
plan.txt Normal file
View File

@@ -0,0 +1,39 @@
# Chat-mail server development (up until Oct 18th)
## Dovecot goals/steps
1. create-user-on-login ("doveauth")
- repackage so that "doveauth" does not come from a hard-coded path
- persistence of accounts
2. per-user quota (adaptive)
3. automatic expiry of messages older than M days
4. automatic expiry of users that haven't logged in for N days
## Postfix goals/steps
1. block all outgoing mails with our own LMTP program
2. only allow (outgoing) mails if secure-join or autocrypt-pgp-encrypted format
(probably via an lmtp service)
3. basic outgoing send rate/limits (depending on "account-rating")
## online tests (first with plain python/pytest)
- write tests for dovecot login (exists)
- write tests for postfix logins
- write A<>B send/receive tests
## Delta Chat
1. qr code that defines access to a chatmail instance (like mailadm but without http etc.)
2. support for creating username/password and verifying login works

3
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
export CHATMAIL_DOMAIN="${1:-c1.testrun.org}"
chatmail-pyinfra/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" deploy.py

4
scripts/init.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
python3 -m venv chatmail-pyinfra/venv
chatmail-pyinfra/venv/bin/pip install pyinfra
chatmail-pyinfra/venv/bin/pip install -e chatmail-pyinfra