diff --git a/modules/cloud/mail/default.nix b/modules/cloud/mail/default.nix new file mode 100644 index 0000000..aa20e28 --- /dev/null +++ b/modules/cloud/mail/default.nix @@ -0,0 +1,343 @@ +{ config, pkgs, lib, ... }: + +with lib; +let + cfg = config.cloud.mail; + + name = "maddy"; + + stateDir = "/var/lib/${name}"; + runtimeDir = "/run/${name}"; + + smtpPort = 58787; + smtpsPort = 46565; + imapPort = 9333; + mtaStsPort = 8003; +in +{ + options.cloud.mail = { + enable = mkEnableOption "Enable the email server"; + + package = mkOption { + type = types.package; + default = pkgs.maddy; + }; + + hostname = mkOption { + type = types.str; + default = "mx1.nkagami.me"; + description = "The hostname where the server is run on"; + }; + + primaryDomain = mkOption { + type = types.str; + default = "nkagami.me"; + description = "The primary email domain"; + }; + + additionalDomains = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Additional domain names to be used"; + }; + + tls = { + certFile = mkOption { + type = types.str; + description = "Path to the certificate file."; + }; + keyFile = mkOption { + type = types.str; + description = "Path to the key file."; + }; + }; + + usersFile = mkOption { + type = types.path; + description = '' + The file containing user:hashed-password pairs to be used in the mail server. + Check `maddyctl hash` to generate hashed passwords. + ''; + }; + }; + + config = + let + configFile = pkgs.writeText "maddy.conf" '' + # Globals + state_dir ${stateDir} + runtime_dir ${runtimeDir} + + # Base variables + + $(hostname) = ${cfg.hostname} + $(primary_domain) = ${cfg.primaryDomain} + $(local_domains) = ${cfg.primaryDomain} ${lib.strings.concatStringsSep " " cfg.additionalDomains} + + tls file "${cfg.tls.certFile}" "${cfg.tls.keyFile}" + + # Authentication + auth.pass_table local_authdb { + table file ${cfg.usersFile} + } + + # Local storage + storage.imapsql local_mailboxes { + driver "postgres" + dsn "dbname='${name}' user='${name}' host='/run/postgresql' sslmode=disable" + appendlimit 256M + } + + # ---------------------------------------------------------------------------- + # SMTP endpoints + message routing + + hostname $(hostname) + + msgpipeline local_routing { + # Insert handling for special-purpose local domains here. + # e.g. + # destination lists.example.org { + # deliver_to lmtp tcp://127.0.0.1:8024 + # } + + destination postmaster $(local_domains) { + modify { + replace_rcpt regexp "(.+)\+(.+)@(.+)" "$1@$3" + replace_rcpt file /etc/maddy/aliases + } + + deliver_to &local_mailboxes + } + + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } + + smtp tcp://0.0.0.0:25 { + limits { + # Up to 20 msgs/sec across max. 10 SMTP connections. + all rate 20 1s + all concurrency 10 + } + + dmarc yes + check { + require_mx_record + dkim + spf + } + + source $(local_domains) { + reject 501 5.1.8 "Use Submission for outgoing SMTP" + } + default_source { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } + } + + submission tcp://0.0.0.0:${toString smtpPort} tls://0.0.0.0:${toString smtpsPort} { + limits { + # Up to 50 msgs/sec across any amount of SMTP connections. + all rate 50 1s + } + + auth &local_authdb + + source $(local_domains) { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + modify { + dkim $(primary_domain) $(local_domains) default + } + deliver_to &remote_queue + } + } + default_source { + reject 501 5.1.8 "Non-local sender domain" + } + } + + target.remote outbound_delivery { + limits { + # Up to 20 msgs/sec across max. 10 SMTP connections + # for each recipient domain. + destination rate 20 1s + destination concurrency 10 + } + mx_auth { + dane + mtasts { + cache fs + fs_dir mtasts_cache/ + } + local_policy { + min_tls_level encrypted + min_mx_level none + } + } + } + + target.queue remote_queue { + target &outbound_delivery + + autogenerated_msg_domain $(primary_domain) + bounce { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.0.0 "Refusing to send DSNs to non-local addresses" + } + } + } + + # ---------------------------------------------------------------------------- + # IMAP endpoints + + imap tls://0.0.0.0:${toString imapPort} { + auth &local_authdb + storage &local_mailboxes + } + ''; + + mtaStsDir = pkgs.writeTextDir ".well-known/mta-sts.txt" '' + version: STSv1 + mode: enforce + max_age: 604800 + mx: ${cfg.hostname} + ''; + in + mkIf cfg.enable { + # users + users.users."${name}" = { + group = "${name}"; + isSystemUser = true; + }; + users.groups."${name}" = { }; + + # database + cloud.postgresql.databases = [ name ]; + + # MTA-STS server + services.nginx.enable = true; + services.nginx.virtualHosts.maddy-mta-sts = { + listen = [{ addr = "127.0.0.1"; port = mtaStsPort; }]; + root = mtaStsDir; + }; + + # Firewall + networking.firewall.allowedTCPPorts = [ 25 ]; + + # traefik + cloud.traefik.hosts.maddy-smtp = { + protocol = "tcp"; + port = smtpPort; + host = "mx1.nkagami.me"; + tlsPassthrough = false; + entrypoints = [ "smtp-submission" ]; + }; + cloud.traefik.hosts.maddy-smtps = { + protocol = "tcp"; + port = smtpsPort; + host = "mx1.nkagami.me"; + entrypoints = [ "smtp-submission-ssl" ]; + }; + cloud.traefik.hosts.maddy-imap = { + protocol = "tcp"; + port = imapPort; + host = "mx1.nkagami.me"; + entrypoints = [ "imap" ]; + }; + cloud.traefik.hosts.maddy-mta-sts = { + port = mtaStsPort; + host = "mta-sts.nkagami.me"; + }; + + # maddy itself + systemd.services."${name}" = { + after = [ "network.target" "traefik-certs-dumper.service" ]; + wantedBy = [ "multi-user.target" ]; + + description = "maddy mail server"; + documentation = [ + "man:maddy(1)" + "man:maddy.conf(5)" + "https://maddy.email" + ]; + + serviceConfig = { + Type = "notify"; + NotifyAccess = "main"; + + User = name; + Group = name; + + WorkingDirectory = "/var/lib/${name}"; + + ConfigurationDirectory = name; + RuntimeDirectory = name; + StateDirectory = name; + LogsDirectory = name; + ReadOnlyPaths = "/usr/lib/${name} ${cfg.tls.keyFile} ${cfg.tls.certFile}"; + ReadWritePaths = "/var/lib/${name}"; + + # Strict sandboxing. You have no reason to trust code written by strangers from GitHub. + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectHostname = true; + ProtectClock = true; + ProtectControlGroups = true; + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6"; + + # Additional sandboxing. You need to disable all of these options + # for privileged helper binaries (for system auth) to work correctly. + NoNewPrivileges = true; + PrivateDevices = true; + DeviceAllow = "/dev/syslog"; + RestrictSUIDSGID = true; + ProtectKernelModules = true; + MemoryDenyWriteExecute = true; + RestrictNamespaces = true; + RestrictRealtime = true; + LockPersonality = true; + + # Graceful shutdown with a reasonable timeout. + TimeoutStopSec = "7s"; + KillMode = "mixed"; + KillSignal = "SIGTERM"; + + + # Required to bind on ports lower than 1024. + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + CapabilityBoundingSet = "CAP_NET_BIND_SERVICE"; + + # Bump FD limitations. Even idle mail server can have a lot of FDs open (think + # of idle IMAP connections, especially ones abandoned on the other end and + # slowly timing out). + LimitNOFILE = 131072; + + # Limit processes count to something reasonable to + # prevent resources exhausting due to big amounts of helper + # processes launched. + LimitNPROC = 512; + + # Restart server on any problem. + Restart = "on-failure"; + # ... Unless it is a configuration problem. + RestartPreventExitStatus = 2; + }; + script = "${cfg.package}/bin/maddy -config ${configFile}"; + reload = "/bin/kill -USR1 $MAINPID"; + }; + }; +} diff --git a/modules/cloud/traefik/certs-dumper.nix b/modules/cloud/traefik/certs-dumper.nix new file mode 100644 index 0000000..40826aa --- /dev/null +++ b/modules/cloud/traefik/certs-dumper.nix @@ -0,0 +1,46 @@ +{ pkgs, config, lib, ... }: + +with lib; +let + cfg = config.cloud.traefik.certsDumper; +in +{ + options.cloud.traefik.certsDumper = { + enable = mkEnableOption "Dump certs onto a given directory "; + package = mkOption { + type = types.package; + default = pkgs.unstable.traefik-certs-dumper; + description = "The certs dumper package to use"; + }; + destination = mkOption { + type = types.str; + default = "/var/lib/traefik-certs"; + description = "The destination folder to dump certs onto"; + }; + }; + + config.systemd.services.traefik-certs-dumper = mkIf cfg.enable { + after = [ "traefik.service" ]; + path = with pkgs; [ openssl ]; + wantedBy = [ "multi-user.target" ]; + + description = "Dump certificates generated by traefik to a destination folder"; + serviceConfig = + let + user = config.systemd.services.traefik.serviceConfig.User; + group = config.systemd.services.traefik.serviceConfig.Group; + certsPath = config.cloud.traefik.certsPath; + in + { + User = user; + Group = group; + ExecStart = "${cfg.package}/bin/traefik-certs-dumper file --watch --domain-subdir=true --version v2 --source ${certsPath} --dest ${cfg.destination} --post-hook 'chmod -R +r ${cfg.destination}'"; + LimitNOFILE = "1048576"; + PrivateTmp = "true"; + PrivateDevices = "true"; + ProtectHome = "true"; + ProtectSystem = "strict"; + StateDirectory = "traefik-certs"; + }; + }; +} diff --git a/modules/cloud/traefik/config.nix b/modules/cloud/traefik/config.nix index 5e5395b..8da7eda 100644 --- a/modules/cloud/traefik/config.nix +++ b/modules/cloud/traefik/config.nix @@ -7,18 +7,20 @@ let # Copied from traefik.nix jsonValue = with types; let - valueType = nullOr (oneOf [ - bool - int - float - str - (lazyAttrsOf valueType) - (listOf valueType) - ]) // { + valueType = nullOr + (oneOf [ + bool + int + float + str + (lazyAttrsOf valueType) + (listOf valueType) + ]) // { description = "JSON value"; emptyValue.value = { }; }; - in valueType; + in + valueType; hostType = with types; submodule { options = { @@ -41,49 +43,76 @@ let description = "The port that the service is listening on"; }; entrypoints = mkOption { - type = listOf (enum ["http" "https" "smtp-submission" "imap"]); + type = listOf (enum [ "http" "https" "smtp-submission" "smtp-submission-ssl" "imap" ]); default = [ "https" ]; description = "The entrypoints that will serve the host"; }; middlewares = mkOption { type = listOf jsonValue; - default = []; + default = [ ]; description = "The middlewares to be used with the host."; }; + protocol = mkOption { + type = enum [ "http" "tcp" ]; + default = "http"; + description = "The protocol of the router and service"; + }; + tlsPassthrough = mkOption { + type = types.bool; + default = true; + description = "Sets the TCP passthrough value. Defaults to `true` if the connection is tcp"; + }; }; }; # Returns the filter given a host configuration - filterOfHost = host : + filterOfHost = host: + let + hostFilter = if host.protocol == "http" then "Host" else "HostSNI"; + in if host.filter != null then host.filter - else if host.path == null then "Host(`${host.host}`)" - else "Host(`${host.host}`) && Path(`${host.path}`)"; + else if host.path == null then "${hostFilter}(`${host.host}`)" + else "${hostFilter}(`${host.host}`) && Path(`${host.path}`)"; # Turns a host configuration into dynamic traefik configuration - hostToConfig = name : host : { - http.routers."${name}-router" = { - rule = filterOfHost host; - entryPoints = host.entrypoints; - tls.certResolver = "le"; - service = "${name}-service"; - middlewares = lists.imap0 (id: m: "${name}-middleware-${toString id}") host.middlewares; - }; - http.services."${name}-service".loadBalancer.servers = [ - { url = "http://localhost:${toString host.port}"; } - ]; - http.middlewares = builtins.listToAttrs (lists.imap0 (id: v: { - name = "${name}-middleware-${toString id}"; - value = v; - }) host.middlewares); + hostToConfig = name: host: { + "${host.protocol}" = { + routers."${name}-router" = { + rule = filterOfHost host; + entryPoints = host.entrypoints; + tls = { certResolver = "le"; } // (if host.protocol == "tcp" then { passthrough = if (host ? tlsPassthrough) then host.tlsPassthrough else true; } else { }); + service = "${name}-service"; + + } // ( + if host.protocol == "http" then + { middlewares = lists.imap0 (id: m: "${name}-middleware-${toString id}") host.middlewares; } + else if host.middlewares == [ ] then + { } + else abort "Cannot have middlewares on tcp routers" + ); + services."${name}-service".loadBalancer.servers = [ + (if host.protocol == "http" then + { url = "http://localhost:${toString host.port}"; } + else { address = "127.0.0.1:${toString host.port}"; } + ) + ]; + } // (if (host.middlewares != [ ]) then { + middlewares = builtins.listToAttrs (lists.imap0 + (id: v: { + name = "${name}-middleware-${toString id}"; + value = v; + }) + host.middlewares); + } else { }); }; in { options.cloud.traefik.hosts = mkOption { type = types.attrsOf hostType; - default = {}; + default = { }; description = "The HTTP hosts to run on the server"; }; - config.cloud.traefik.config = builtins.foldl' attrsets.recursiveUpdate {} (attrsets.mapAttrsToList hostToConfig cfg.hosts); + config.cloud.traefik.config = builtins.foldl' attrsets.recursiveUpdate { } (attrsets.mapAttrsToList hostToConfig cfg.hosts); } diff --git a/modules/cloud/traefik/dashboard.nix b/modules/cloud/traefik/dashboard.nix index cedf300..4c99159 100644 --- a/modules/cloud/traefik/dashboard.nix +++ b/modules/cloud/traefik/dashboard.nix @@ -35,6 +35,7 @@ in entryPoints = [ "https" ]; middlewares = [ "dashboard-auth" ]; service = "api@internal"; + tls.certResolver = "le"; }; }; } diff --git a/modules/cloud/traefik/default.nix b/modules/cloud/traefik/default.nix index 009fa4d..5a3a333 100644 --- a/modules/cloud/traefik/default.nix +++ b/modules/cloud/traefik/default.nix @@ -21,7 +21,7 @@ let cfg = config.cloud.traefik; in { - imports = [ ./config.nix ./dashboard.nix ]; + imports = [ ./config.nix ./dashboard.nix ./certs-dumper.nix ]; options.cloud.traefik = { cloudflareKeyFile = mkOption { type = types.path; @@ -33,6 +33,12 @@ in default = {}; description = "The dynamic configuration to be passed to traefik"; }; + + certsPath = mkOption { + type = types.str; + default = "/var/lib/traefik/acme.json"; + description = "The location to read and write the certificates file onto"; + }; }; config.services.traefik = { @@ -52,6 +58,7 @@ in ## IMAP and SMTP entrypoints.imap.address = ":993"; entrypoints.smtp-submission.address = ":587"; + entrypoints.smtp-submission-ssl.address = ":465"; # Logging # ------- @@ -62,7 +69,7 @@ in # ------------------ certificatesResolvers.le.acme = { email = "natsukagami@gmail.com"; - storage = "/var/lib/traefik/acme.json"; + storage = cfg.certsPath; dnsChallenge.provider = "cloudflare"; dnsChallenge.delayBeforeCheck = 10; }; @@ -74,6 +81,6 @@ in config.systemd.services.traefik.environment.CF_DNS_API_TOKEN_FILE = cfg.cloudflareKeyFile; # Set up firewall to allow traefik traffic. - config.networking.firewall.allowedTCPPorts = [ 80 443 993 587 ]; + config.networking.firewall.allowedTCPPorts = [ 80 443 993 587 465 ]; config.networking.firewall.allowedUDPPorts = [ 443 ]; # QUIC } diff --git a/nki-personal-do/configuration.nix b/nki-personal-do/configuration.nix index 9639481..e6f6a8b 100644 --- a/nki-personal-do/configuration.nix +++ b/nki-personal-do/configuration.nix @@ -7,6 +7,7 @@ ../modules/cloud/postgresql ../modules/cloud/traefik ../modules/cloud/bitwarden + ../modules/cloud/mail ]; boot.cleanTmpDir = true; @@ -54,4 +55,14 @@ enable = true; usersFile = config.sops.secrets.traefik-dashboard-users.path; }; + cloud.traefik.certsDumper.enable = true; + + # Mail + sops.secrets.mail-users = { owner = "maddy"; }; + cloud.mail = { + enable = true; + tls.certFile = "${config.cloud.traefik.certsDumper.destination}/${config.cloud.mail.hostname}/certificate.crt"; + tls.keyFile = "${config.cloud.traefik.certsDumper.destination}/${config.cloud.mail.hostname}/privatekey.key"; + usersFile = config.sops.secrets.mail-users.path; + }; } diff --git a/nki-personal-do/secrets/secrets.yaml b/nki-personal-do/secrets/secrets.yaml index 56cac55..d9dc3b6 100644 --- a/nki-personal-do/secrets/secrets.yaml +++ b/nki-personal-do/secrets/secrets.yaml @@ -1,7 +1,8 @@ tinc-private-key: ENC[AES256_GCM,data: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,iv:Aa/5BI0ldoq68b3aQ5RhTwQEmUldUOBK7LK6Jbq/gdY=,tag:y8pGEOgeeDtiBSd7ZFROqg==,type:str] cloudflare-dns-api-token: ENC[AES256_GCM,data:2ny3JehpK30fTUDKrbzHv1QOczriChRyMQn6kNPULpUJ+eVwdptLvg==,iv:8wNAn3oawzLez7sO4ZvhFXcaZIpFVKgKCvTBlszFHn8=,tag:fRaO+u/5MtAWnTiy2Zwh0Q==,type:str] #ENC[AES256_GCM,data:KWrVRQg+cLm5MUdfsYrh7hkI4CWkl4Z0sDj0769eebeXDy+veixrQrxh1ZW+ro3WLwoIdU/IH5DPM4TWYn2qoM5aDHjGX764pr1x,iv:uZHBsGvSHv9vd/Wragl1dYNJ+8vCcMit2K3SrMFlz7s=,tag:7z4LyADfQvXsM2vvtWru8w==,type:comment] -traefik-dashboard-users: ENC[AES256_GCM,data:r2QZ0hwGHMQ6LQ4OAf1QQkyyc9ONudPJN0JkrlEgXAl0cpbrzvLRqkQip3c/o/TIRnP27vRq5kLhowaj64lhzROCxJZxusYEtn5axg==,iv:inbNrLjHp4Oj+AWC6sk/70GLojrFQtoASURLHBrBZCc=,tag:H9S89lkOQH6Tv60lgQpSoQ==,type:str] +traefik-dashboard-users: ENC[AES256_GCM,data:kviapOq+xzxhjryse+5DaZbXRS/LEYyjqqFbHymXAZVEkWlu0T5pZ2bxSNCbXN+tXnb0u+6YPgGCaRNPLW74AF1hO8W8QqlLDA==,iv:41bwPyFQcuOLILTjLWUu5Kcnct/MaIIJsMbllc+n7Y0=,tag:17HyUjfRUcLGb0FrUm1O2A==,type:str] +mail-users: ENC[AES256_GCM,data:66tQo045ekEQJncnDLdJiQ6NneyRW9i0J3mr9fejo1SnB1OBRnEf++d36sL/Yi2fGhM2Mh2kObutodDGijYrx4ZtEDAxFzOuvEoLH+HpwAdHAcQ=,iv:jZ5EAEC2s0Z35uY6j4tQ8JLAOACmLAuavdMW0udhpzU=,tag:Y7dwqEiccQ8a1SwhIPAMfA==,type:str] sops: kms: [] gcp_kms: [] @@ -17,8 +18,8 @@ sops: NUovcTZlOVpyTm5WWGkyUmdLRUVpcmMK1YIwNE/5avvplxqtUFs1JZn7f2AuTzyR lRtXUm8InT5GwV50Ot6FLdai5aVxpicafduH/J5RSAXqL8LssQi7HA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2021-11-01T19:42:56Z" - mac: ENC[AES256_GCM,data:0zwLfeB35KlCW5DhaLxSta6a4mBR30Z6PiyrNVPXsJ6rWF4DI02G7geJgPc19rWc6APXKyPjpG2Ij79I/buayG76hUiJY5wWa8IvRDZYjiMsqLy8K3ZD1xPoUaTxzdvV1YGIsD2JRCLXaAaLDxxFDWtCCwELOAsphyaORhexvNs=,iv:9+QqpcbOjM+1YPjySimxeDaG7ovmbvqzL2DynbE+QLc=,tag:V3qSKqyxcMAU8KhZlpI8CA==,type:str] + lastmodified: "2021-11-01T22:35:16Z" + mac: ENC[AES256_GCM,data:EMJWOssHLTHQ/5/7OkMex5sN0oBPnEpeWLQQnlTkMeahtdO3p5isYNPfnFGLQR/2XyfeBJ/uvVUDI7T1k/Cc5Mr6bUwPAXRhKhOm50uQxxgI7i73BsNkyDMwJGAN0ZAPy2Rw3yjPorI0sVrmarQSBKPcLNspKTn9jcn9nMLuzrI=,iv:1neuIRh5A8ODu2O+ytMoFVNo3n8w6Kru1XgrJ3YGzB4=,tag:/BLZOrWCTck8v/3QFe9vXw==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.7.1