2025-02-25 15:32:36 +01:00
/*
2025-02-25 16:09:44 +01:00
* Forwarding mails : Update the Sops-secrets in the machine directory , rebuild on the VM and deploy .
* Everything else should happen automatically but new redirects might take up to two hours due HRZ infrastructure .
2025-02-25 15:32:36 +01:00
* 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 .
* /
{
config ,
lib ,
2025-02-25 16:09:44 +01:00
pkgs ,
2025-02-25 15:32:36 +01:00
. . .
} : 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 " m a t h e b a u m a i l s e r v i c e " ;
stalwartAdminHash = mkOption {
type = str ;
description = " S t r i n g c o n t a i n i n g t h e h a s h e d f a l l b a c k a d m i n p a s s w o r d " ;
} ;
2025-02-25 16:09:44 +01:00
domains = mkOption {
type = listOf ( lib . types . submodule {
options = {
domain = mkOption {
description = " D o m a i n n a m e t h a t w e s e r v e . W e a l s o p u s h i t s a d d r e s s e s t o H R Z . " ;
type = strMatching " ^ ( [ a - z 0 - 9 ] + ( - [ a - z 0 - 9 ] + ) * \. ) + [ a - z ] { 2 , } $ " ; #Regex from https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s15.html
} ;
allowlistPass = mkOption {
description = " P a s s w o r d f i l e f o r t h e H R Z A P I t h a t g e t s a l i s t o f m a i l a d d r e s s e s t h a t w e s e r v e " ;
type = path ;
} ;
virt_aliases = mkOption {
description = " F i l e p a t h t o a v i r t u a l a l i a s f i l e a p p l i c a b l e f o r t h i s d o m a i n " ;
type = path ;
default = " / d e v / n u l l " ; # there might not be an alias file and reading an empty one works with our implementation
} ;
} ;
} ) ;
} ;
2025-02-25 15:32:36 +01:00
} ;
config = mkIf cfg . enable {
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 " = {
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 " = {
# 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 = [ " [ : : ] : 8 0 " ] ;
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 = {
# we only talk to HRZ and our own VMs anyway
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 ; # Don't assume TLS on this port but use STARTTLS
} ;
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 because this field does not accept ip addresses.
port = 25 ;
protocol = " s m t p " ;
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 = [
{
" 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 ' | | s t a r t s _ w i t h ( r e m o t e _ i p , ' 1 9 2 . 1 6 8 . 0 . ' ) " ; #TODO restrict trust by IP
" t h e n " = true ;
}
{ " e l s e " = false ; }
] ;
} ;
2025-02-25 16:09:44 +01:00
# Stalwart gets its configuration from two places: A TOML configuration file that we control in this module
# and from a database that can be configured from web management interface or via Rest API.
# We here define what comes from the TOML-file and especially add "sieve.trusted.scripts.*" to the default ones
# because only TOML-based keys may use macros to load files from disk.
# We want this to be able to load our sieve-script for mail forwarding.
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 macros to be able to include our redirection script
sieve . trusted . scripts . redirects . contents = " % { f i l e : / t m p / v i r t _ a l i a s e s } % " ; # generated redirect script
session . data . script = " ' r e d i r e c t s ' " ;
2025-02-25 15:32:36 +01:00
authentication . fallback-admin = {
user = " a d m i n " ;
# 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 = " s q l i t e " ;
db . path = " / v a r / l i b / s t a l w a r t - m a i l / d a t a / i n d e x . s q l i t e 3 " ;
fs . type = " f s " ;
fs . path = " / v a r / l i b / s t a l w a r t - m a i l / d a t a / b l o b s " ;
} ;
} ;
} ;
} ;
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
} ;
2025-02-25 16:09:44 +01:00
systemd = {
services = {
" s t a l w a r t - m a i l " = {
restartTriggers = lib . attrsets . mapAttrsToList ( _ : aliaslist : aliaslist . sopsFile ) config . sops . secrets ; # restart if secrets, especially alias files, have changed.
serviceConfig . PrivateTmp = lib . mkForce false ; # enable access to generated Sieve script
} ;
" v i r t - a l i a s e s - g e n e r a t o r " = {
description = " V i r t u a l A l i a s e s G e n e r a t o r : G e n e r a t e a s i e v e s c r i p t f r o m t h e v i r t u a l a l i a s f i l e " ;
script = lib . strings . concatStringsSep " " ( [ " ${ pkgs . alias-to-sieve } / b i n / a l i a s _ t o _ s i e v e " ] ++ map ( x : " ${ x . virt_aliases } ${ x . domain } " ) cfg . domains ++ [ " > / t m p / v i r t _ a l i a s e s " ] ) ;
wantedBy = [ " s t a l w a r t - m a i l . s e r v i c e " ] ; # Rerun on stalwart restart because forwardings may have changed.
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 = false ;
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 ;
} ;
} ;
} ;
} ;
2025-02-25 15:32:36 +01: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 " ;
} ;
} ;
}