2024-07-10 20:56:46 +00:00
{
config ,
lib ,
pkgs ,
2024-11-10 15:08:58 +00:00
flake-inputs ,
2024-07-10 20:56:46 +00:00
. . .
} : let
inherit
( lib )
mkIf
mkEnableOption
mkOption
;
inherit ( lib . types ) listOf str ;
cfg = config . services . mathebau-mail ;
in {
options . services . mathebau-mail = {
enable = mkEnableOption " m a t h e b a u m a i l s e r v i c e " ;
domains = mkOption {
type = listOf ( lib . types . submodule {
options = {
domain = mkOption {
type = str ;
} ;
allowlistPass = mkOption {
type = str ;
} ;
} ;
} ) ;
} ;
} ;
config = mkIf cfg . enable {
2024-11-10 15:08:58 +00:00
environment . systemPackages = [ flake-inputs . alias-to-sieve . packages . x86_64-linux . default ] ;
2024-07-10 20:56:46 +00:00
services = {
stalwart-mail = {
enable = true ;
openFirewall = true ;
settings = {
server = {
lookup . default . hostname = " f b 0 4 1 8 4 . m a t h e m a t i k . t u - d a r m s t a d t . d e " ; # Because the DNS PTR of 130.83.2.184 is this and this should be used in SMTP EHLO.
listener = {
" s m t p " = {
bind = [ " [ : : ] : 2 5 " ] ;
protocol = " s m t p " ;
} ;
" s u b m i s s i o n s " = {
# Enabling sending from these domains privately blocked on https://github.com/stalwartlabs/mail-server/issues/618
bind = [ " [ : : ] : 4 6 5 " ] ;
protocol = " s m t p " ;
tls . implicit = true ;
} ;
" i m a p t l s " = {
bind = [ " [ : : ] : 9 9 3 " ] ;
protocol = " i m a p " ;
tls . implicit = true ;
} ;
" m a n a g e m e n t " = {
bind = [ " [ : : ] : 8 0 " ] ; # This must also bind publically for ACME to work.
protocol = " h t t p " ;
} ;
} ;
} ;
acme . letsencrypt = {
directory = " h t t p s : / / a c m e - v 0 2 . a p i . l e t s e n c r y p t . o r g / d i r e c t o r y " ; # This setting is necessary for this block to be activated
challenge = " h t t p - 0 1 " ;
contact = [ " r o o t @ m a t h e b a u . d e " ] ;
domains = [ " f b 0 4 1 8 4 . m a t h e m a t i k . t u - d a r m s t a d t . d e " " i m a p . m a t h e b a u . d e " " s m t p . m a t h e b a u . d e " ] ;
default = true ;
} ;
spam . header . is-spam = " D u m m y h e a d e r " ; # 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 = " r e l a x e d " ;
arc . verify = " r e l a x e d " ;
dmarc . verify = " r e l a x e d " ;
iprev . verify = " r e l a x e d " ;
spf . verify . ehlo = " r e l a x e d " ;
spf . verify . mail-from = " r e l a x e d " ;
} ;
# Forward outgoing mail to HRZ or mail VMs.
# see https://stalw.art/docs/smtp/outbound/routing/ relay host example
queue . outbound = {
next-hop = [
{
" i f " = " r c p t _ d o m a i n = ' l i s t s . m a t h e b a u . d e ' " ;
" t h e n " = " ' m a i l m a n ' " ;
}
{
" i f " = " i s _ l o c a l _ d o m a i n ( ' ' , r c p t _ d o m a i n ) " ;
" t h e n " = " ' l o c a l ' " ;
}
{ " e l s e " = " ' h r z ' " ; }
] ;
tls = {
mta-sts = " d i s a b l e " ;
dane = " d i s a b l e " ;
starttls = " o p t i o n a l " ; # e.g. Lobon does not offer starttls
} ;
} ;
remote . " h r z " = {
address = " m a i l o u t . h r z . t u - d a r m s t a d t . d e " ;
port = 25 ;
protocol = " s m t p " ;
tls . implicit = false ; # somehow this is needed here
} ;
remote . " m a i l m a n " = {
address = " l o b o n . m a t h e b a u . d e " ; # must be created in DNS as a MX record
port = 25 ;
protocol = " s m t p " ;
tls . implicit = false ; # somehow this is needed here
} ;
# In order to accept mail that we only forward
# without having to generate an account.
# Invalid addresses are filtered by DFN beforehand.
session . rcpt = {
catch-all = true ;
relay = [
{
" i f " = " ! i s _ e m p t y ( a u t h e n t i c a t e d _ a s ) | | r c p t _ d o m a i n = = ' l i s t s . m a t h e b a u . d e ' " ;
" t h e n " = true ;
}
{ " e l s e " = false ; }
] ;
} ;
config . local-keys =
[
" s t o r e . * "
" d i r e c t o r y . * "
" t r a c e r . * "
" s e r v e r . * "
" ! s e r v e r . b l o c k e d - i p . * "
" a u t h e n t i c a t i o n . f a l l b a c k - a d m i n . * "
" c l u s t e r . n o d e - i d "
" s t o r a g e . d a t a "
" s t o r a g e . b l o b "
" s t o r a g e . l o o k u p "
" s t o r a g e . f t s "
" s t o r a g e . d i r e c t o r y "
" l o o k u p . d e f a u l t . h o s t n a m e "
" c e r t i f i c a t e . * "
] # the default ones
++ [ " s i e v e . t r u s t e d . s c r i p t s . * " ] ; #for marcos to be able to include our redirection script
sieve . trusted . scripts . redirects . contents = " % { f i l e : / r u n / s e c r e t s / m a i l F o r w a r d S i e v e } % " ;
session . data . script = " ' r e d i r e c t s ' " ;
authentication . fallback-admin = {
user = " a d m i n " ;
secret = " $ a r g o n 2 i $ v = 1 9 $ m = 4 0 9 6 , t = 3 , p = 1 $ d 0 h Y O T k z c l p z S m F T Z U p l W n h V e W E $ I 7 q 9 u B 1 9 R W L 0 o Z K a P l M P S l G f F p 6 F Q / v r x 8 0 F F K C s a l g " ;
} ;
} ;
} ;
} ;
environment . persistence . ${ config . impermanence . name } = {
directories = [
" / v a r / l i b / s t a l w a r t - m a i l "
] ;
files = [ " / r o o t / . s s h / k n o w n _ h o s t s " ] ; # for the backup server bragi
} ;
# Update HRZ allowlist
# For account details see https://www-cgi.hrz.tu-darmstadt.de/mail/
# will stop working if no valid TUIDs are associated to our domain.
systemd . timers . " m a i l A l l o w l i s t " = {
wantedBy = [ " t i m e r s . t a r g e t " ] ;
timerConfig = {
OnBootSec = " 5 m " ; # Run every 5 minutes
OnUnitActiveSec = " 5 m " ;
RandomizedDelaySec = " 2 m " ; # prevent overload on regular intervals
Unit = " m a i l A l l o w l i s t . s e r v i c e " ;
} ;
} ;
# ${pkgs.gnugrep}/bin/grep -o -e "[A-Za-z0-9.!#\$%&'*+-/=?^_`{|}~]+@koma89.tu-darmstadt.de" /run/secrets/mailForwardSieve >> /tmp/addresses # This doesn't catch all RFC conform local parts. Improve if you need.
systemd . services . " m a i l A l l o w l i s t " = {
description = " A l l o w l i s t u p d a t e : P o s t t h e m a i l a d d r e s s e s t o t h e H R Z a l l o w l l i s t " ;
script = let
scriptTemplate = {
domain ,
allowlistPass ,
} : ''
# Get the mail addresses' local-part
$ { pkgs . stalwart-mail } /bin/stalwart-cli - - url http://localhost:80 - c $ ( cat /run/secrets/stalwartAdmin ) account list | grep ' @ $ { domain } ' | sed ' s / | // ' | sed ' s / | // ' | tee /tmp/addresses
$ { pkgs . stalwart-mail } /bin/stalwart-cli - - url http://localhost:80 - c $ ( cat /run/secrets/stalwartAdmin ) list list | grep ' @ $ { domain } ' | sed ' s / | // ' | sed ' s / | // ' | tee - a /tmp/addresses
$ { pkgs . stalwart-mail } /bin/stalwart-cli - - url http://localhost:80 - c $ ( cat /run/secrets/stalwartAdmin ) group list | grep ' @ $ { domain } ' | sed ' s / | // ' | sed ' s / | // ' | tee - a /tmp/addresses
$ { pkgs . gnugrep } /bin/grep - o - e " [ A - Z a - z 0 - 9 . ! # \$ % & ' * + - / = ? ^ _ { | } ~ ] * @ ${ domain } " /run/secrets/mailForwardSieve | tee - a /tmp/addresses # This doesn't catch all RFC conform local parts. Improve if you need.
# Post local-parts to HRZ
$ { pkgs . curl } /bin/curl https://www-cgi.hrz.tu-darmstadt.de/mail/whitelist-update.php - F emaildomain = $ { domain } - F password = $ ( cat $ { allowlistPass } ) - F emailliste = @ /tmp/addresses - F meldungen = voll
# Cleanup
rm /tmp/addresses
'' ;
in
lib . strings . concatStringsSep " " ( map scriptTemplate cfg . domains ) ;
serviceConfig = {
Type = " o n e s h o t " ;
User = " s t a l w a r t - m a i l " ;
NoNewPrivileges = true ;
# See https://www.man7.org/linux/man-pages/man5/systemd.exec.5.html
PrivateTmp = true ;
ProtectHome = true ;
ReadOnlyPaths = " / " ;
ReadWritePaths = " / t m p " ;
InaccessiblePaths = " - / l o s t + f o u n d " ;
PrivateDevices = true ;
PrivateUsers = true ;
ProtectHostname = true ;
ProtectClock = true ;
ProtectKernelTunables = true ;
ProtectKernelModules = true ;
ProtectKernelLogs = true ;
ProtectControlGroups = true ;
LockPersonality = true ;
MemoryDenyWriteExecute = true ;
RestrictRealtime = true ;
RestrictSUIDSGID = true ;
} ;
} ;
2024-11-10 15:03:02 +00:00
2024-07-10 20:56:46 +00:00
# Backups
services . borgbackup . jobs . mail = {
paths = [
" / v a r / l i b / s t a l w a r t - m a i l / d a t a "
] ;
encryption . mode = " n o n e " ; # Otherwise the key is next to the backup or we have human interaction.
environment = {
BORG_RSH = " s s h - i / r u n / s e c r e t s / b a c k u p K e y " ;
# “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 = " y e s " ;
} ;
repo = " b o r g @ 1 9 2 . 1 6 8 . 1 . 1 1 : k a l u u t " ; # TODO for https://gitea.mathebau.de/Fachschaft/nixConfig/issues/33
startAt = " d a i l y " ;
user = " r o o t " ;
group = " r o o t " ;
} ;
} ;
}