Mail machine #47

Merged
Gonne merged 9 commits from Gonne/nixConfig:nyarlathotep into main 2025-02-27 15:59:49 +00:00
2 changed files with 178 additions and 0 deletions
Showing only changes of commit d7b8f935cd - Show all commits

View file

@ -1,12 +1,19 @@
{config, ...}: { {config, ...}: {
imports = [ imports = [
./hardware-configuration.nix ./hardware-configuration.nix
../../modules/mail.nix
../../roles ../../roles
../../roles/vm.nix ../../roles/vm.nix
../../modules/vmNetwork.nix ../../modules/vmNetwork.nix
]; ];
# System configuration here # System configuration here
services.mathebau-mail = {
enable = true;
# see passwd on azathoth for plaintext or machine secret in encoded format for HTTP Basic AUTH
stalwartAdminHash = "$argon2i$v=19$m=4096,t=3,p=1$d0hYOTkzclpzSmFTZUplWnhVeWE$I7q9uB19RWL0oZKaPlMPSlGfFp6FQ/vrx80FFKCsalg";
};
networking.hostName = "kaalut"; networking.hostName = "kaalut";
vmNetwork.ipv4 = "192.168.0.17"; vmNetwork.ipv4 = "192.168.0.17";
system.stateVersion = "24.05"; system.stateVersion = "24.05";

171
nixos/modules/mail.nix Normal file
View file

@ -0,0 +1,171 @@
/*
* Using the web admin interface: Set your SSH to do portforwarding of some local port to port 80 of the VM and
* and use your personal admin account or create one using the fallback admin password.
* Create users with mail boxes: Go to the admin interface and create them.
* Stalwart mailserver docs can be found at https://stalw.art/docs
* DNS-Records: Collect the right DNS entries from the management interface and copy them to the DNS hoster. Caution:
* Not all entries are applicable since we relay via HRZ.
*/
{

This should either move somewhere closer to main documentation or should have a reference at least a reference.
This seems important and I probably wouldn't look here first to find this information.

This should either move somewhere closer to main documentation or should have a reference at least a reference. This seems important and I probably wouldn't look here first to find this information.

I think its intuitive here and will wait for specific suggestions.

I think its intuitive here and will wait for specific suggestions.

I think we should open an issue, to start a doc folder. I also want to write a document for people to get started. I kind of think the README has outgrown this purpose.

I think we should open an issue, to start a doc folder. I also want to write a document for people to get started. I kind of think the README has outgrown this purpose.
config,
lib,
...
}: let
inherit
(lib)
mkIf
mkEnableOption
mkOption
;
inherit (lib.types) listOf strMatching str path;
cfg = config.services.mathebau-mail;
in {
options.services.mathebau-mail = {
enable = mkEnableOption "mathebau mail service";
stalwartAdminHash = mkOption {
type = str;
description = "String containing the hashed fallback admin password";
};
};
config = mkIf cfg.enable {
services = {
stalwart-mail = {
enable = true;
openFirewall = true;
settings = {
server = {
lookup.default.hostname = "fb04184.mathematik.tu-darmstadt.de"; # Because the DNS PTR of 130.83.2.184 is this and this should be used in SMTP EHLO.
listener = {
"smtp" = {
bind = ["[::]:25"];
protocol = "smtp";
};
"submissions" = {
bind = ["[::]:465"];
protocol = "smtp";
tls.implicit = true;
};
"imaptls" = {
bind = ["[::]:993"];
protocol = "imap";
tls.implicit = true;
};
"management" = {
# Cthulhu forwards requests for http://fb04184.mathematik.tu-darmstadt.de/.well-known/acme-challenge/ http://imap.mathebau.de/.well-known/acme-challenge/ and http://smtp.mathebau.de/.well-known/acme-challenge/
# for TLS certificate challenge validation
# whereas the rest of the management interface is not available publically.
# It can be reached via SSH and portforwarding.
bind = ["[::]:80"];
protocol = "http";
};
};
};
acme.letsencrypt = {
directory = "https://acme-v02.api.letsencrypt.org/directory"; # This setting is necessary for this block to be activated
challenge = "http-01";
contact = ["root@mathebau.de"];
domains = ["fb04184.mathematik.tu-darmstadt.de" "imap.mathebau.de" "smtp.mathebau.de"];
default = true;
};
spam.header.is-spam = "Dummyheader"; # disable moving to spam which would conflict with forwarding
auth = {
# TODO check if HRZ conforms to these standards and we can validate them strictly
dkim.verify = "relaxed";
arc.verify = "relaxed";
dmarc.verify = "relaxed";
iprev.verify = "relaxed";
spf.verify.ehlo = "relaxed";
spf.verify.mail-from = "relaxed";
};
# Forward outgoing mail to HRZ or mail VMs.
# see https://stalw.art/docs/smtp/outbound/routing/ relay host example
queue.outbound = {
next-hop = [
{
"if" = "rcpt_domain = 'lists.mathebau.de'";
"then" = "'mailman'";
}
{
"if" = "is_local_domain('', rcpt_domain)";
"then" = "'local'";
}
{"else" = "'hrz'";}
];
tls = {
# we only talk to HRZ and our own VMs anyway
mta-sts = "disable";
dane = "disable";
starttls = "optional"; # e.g. Lobon does not offer starttls
};
};
remote."hrz" = {
https://www.hrz.tu-darmstadt.de/hrz_aktuelles/news_details_177024.en.jsp Maybe parts of this are now?

I would like to evaluate this in production based on logs.

I would like to evaluate this in production based on logs.

Fine by me, that should be an easy fix

Fine by me, that should be an easy fix
address = "mailout.hrz.tu-darmstadt.de";
port = 25;
protocol = "smtp";
tls.implicit = false; # Don't assume TLS on this port but use STARTTLS
};
remote."mailman" = {
address = "lobon.mathebau.de"; # must be created in DNS as a MX record because this field does not accept ip addresses.
port = 25;
protocol = "smtp";
tls.implicit = false; # Don't assume TLS on this port but use STARTTLS
};
session.rcpt = {
# In order to accept mail that we only forward
# without having to generate an account.
# Invalid addresses are filtered by DFN beforehand.
catch-all = true;
relay = [
{
"if" = "!is_empty(authenticated_as) || rcpt_domain == 'lists.mathebau.de' || starts_with(remote_ip, '192.168.0.')"; #TODO restrict trust by IP
"then" = true;
}
{"else" = false;}
];
};
Gonne marked this conversation as resolved

I'm sad about the state of DANE. I like it.

I'm sad about the state of DANE. I like it.
authentication.fallback-admin = {

Is there a domain specific option? then we could only disable this for mailman and else require it.

Is there a domain specific option? then we could only disable this for mailman and else require it.

The documentation mentions no such thing.

The documentation mentions no such thing.

sad, is this an issue for upstream?

sad, is this an issue for upstream?

Only if someone cares enough to write one. I don't.

Only if someone cares enough to write one. I don't.
user = "admin";
# see passwd on azathoth for plaintext or machine secret in encoded format for HTTP Basic AUTH
secret = cfg.stalwartAdminHash;
};
store = {
# structured data in SQLite, blobs on filesystem
db.type = "sqlite";
db.path = "/var/lib/stalwart-mail/data/index.sqlite3";
fs.type = "fs";
fs.path = "/var/lib/stalwart-mail/data/blobs";
};
};
};
};
environment.persistence.${config.impermanence.name} = {
directories = [
"/var/lib/stalwart-mail"
];
files = ["/root/.ssh/known_hosts"]; # for the backup server bragi
};
# Backups
services.borgbackup.jobs.mail = {

does this only work for local ipv4 connections? That might change at some point, right?

does this only work for local ipv4 connections? That might change at some point, right?

Yes, but we probably want to give credentials to the VMs and remove the trust by IP anyways.

Yes, but we probably want to give credentials to the VMs and remove the trust by IP anyways.

Fine by me, but we will need to document somewhere how this is done. Else people after us will be lost

Fine by me, but we will need to document somewhere how this is done. Else people after us will be lost
paths = [
"/var/lib/stalwart-mail/data"
];
encryption.mode = "none"; # Otherwise the key is next to the backup or we have human interaction.
environment = {
BORG_RSH = "ssh -i /run/secrets/backupKey";
# “Borg ensures that backups are not created on random drives that just happen to contain a Borg repository.”
# https://borgbackup.readthedocs.io/en/stable/deployment/automated-local.html
# We don't want this in order to not need to persist borg cache and simplify new deployments.
BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK = "yes";
};
repo = "borg@192.168.1.11:kaluut"; # TODO for https://gitea.mathebau.de/Fachschaft/nixConfig/issues/33
startAt = "daily";
user = "root";
group = "root";
};
};
}