diff --git a/secrets/dns/challenge.age b/secrets/dns/challenge.age new file mode 100644 index 0000000..3f50ce7 --- /dev/null +++ b/secrets/dns/challenge.age @@ -0,0 +1,16 @@ +age-encryption.org/v1 +-> ssh-ed25519 VQDASQ mK2dVgCVFtLPW3eaRemGvmV8XRCXjvlWgh3CyEP/HRs +Iv49IVQqzO14ggd3yqTbjEzEhWxLlxdeXEKATO6i3sE +-> ssh-ed25519 PV+Tsg vuk/IMPh7O6erZ4/lWAAeVQODnMFFIOrfY0fLOXT6Sk +B13BqD51YsPppMEJRnApEQ4N20YWSzHLv3YOGvXpERE +-> ssh-ed25519 Ecp4NA medZ8WVt4uwDLfFtxnpGbbBwP/JZQlWoHYfbuDtb0Us +JjKUAZUMYK4vXaS6QFVZVKoQO+zO7+kUyJGXC050yvE +-> ssh-ed25519 vHebMw FkWAklfnd60izhTwSPkmBZqMawBgcmG3p4xWhcUFElc +1TBv5EfHjaJFY/7kpDt7MBBB/svS2zqlNw3/4HkDxfk +-> ssh-ed25519 um7xWA lcSH23MCgWy3YevbzzZeOI9S7U1uzOwJvSagskSyl2k +7BoMoGFVx/I6P6kQE0Ku4wXMASCIdioX19n9BOlEoNM +-> ssh-ed25519 oRtTqQ QRA+pNeSutaGDIE1Ov2e6z/6/zwWnhhN+iTQoABvWgc +gTxHrYwKwr6sQXA5ySKWav8+IYxw+NN+54q+3fc6C4I +--- qrDkUs3UczTG7EDiOxbULZQ8+O4aAP6J6a6ys2tuyjI +tzOb3쁠'W7b,Uo*Z%;ʖ?F {MNg:nEXᢗz \r- + 5CN 7r^Mg>}m֐` \ No newline at end of file diff --git a/secrets/mail/mbox/test.age b/secrets/mail/mbox/test.age new file mode 100644 index 0000000..8905b4b --- /dev/null +++ b/secrets/mail/mbox/test.age @@ -0,0 +1,16 @@ +age-encryption.org/v1 +-> ssh-ed25519 VQDASQ iTY4Dv+Hhxdd7XBliAhAKvPvm96cZcKOqkbC1/PPZEA +buKTlrIQwx2O6dw0PO2U20dYwHfyjvb99IVp9xAKmCc +-> ssh-ed25519 PV+Tsg fEV9Wshu13hi5+GNCgQM8p9JSFm3N7vhZENxEXBdqWM +uXyz+OfdsLV4PEwjErBIR46cEzp4wYyB/q2XtIgh8ic +-> ssh-ed25519 Ecp4NA oaBz9Yd6fgz3wqf85RR1+m45Rj61t1OOiJ3J92WCsx8 +4XSBmWE7VlkcvmProSMN1YJ67I2c2VDDZz46QYEiUVI +-> ssh-ed25519 vHebMw ngxp3WhtYOOCUdmqK3PUqVv94iSzQkcXwTkugQ6mzBI +PNZZbo9nXJmpe2ajwn8KQ101zP71bfcYNd3FdCS/+Fs +-> ssh-ed25519 um7xWA ukb8EQy2ksckyJI6XAPviKdiFj+KdiW2uyKo8W+ZV1k +62WU+yOy322tDkuodVdq7405lt+PlUMCOEom3/0JbR0 +-> ssh-ed25519 oRtTqQ liDAk9KgFQLzcNDa+EKeLMoXHJoc1UnPrhSd7iPWDgo +0x/LIAuRb4f5ebWREJc5XwP2UmObNlaQAR1s0vW1WH4 +--- +Mt/wbUkihs5CGurWGR2IWjolm2APznuFHs7tUR+teY +(/wr\܏ +YyӐ֯M?#>gfMxۓF7L?_KĈKn`/P9GbxAN \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 0d1d7b7..f8face1 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -97,8 +97,11 @@ in # DNS secrets "dns/tsig.age".publicKeys = system-dns ++ users; + # This is the secret of the tsig key isolated, if the tsig.age is modified, this one should be updated accordingly + "dns/challenge.age".publicKeys = system-mail ++ users; # Mail secrets "mail/dkim.age".publicKeys = system-mail ++ users; + "mail/mbox/test.age".publicKeys = system-mail ++ users; } \ No newline at end of file diff --git a/shared/commons/nftables.nix b/shared/commons/nftables.nix index 111cfb5..52a0aa5 100644 --- a/shared/commons/nftables.nix +++ b/shared/commons/nftables.nix @@ -65,6 +65,14 @@ in udp dport 53 accept '' else ""} + ${if lib.elem myName mapping.mail.hosts then '' + # Mail server (without IMAP) + # With support for both SSL & STARTTLS + tcp dport 25 accept + tcp dport 465 accept + tcp dport 587 accept + '' else ""} + # Log anything else ip protocol tcp counter log prefix "tcp.in.dropped: " ip protocol udp counter log prefix "udp.in.dropped: " diff --git a/shared/mail.nix b/shared/mail.nix index a26809f..d9b4d0e 100644 --- a/shared/mail.nix +++ b/shared/mail.nix @@ -1,4 +1,9 @@ { ... }: +let + ensureAccountsWithoutIMAP = [ + "test@lasuite.federez.net" + ]; +in { # Import dependencies imports = [ diff --git a/shared/mail/maddy.nix b/shared/mail/maddy.nix index 409749a..35e38e5 100644 --- a/shared/mail/maddy.nix +++ b/shared/mail/maddy.nix @@ -9,13 +9,219 @@ let # And mapping mapping = import ./../../mapping.nix; + acct = ensureAccountsWithoutIMAP; + + generatedCredsCommand = mbox: '' + ${pkgs.maddy}/bin/maddyctl creds remove --yes ${mbox} + ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${config.age.secrets."mbox-${builtins.head (builtins.split "@" mbox)}".path}) ${mbox} + ''; + + maddyDeployCreds = pkgs.writeShellScriptBin "maddyDeployCreds" '' + ${builtins.concatStringsSep "\n" (map generatedCredsCommand acct)} + ''; + + hostToDomain = hostname: "${builtins.replaceStrings ["-"] ["."] hostname}.net"; + + buildSecret = mbox: { + "mbox-${builtins.head (builtins.split "@" mbox)}" = { + file = ./../../secrets/mail/mbox + ( "/" + builtins.head (builtins.split "@" mbox) + ".age" ); + owner = "maddy"; + group = "maddy"; + }; + }; + + generatedSecrets = lib.foldl' (acc: mbox: acc // buildSecret mbox) {} acct; in { - age.secrets = { - "tsig" = { - file = ./../../secrets/mail/tsig.age; - owner = "knot"; - group = "knot"; + age.secrets = generatedSecrets // { + "challenge" = { + file = ./../../secrets/dns/challenge.age; + owner = "acme"; + group = "acme"; + }; + "dkim-keys" = { + file = ./../../secrets/mail/dkim.age; + mode = "440"; + owner = "maddy"; + group = "maddy"; + }; + }; + + services.maddy = { + enable = true; + hostname = "lasuite.federez.net"; + user = "maddy"; + group = "maddy"; + openFirewall = true; + primaryDomain = "lasuite.federez.net"; + tls = { + loader = "file"; + certificates = [{ + keyPath = "/var/lib/acme/lasuite.federez.net/key.pem"; + certPath = "/var/lib/acme/lasuite.federez.net/fullchain.pem"; + }]; + extraConfig = '' + protocols tls1.0 tls1.3 + ''; + }; + config = '' + auth.pass_table local_authdb { + table sql_table { + driver sqlite3 + dsn credentials.db + table_name passwords + } + } + + table.chain local_rewrites { + optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" + optional_step static { + entry postmaster postmaster@$(primary_domain) + } + optional_step file /etc/maddy/aliases + } + + msgpipeline local_routing { + default_destination { + reject 550 5.1.1 "We don't accept incoming email on lasuite.federez.net, please use federez.net ml" + } + } + + smtp tcp://0.0.0.0:25 { + limits { + 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 tls://0.0.0.0:465 tcp://0.0.0.0:587 { + limits { + all rate 50 1s + } + auth &local_authdb + source $(local_domains) { + check { + authorize_sender { + prepare_email &local_rewrites + user_to_email identity + } + } + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + modify { + dkim { + debug yes + domains $(primary_domain) $(local_domains) + selector default + key_path ${config.age.secrets.dkim-keys.path} + } + } + deliver_to &remote_queue + } + } + default_source { + reject 501 5.1.8 "Non-local sender domain" + } + } + + target.remote outbound_delivery { + limits { + 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" + } + } + } + ''; + # ensureAccounts automatically create an IMAP account which we do not wish + ensureAccounts = []; + ensureCredentials = { + # Be careful there: those are deployed at runtime only if they do not already exists + # As a result using agenix to deploy them will silently failed + # See impl: https://github.com/NixOS/nixpkgs/blob/nixos-25.05/nixos/modules/services/mail/maddy.nix + # We will write our own script. + # But avoid doing this: + # "test@lasuite.federez.net".passwordFile = config.age.secrets.monitoring.path; + # "test2@lasuite.federez.net".passwordFile = config.age.secrets.postmaster.path; + # ... + }; + }; + + # Execute custom password setup script + systemd.services.maddyDeployCredsSrv = { + wantedBy = [ "multi-user.target" ]; + after = [ "maddy.service" ]; + description = "Add password to maddy's static account"; + serviceConfig = { + Type = "oneshot"; + User = "maddy"; + ExecStart = "${maddyDeployCreds}/bin/maddyDeployCreds"; + }; + }; + + security.acme = { + acceptTerms = true; + defaults.email = "monitoring@lasuite.federez.net"; + certs = { + "lasuite.federez.net" = { + domain = "lasuite.federez.net"; + # Needed for mta-sts compliance + extraDomainNames = map hostToDomain mapping.mail.hosts; + group = "maddy"; + dnsProvider = "rfc2136"; + reloadServices = [ + "maddy" + ]; + dnsPropagationCheck = true; + enableDebugLogs = true; + environmentFile = "${pkgs.writeText "dns-creds" '' + RFC2136_NAMESERVER=172.19.${nodes.${mapping.dns.master}.zone}.${nodes.${mapping.dns.master}.id} + RFC2136_TSIG_KEY=letsencrypt + RFC2136_TSIG_ALGORITHM=hmac-sha512. + RFC2136_TSIG_SECRET_FILE="${config.age.secrets.challenge.path}" + ''}"; + }; }; }; } \ No newline at end of file