From 02325a70178f1166fdd93213b71a516b388b58ab Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Wed, 30 Jul 2025 21:36:48 +0200 Subject: [PATCH 1/2] Begin preparing to move LDAP accounts into passdb only This should allow LDAP accounts to have password set in LDAP, as well as provisioning service accounts statically in nix. This will also move alias configuration of all accounts into nix as well. --- .../infrastructure/mailserver/accounts.nix | 128 ++++++++++++++++++ .../nixos/infrastructure/mailserver/ldap.nix | 70 ++++------ 2 files changed, 158 insertions(+), 40 deletions(-) create mode 100644 nix/modules/nixos/infrastructure/mailserver/accounts.nix diff --git a/nix/modules/nixos/infrastructure/mailserver/accounts.nix b/nix/modules/nixos/infrastructure/mailserver/accounts.nix new file mode 100644 index 0000000..5a948b6 --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/accounts.nix @@ -0,0 +1,128 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.khscodes.infrastructure.mailserver; + accountOption = lib.khscodes.mkSubmodule { + description = "mail account"; + options = { + name = lib.mkOption { + type = lib.types.str; + example = "user1@example.com"; + description = "Username"; + }; + + aliases = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = [ + "abuse@example.com" + "postmaster@example.com" + ]; + default = [ ]; + description = '' + A list of aliases of this login account. + Note: Use list entries like "@example.com" to create a catchAll + that allows sending from all email addresses in these domain. + ''; + }; + + aliasesRegexp = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = [ ''/^tom\..*@domain\.com$/'' ]; + default = [ ]; + description = '' + Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). + ''; + }; + + catchAll = lib.typs.mkOption { + type = lib.types.listOf (lib.types.enum config.mailserver.domains); + example = [ + "example.com" + "example2.com" + ]; + default = [ ]; + description = '' + For which domains should this account act as a catch all? + Note: Does not allow sending from all addresses of these domains. + ''; + }; + + quota = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "2G"; + description = '' + Per user quota rules. Accepted sizes are `xx k/M/G/T` with the + obvious meaning. Leave blank for the standard quota `100G`. + ''; + }; + + sieveScript = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; + example = '' + require ["fileinto", "mailbox"]; + + if address :is "from" "gitlab@mg.gitlab.com" { + fileinto :create "GitLab"; + stop; + } + + # This must be the last rule, it will check if list-id is set, and + # file the message into the Lists folder for further investigation + elsif header :matches "list-id" "" { + fileinto :create "Lists"; + stop; + } + ''; + description = '' + Per-user sieve script. + ''; + }; + + sendOnly = lib.types.mkOption { + type = lib.types.bool; + default = false; + description = '' + Specifies if the account should be a send-only account. + Emails sent to send-only accounts will be rejected from + unauthorized senders with the `sendOnlyRejectMessage` + stating the reason. + ''; + }; + + sendOnlyRejectMessage = lib.mkOption { + type = lib.types.str; + default = "This account cannot receive emails."; + description = '' + The message that will be returned to the sender when an email is + sent to a send-only account. Only used if the account is marked + as send-only. + ''; + }; + + isLdapAccount = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Marks the account as an LDAP account. Non LDAP accounts gets passwords provisioned, such that other services can use them"; + }; + }; + }; + + systemAccounts = lib.attrsets.foldlAttrs ( + acc: name: value: + if value.isLdapAccount then acc else acc ++ [ name ] + ) [ ] cfg.accounts; +in +{ + options.khscodes.infrastructure.mailserver.accounts = lib.mkOption { + type = lib.types.attrsOf accountOption; + default = { }; + }; + config = lib.mkIf cfg.enable { + }; +} diff --git a/nix/modules/nixos/infrastructure/mailserver/ldap.nix b/nix/modules/nixos/infrastructure/mailserver/ldap.nix index 8699fbc..2b0ae43 100644 --- a/nix/modules/nixos/infrastructure/mailserver/ldap.nix +++ b/nix/modules/nixos/infrastructure/mailserver/ldap.nix @@ -1,7 +1,12 @@ -{ lib, config, ... }: +{ + lib, + config, + pkgs, + ... +}: let cfg = config.khscodes.infrastructure.mailserver; - secretFile = "/run/secret/dovecot/ldap"; + secretFile = "/run/secret/dovecot/dovecot-ldap.conf.ext"; in { options.khscodes.infrastructure.mailserver = { @@ -16,45 +21,20 @@ in }; config = lib.mkIf cfg.enable { - mailserver.ldap = { - enable = true; - uris = [ "ldaps://login.kaareskovgaard.net" ]; - searchBase = "dc=login,dc=kaareskovgaard,dc=net"; - searchScope = "sub"; - bind = { - dn = "dn=token"; - passwordFile = secretFile; - }; - dovecot = { - # Map LDAP uid to dovecot user, and ldap userPassword to dovecot password - passAttrs = "uid=user"; - passFilter = "(&(class=account)(memberOf=mail_user)(uid=%u))"; - # This filter is used both when receiving mail (thus needing to lookup by mail address, and when authenticating, requriing the lookup by uid.) - # Note that the pass filter only allows looking up by uid, so should still only be able to authenticate using that. - userFilter = "(&(class=account)(memberOf=mail_user)(|(mail=%u)(uid=%u)))"; - userAttrs = "uid=user"; - }; - postfix = { - filter = "(&(class=account)(memberOf=mail_user)(mail=%s))"; - mailAttribute = "uid"; - uidAttribute = "uid"; - }; - }; + services.dovecot2.extraConfig = '' + passdb { + driver = ldap + args = ${secretFile} + } + ''; systemd.services = { dovecot2 = { unitConfig = { ConditionPathExists = [ secretFile ]; }; - }; - postfix = { - unitConfig = { - ConditionPathExists = [ secretFile ]; - }; - }; - postfix-setup = { - unitConfig = { - ConditionPathExists = [ secretFile ]; - }; + serviceConfig.ReadOnlyPaths = [ + secretFile + ]; }; }; @@ -62,15 +42,25 @@ in { contents = '' {{- with secret "${cfg.ldap.mount}/data/${cfg.ldap.path}" -}} - {{ .Data.data.apiToken }} + ldap_version = 3 + uris = ldaps://login.kaareskovgaard.net + + tls_require_cert = hard + tls_ca_cert_file = ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + dn = dn=token + dnpass = {{ .Data.data.apiToken }} + auth_bind = yes + base = dc=login,dc=kaareskovgaard,dc=net + scope = subtree + pass_attrs = uid=user + pass_filter = (&(class=account)(memberOf=mail_user)(uid=%u) {{- end -}} ''; destination = secretFile; - owner = "dovecot"; - group = "dovecot"; + owner = "root"; + group = "root"; restartUnits = [ "dovecot2.service" - "postfix.service" ]; } ]; From fbe957b046e179dac825165638612dce0bf2e401 Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Thu, 31 Jul 2025 00:04:13 +0200 Subject: [PATCH 2/2] Move the setup of the mailserver around Currently delivery of mails is broken. There's some work to be done in accounts.nix. But once done this should (I think) support all the use cases desired. --- .../infrastructure/mailserver/accounts.nix | 128 --------- .../nixos/infrastructure/mailserver/acme.nix | 9 - .../infrastructure/mailserver/managesieve.nix | 9 - .../mx.kaareskovgaard.net/default.nix | 92 ++----- .../mailserver/accounts.nix | 244 ++++++++++++++++++ .../mx.kaareskovgaard.net/mailserver/acme.nix | 6 + .../mailserver/dane.nix | 0 .../mailserver/default.nix | 12 +- .../mailserver/dkim.nix | 35 +-- .../mailserver/dmarc.nix | 4 +- .../mailserver/ldap.nix | 22 +- .../mailserver/managesieve.nix | 10 + .../mailserver/mta-sts.nix | 4 +- .../mailserver/openid-connect.nix | 22 +- .../mailserver/prometheus.nix | 6 +- .../mailserver/roundcube.nix | 18 ++ .../mx.kaareskovgaard.net}/mailserver/spf.nix | 4 +- .../mailserver/tls-rpt.nix | 4 +- .../mx.kaareskovgaard.net/testuser.nix | 71 ----- .../mx.kaareskovgaard.net/users.nix | 11 + 20 files changed, 367 insertions(+), 344 deletions(-) delete mode 100644 nix/modules/nixos/infrastructure/mailserver/accounts.nix delete mode 100644 nix/modules/nixos/infrastructure/mailserver/acme.nix delete mode 100644 nix/modules/nixos/infrastructure/mailserver/managesieve.nix create mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix create mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/acme.nix rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/dane.nix (100%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/default.nix (89%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/dkim.nix (87%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/dmarc.nix (82%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/ldap.nix (78%) create mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/managesieve.nix rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/mta-sts.nix (93%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/openid-connect.nix (55%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/prometheus.nix (60%) create mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/roundcube.nix rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/spf.nix (78%) rename nix/{modules/nixos/infrastructure => systems/aarch64-linux/mx.kaareskovgaard.net}/mailserver/tls-rpt.nix (80%) delete mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/testuser.nix create mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix diff --git a/nix/modules/nixos/infrastructure/mailserver/accounts.nix b/nix/modules/nixos/infrastructure/mailserver/accounts.nix deleted file mode 100644 index 5a948b6..0000000 --- a/nix/modules/nixos/infrastructure/mailserver/accounts.nix +++ /dev/null @@ -1,128 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: -let - cfg = config.khscodes.infrastructure.mailserver; - accountOption = lib.khscodes.mkSubmodule { - description = "mail account"; - options = { - name = lib.mkOption { - type = lib.types.str; - example = "user1@example.com"; - description = "Username"; - }; - - aliases = lib.mkOption { - type = lib.types.listOf lib.types.str; - example = [ - "abuse@example.com" - "postmaster@example.com" - ]; - default = [ ]; - description = '' - A list of aliases of this login account. - Note: Use list entries like "@example.com" to create a catchAll - that allows sending from all email addresses in these domain. - ''; - }; - - aliasesRegexp = lib.mkOption { - type = lib.types.listOf lib.types.str; - example = [ ''/^tom\..*@domain\.com$/'' ]; - default = [ ]; - description = '' - Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). - ''; - }; - - catchAll = lib.typs.mkOption { - type = lib.types.listOf (lib.types.enum config.mailserver.domains); - example = [ - "example.com" - "example2.com" - ]; - default = [ ]; - description = '' - For which domains should this account act as a catch all? - Note: Does not allow sending from all addresses of these domains. - ''; - }; - - quota = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "2G"; - description = '' - Per user quota rules. Accepted sizes are `xx k/M/G/T` with the - obvious meaning. Leave blank for the standard quota `100G`. - ''; - }; - - sieveScript = lib.mkOption { - type = lib.types.nullOr lib.types.lines; - default = null; - example = '' - require ["fileinto", "mailbox"]; - - if address :is "from" "gitlab@mg.gitlab.com" { - fileinto :create "GitLab"; - stop; - } - - # This must be the last rule, it will check if list-id is set, and - # file the message into the Lists folder for further investigation - elsif header :matches "list-id" "" { - fileinto :create "Lists"; - stop; - } - ''; - description = '' - Per-user sieve script. - ''; - }; - - sendOnly = lib.types.mkOption { - type = lib.types.bool; - default = false; - description = '' - Specifies if the account should be a send-only account. - Emails sent to send-only accounts will be rejected from - unauthorized senders with the `sendOnlyRejectMessage` - stating the reason. - ''; - }; - - sendOnlyRejectMessage = lib.mkOption { - type = lib.types.str; - default = "This account cannot receive emails."; - description = '' - The message that will be returned to the sender when an email is - sent to a send-only account. Only used if the account is marked - as send-only. - ''; - }; - - isLdapAccount = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Marks the account as an LDAP account. Non LDAP accounts gets passwords provisioned, such that other services can use them"; - }; - }; - }; - - systemAccounts = lib.attrsets.foldlAttrs ( - acc: name: value: - if value.isLdapAccount then acc else acc ++ [ name ] - ) [ ] cfg.accounts; -in -{ - options.khscodes.infrastructure.mailserver.accounts = lib.mkOption { - type = lib.types.attrsOf accountOption; - default = { }; - }; - config = lib.mkIf cfg.enable { - }; -} diff --git a/nix/modules/nixos/infrastructure/mailserver/acme.nix b/nix/modules/nixos/infrastructure/mailserver/acme.nix deleted file mode 100644 index 1321901..0000000 --- a/nix/modules/nixos/infrastructure/mailserver/acme.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ lib, config, ... }: -let - cfg = config.khscodes.infrastructure.mailserver; -in -{ - config = lib.mkIf cfg.enable { - khscodes.services.nginx.virtualHosts."${config.khscodes.networking.fqdn}" = { }; - }; -} diff --git a/nix/modules/nixos/infrastructure/mailserver/managesieve.nix b/nix/modules/nixos/infrastructure/mailserver/managesieve.nix deleted file mode 100644 index 982cfbd..0000000 --- a/nix/modules/nixos/infrastructure/mailserver/managesieve.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ config, lib, ... }: -let - cfg = config.khscodes.infrastructure.mailserver; -in -{ - config = lib.mkIf cfg.enable { - services.dovecot2.protocols = [ "sieve" ]; - }; -} diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix index 5e9e54a..e0255ad 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix @@ -1,84 +1,46 @@ { inputs, - config, ... }: { imports = [ "${inputs.self}/nix/profiles/nixos/hetzner-server.nix" - # ./testuser.nix + ./mailserver ]; - khscodes.infrastructure.provisioning.pre.modules = [ - ( - { ... }: - { - khscodes.vault = { - enable = true; - mount."mx.kaareskovgaard.net" = { - path = "mx.kaareskovgaard.net"; - type = "kv"; - options = { - version = "2"; + khscodes = { + infrastructure = { + hetzner-instance = { + enable = true; + mapRdns = true; + server_type = "cax11"; + }; + provisioning.pre.modules = [ + ( + { ... }: + { + khscodes.vault = { + enable = true; + mount."mx.kaareskovgaard.net" = { + path = "mx.kaareskovgaard.net"; + type = "kv"; + options = { + version = "2"; + }; + description = "Secrets used for mx.kaareskovgaard.net"; + }; }; - description = "Secrets used for mx.kaareskovgaard.net"; - }; - }; - } - ) - ]; - khscodes.infrastructure = { - kanidm-client-application = { - enable = true; - appName = "dovecot"; - secretOwner = "dovecot2"; - perms = "0644"; + } + ) + ]; }; - hetzner-instance = { - enable = true; - mapRdns = true; - server_type = "cax11"; - }; - mailserver = { - enable = true; + "mx.kaareskovgaard.net" = { domains = [ "agerlin-skovgaard.dk" "agerlinskovgaard.dk" ]; - dkim = { - vault = { - mount = "mx.kaareskovgaard.net"; - prefixPath = "dkim"; - }; - }; - ldap = { - mount = "kanidm"; - path = "ldap/dovecot"; - }; + accounts = import ./users.nix; }; }; - services.roundcube = { - enable = true; - hostName = "mail.kaareskovgaard.net"; - configureNginx = true; - extraConfig = '' - # starttls needed for authentication, so the fqdn required to match - # the certificate - $config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}"; - $config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}"; - $config['oauth_provider'] = 'generic'; - $config['oauth_provider_name'] = 'Kanidm'; - $config['oauth_client_id'] = 'dovecot'; - $config['oauth_client_secret'] = file_get_contents("${config.khscodes.infrastructure.kanidm-client-application.secretFile}"); - $config['oauth_auth_uri'] = 'https://login.kaareskovgaard.net/ui/oauth2'; - $config['oauth_token_uri'] = 'https://login.kaareskovgaard.net/oauth2/token'; - $config['oauth_identity_uri'] = 'https://login.kaareskovgaard.net/oauth2/openid/dovecot/userinfo'; - $config['oauth_identity_fields'] = ['preferred_username']; - $config['oauth_scope'] = 'email openid profile'; - $config['plugins'] = [ - 'managesieve', - ]; - ''; - }; khscodes.services.nginx = { enable = true; virtualHosts."mail.kaareskovgaard.net" = { }; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix new file mode 100644 index 0000000..cb4c0ad --- /dev/null +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix @@ -0,0 +1,244 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.khscodes."mx.kaareskovgaard.net"; + passDbFile = "/run/secret/dovecot/passwd"; + bogusPasswdFile = pkgs.writeTextFile { + name = "bogus-passwd"; + text = "$6$1234"; + }; + accountPrimaryEmail = + name: account: if account.isLdapAccount then lib.lists.head account.aliases else name; + accountAlternativeEmails = + name: account: + if account.isLdapAccount then + lib.lists.ifilter0 (idx: _: idx > 0) account.aliases + else + account.aliases; + accountOption = lib.khscodes.mkSubmodule { + description = "mail account"; + options = { + name = lib.mkOption { + type = lib.types.str; + example = "user1@example.com"; + description = "Username"; + }; + + aliases = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = [ + "abuse@example.com" + "postmaster@example.com" + ]; + default = [ ]; + description = '' + A list of aliases of this login account. + Note: Use list entries like "@example.com" to create a catchAll + that allows sending from all email addresses in these domain. + ''; + }; + + aliasesRegexp = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = [ ''/^tom\..*@domain\.com$/'' ]; + default = [ ]; + description = '' + Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). + ''; + }; + + catchAll = lib.mkOption { + type = lib.types.listOf (lib.types.enum config.mailserver.domains); + example = [ + "example.com" + "example2.com" + ]; + default = [ ]; + description = '' + For which domains should this account act as a catch all? + Note: Does not allow sending from all addresses of these domains. + ''; + }; + + quota = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "2G"; + description = '' + Per user quota rules. Accepted sizes are `xx k/M/G/T` with the + obvious meaning. Leave blank for the standard quota `100G`. + ''; + }; + + sieveScript = lib.mkOption { + type = lib.types.nullOr lib.types.lines; + default = null; + example = '' + require ["fileinto", "mailbox"]; + + if address :is "from" "gitlab@mg.gitlab.com" { + fileinto :create "GitLab"; + stop; + } + + # This must be the last rule, it will check if list-id is set, and + # file the message into the Lists folder for further investigation + elsif header :matches "list-id" "" { + fileinto :create "Lists"; + stop; + } + ''; + description = '' + Per-user sieve script. + ''; + }; + + sendOnly = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Specifies if the account should be a send-only account. + Emails sent to send-only accounts will be rejected from + unauthorized senders with the `sendOnlyRejectMessage` + stating the reason. + ''; + }; + + sendOnlyRejectMessage = lib.mkOption { + type = lib.types.str; + default = "This account cannot receive emails."; + description = '' + The message that will be returned to the sender when an email is + sent to a send-only account. Only used if the account is marked + as send-only. + ''; + }; + + isLdapAccount = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Marks the account as an LDAP account. Non LDAP accounts gets passwords provisioned, such that other services can use them"; + }; + }; + }; + + systemAccounts = lib.attrsets.foldlAttrs ( + acc: name: value: + if value.isLdapAccount then acc else acc ++ [ name ] + ) [ ] cfg.accounts; + + systemAccountsPassDbTemplateContents = + lib.concatStringsSep "\n" ( + lib.lists.map (account: '' + {{- with secret "mx.kaareskovgaard.net/data/users/${account}" -}} + "${account}:{{ .Data.data.hashed_password }}::::::" + {{- end -}} + '') systemAccounts + ) + # Just make sure the file is not empty + + "\n"; + + # I would like to not use emails as usernames, but this is not something postfix is particularly happy with, + # as far as I can see. I *think* I can do this by randomly selecting an address, ie. the first, as the "primary" + # address, and then map all other addresses to that one. Then create a virtual mailbox map that maps that back + # to the username before delivering it to lmtp. All of this just worked when using ldap, but that meant + # not using code configured accounts set up with opentofu as well, and would also make the email delivery + # take a hard dependency on ldap (and not just the login process). + + # Create extra virtual aliases for all accounts (as they are not emails, that maps them to their primary email). + # Then we map those emails back into the account name in the mailbox maps. + extraVirtualAliases = lib.mapAttrs (name: account: accountPrimaryEmail name account) cfg.accounts; +in +{ + options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption { + type = lib.types.attrsOf accountOption; + default = { }; + }; + config = { + mailserver = { + inherit extraVirtualAliases; + loginAccounts = lib.attrsets.mapAttrs (name: value: { + inherit (value) + name + aliasesRegexp + catchAll + quota + sieveScript + sendOnly + sendOnlyRejectMessage + ; + aliases = accountAlternativeEmails name value; + hashedPasswordFile = bogusPasswdFile; + }) cfg.accounts; + }; + khscodes.infrastructure.vault-server-approle.policy = { + "mx.kaareskovgaard.net/data/users/*" = { + capabilities = [ "read" ]; + }; + }; + khscodes.infrastructure.provisioning.pre.modules = [ + { + terraform.required_providers.random = { + source = "hashicorp/random"; + version = "3.7.2"; + }; + provider.random = { }; + } + ] + ++ (lib.lists.map ( + account: + let + tfName = lib.khscodes.sanitize-terraform-name account; + in + { + resource.random_password."${tfName}" = { + length = 48; + numeric = true; + lower = true; + upper = true; + special = false; + }; + + resource.vault_kv_secret_v2."${tfName}" = { + mount = config.khscodes.vault.output.mount."mx.kaareskovgaard.net".path; + name = "users/${account}"; + data_json = '' + { + "hashed_password": ''${ jsonencode(resource.random_password.${tfName}.bcrypt_hash) }, + "password": ''${ jsonencode(resource.random_password.${tfName}.result) } + } + ''; + }; + } + ) systemAccounts); + khscodes.services.vault-agent.templates = [ + { + contents = systemAccountsPassDbTemplateContents; + destination = passDbFile; + perms = "0600"; + owner = "dovecot2"; + group = "dovecot2"; + restartUnits = [ + "dovecot2.service" + ]; + } + ]; + systemd.services.dovecot2 = { + unitConfig.ConditionPathExists = [ passDbFile ]; + serviceConfig.ReadOnlyPaths = [ passDbFile ]; + # simple-nixos-mailserver creates its own passwd file, + # but the passwords in that file are all bogus, so replace them + # with our own. + preStart = lib.mkAfter '' + cp ${passDbFile} /run/dovecot2/passwd + ''; + }; + # This prevents local usernames without domain names to get rewritten. + # services.postfix.submissionOptions = submissionOptions; + # services.postfix.submissionsOptions = submissionOptions; + }; +} diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/acme.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/acme.nix new file mode 100644 index 0000000..8cd30ec --- /dev/null +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/acme.nix @@ -0,0 +1,6 @@ +{ config, ... }: +{ + config = { + khscodes.services.nginx.virtualHosts."${config.khscodes.networking.fqdn}" = { }; + }; +} diff --git a/nix/modules/nixos/infrastructure/mailserver/dane.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dane.nix similarity index 100% rename from nix/modules/nixos/infrastructure/mailserver/dane.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dane.nix diff --git a/nix/modules/nixos/infrastructure/mailserver/default.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix similarity index 89% rename from nix/modules/nixos/infrastructure/mailserver/default.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix index c3a3aa9..789e0a6 100644 --- a/nix/modules/nixos/infrastructure/mailserver/default.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix @@ -5,12 +5,11 @@ ... }: let - cfg = config.khscodes.infrastructure.mailserver; + cfg = config.khscodes."mx.kaareskovgaard.net"; fqdn = config.khscodes.networking.fqdn; in { - options.khscodes.infrastructure.mailserver = { - enable = lib.mkEnableOption "Enables setting up stuff for a mail server"; + options.khscodes."mx.kaareskovgaard.net" = { domains = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; @@ -18,6 +17,7 @@ in }; imports = [ inputs.simple-nixos-mailserver.nixosModules.mailserver + ./accounts.nix ./acme.nix ./dmarc.nix ./dane.nix @@ -29,9 +29,9 @@ in ./prometheus.nix ./openid-connect.nix ./ldap.nix + ./roundcube.nix ]; - config = lib.mkIf cfg.enable { - # TODO: Include a similiar rule for openstack + config = { khscodes.infrastructure.hetzner-instance.extraFirewallRules = [ { direction = "out"; @@ -80,6 +80,7 @@ in ) ]; mailserver = { + inherit (cfg) domains; enable = true; enableImap = false; enableImapSsl = true; @@ -87,7 +88,6 @@ in enableSubmissionSsl = true; fqdn = config.khscodes.networking.fqdn; useUTF8FolderNames = true; - domains = cfg.domains; certificateScheme = "acme"; }; services.fail2ban.jails = { diff --git a/nix/modules/nixos/infrastructure/mailserver/dkim.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix similarity index 87% rename from nix/modules/nixos/infrastructure/mailserver/dkim.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix index ef1cfeb..81e0de5 100644 --- a/nix/modules/nixos/infrastructure/mailserver/dkim.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix @@ -5,7 +5,10 @@ ... }: let - cfg = config.khscodes.infrastructure.mailserver; + cfg = config.khscodes."mx.kaareskovgaard.net"; + mount = "mx.kaareskovgaard.net"; + mountExpr = "\${ vault_mount.${lib.khscodes.sanitize-terraform-name mount}.path }"; + prefixPath = "dkim"; opensslStripAsn1 = pkgs.writeTextFile { name = "openssl-strip-asn1"; executable = true; @@ -48,23 +51,9 @@ let ''''${ replace(trimprefix(trimsuffix(${tls_key}.public_key_pem, ${publicKeyEnd}), ${publicKeyBegin}), "\n", "") }''; in { - options.khscodes.infrastructure.mailserver.dkim = { - vault = { - mount = lib.mkOption { - type = lib.types.str; - }; - mountExpr = lib.mkOption { - type = lib.types.str; - default = "\${ vault_mount.${lib.khscodes.sanitize-terraform-name cfg.dkim.vault.mount}.path }"; - }; - prefixPath = lib.mkOption { - type = lib.types.str; - }; - }; - }; - config = lib.mkIf (cfg.enable) { + config = { khscodes.infrastructure.vault-server-approle.policy = { - "${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/*" = { + "${mount}/data/${prefixPath}/*" = { capabilities = [ "read" ]; }; }; @@ -142,8 +131,8 @@ in lib.lists.map (domain: { name = "${lib.khscodes.sanitize-terraform-name domain}_dkim_rsa"; value = { - mount = cfg.dkim.vault.mountExpr; - name = cfg.dkim.vault.prefixPath + "/${domain}/rsa"; + mount = mountExpr; + name = prefixPath + "/${domain}/rsa"; data_json = '' { "dkim_private_key": ''${ jsonencode(resource.tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_rsa.private_key_pem) } } ''; @@ -154,8 +143,8 @@ in lib.lists.map (domain: { name = "${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519"; value = { - mount = cfg.dkim.vault.mountExpr; - name = cfg.dkim.vault.prefixPath + "/${domain}/ed25519"; + mount = mountExpr; + name = prefixPath + "/${domain}/ed25519"; data_json = '' { "dkim_private_key": ''${ jsonencode(resource.tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519.private_key_pem) } } ''; @@ -169,7 +158,7 @@ in lib.lists.map (domain: [ { contents = '' - {{- with secret "${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/${domain}/rsa" -}} + {{- with secret "${mount}/data/${prefixPath}/${domain}/rsa" -}} {{ .Data.data.dkim_private_key }} {{- end -}} ''; @@ -185,7 +174,7 @@ in # rspamd does not like reading ed25519 keys with begin/end pairs, as they get parsed as # rsa keys contents = '' - {{- with secret "${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/${domain}/ed25519" -}} + {{- with secret "${mount}/data/${prefixPath}/${domain}/ed25519" -}} {{ .Data.data.dkim_private_key }} {{- end -}} ''; diff --git a/nix/modules/nixos/infrastructure/mailserver/dmarc.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dmarc.nix similarity index 82% rename from nix/modules/nixos/infrastructure/mailserver/dmarc.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dmarc.nix index bd5a147..998b8e6 100644 --- a/nix/modules/nixos/infrastructure/mailserver/dmarc.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dmarc.nix @@ -1,9 +1,9 @@ { config, lib, ... }: let - cfg = config.khscodes.infrastructure.mailserver; + cfg = config.khscodes."mx.kaareskovgaard.net"; in { - config = lib.mkIf cfg.enable { + config = { khscodes.infrastructure.provisioning.pre.modules = [ { khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: { diff --git a/nix/modules/nixos/infrastructure/mailserver/ldap.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/ldap.nix similarity index 78% rename from nix/modules/nixos/infrastructure/mailserver/ldap.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/ldap.nix index 2b0ae43..200476b 100644 --- a/nix/modules/nixos/infrastructure/mailserver/ldap.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/ldap.nix @@ -1,26 +1,14 @@ { - lib, - config, pkgs, ... }: let - cfg = config.khscodes.infrastructure.mailserver; + ldapMount = "kanidm"; + ldapPath = "ldap/dovecot"; secretFile = "/run/secret/dovecot/dovecot-ldap.conf.ext"; in { - options.khscodes.infrastructure.mailserver = { - ldap = { - mount = lib.mkOption { - type = lib.types.str; - }; - path = lib.mkOption { - type = lib.types.str; - }; - }; - }; - - config = lib.mkIf cfg.enable { + config = { services.dovecot2.extraConfig = '' passdb { driver = ldap @@ -41,7 +29,7 @@ in khscodes.services.vault-agent.templates = [ { contents = '' - {{- with secret "${cfg.ldap.mount}/data/${cfg.ldap.path}" -}} + {{- with secret "${ldapMount}/data/${ldapPath}" -}} ldap_version = 3 uris = ldaps://login.kaareskovgaard.net @@ -78,7 +66,7 @@ in } ]; khscodes.infrastructure.vault-server-approle.policy = { - "${cfg.ldap.mount}/data/${cfg.ldap.path}" = { + "${ldapMount}/data/${ldapPath}" = { capabilities = [ "read" ]; }; }; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/managesieve.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/managesieve.nix new file mode 100644 index 0000000..7264678 --- /dev/null +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/managesieve.nix @@ -0,0 +1,10 @@ +{ + config = { + services.dovecot2.protocols = [ "sieve" ]; + services.roundcube.extraConfig = '' + $config['plugins'] = [ + 'managesieve', + ]; + ''; + }; +} diff --git a/nix/modules/nixos/infrastructure/mailserver/mta-sts.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/mta-sts.nix similarity index 93% rename from nix/modules/nixos/infrastructure/mailserver/mta-sts.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/mta-sts.nix index 57a4bb6..58ea779 100644 --- a/nix/modules/nixos/infrastructure/mailserver/mta-sts.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/mta-sts.nix @@ -6,7 +6,7 @@ }: let fqdn = config.khscodes.networking.fqdn; - cfg = config.khscodes.infrastructure.mailserver; + cfg = config.khscodes."mx.kaareskovgaard.net"; # Increment this if ever changing mta-sts settings. policyVersion = 2; mtaStsWellKnown = pkgs.writeTextFile { @@ -20,7 +20,7 @@ let }; in { - config = lib.mkIf cfg.enable { + config = { khscodes.services.nginx.virtualHosts = ( lib.listToAttrs ( lib.lists.map (domain: { diff --git a/nix/modules/nixos/infrastructure/mailserver/openid-connect.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix similarity index 55% rename from nix/modules/nixos/infrastructure/mailserver/openid-connect.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix index 120886d..3f5014d 100644 --- a/nix/modules/nixos/infrastructure/mailserver/openid-connect.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix @@ -1,10 +1,15 @@ -{ config, lib, ... }: +{ config, ... }: let - cfg = config.khscodes.infrastructure.mailserver; oauthConfigFile = "/run/secret/dovecot/dovecot-oauth2.conf.ext"; in { - config = lib.mkIf cfg.enable { + config = { + khscodes.infrastructure.kanidm-client-application = { + enable = true; + appName = "dovecot"; + secretOwner = "root"; + perms = "0644"; + }; khscodes.services.vault-agent.templates = [ { contents = '' @@ -23,6 +28,17 @@ in restartUnits = [ "dovecot2.service" ]; } ]; + services.roundcube.extraConfig = '' + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = 'Kanidm'; + $config['oauth_client_id'] = 'dovecot'; + $config['oauth_client_secret'] = file_get_contents("${config.khscodes.infrastructure.kanidm-client-application.secretFile}"); + $config['oauth_auth_uri'] = 'https://login.kaareskovgaard.net/ui/oauth2'; + $config['oauth_token_uri'] = 'https://login.kaareskovgaard.net/oauth2/token'; + $config['oauth_identity_uri'] = 'https://login.kaareskovgaard.net/oauth2/openid/dovecot/userinfo'; + $config['oauth_identity_fields'] = ['preferred_username']; + $config['oauth_scope'] = 'email openid profile'; + ''; services.dovecot2.extraConfig = '' auth_mechanisms = $auth_mechanisms oauthbearer xoauth2 diff --git a/nix/modules/nixos/infrastructure/mailserver/prometheus.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/prometheus.nix similarity index 60% rename from nix/modules/nixos/infrastructure/mailserver/prometheus.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/prometheus.nix index 3318a54..b6e2717 100644 --- a/nix/modules/nixos/infrastructure/mailserver/prometheus.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/prometheus.nix @@ -1,9 +1,5 @@ -{ config, lib, ... }: -let - cfg = config.khscodes.infrastructure.mailserver; -in { - config = lib.mkIf cfg.enable { + config = { services.prometheus.exporters.postfix = { enable = true; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/roundcube.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/roundcube.nix new file mode 100644 index 0000000..8d6ab37 --- /dev/null +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/roundcube.nix @@ -0,0 +1,18 @@ +{ config, ... }: +{ + services.roundcube = { + enable = true; + hostName = "mail.kaareskovgaard.net"; + configureNginx = true; + extraConfig = '' + # starttls needed for authentication, so the fqdn required to match + # the certificate + $config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}"; + $config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}"; + ''; + }; + khscodes.services.nginx = { + enable = true; + virtualHosts."mail.kaareskovgaard.net" = { }; + }; +} diff --git a/nix/modules/nixos/infrastructure/mailserver/spf.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/spf.nix similarity index 78% rename from nix/modules/nixos/infrastructure/mailserver/spf.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/spf.nix index 97b437f..4b4b5c8 100644 --- a/nix/modules/nixos/infrastructure/mailserver/spf.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/spf.nix @@ -1,9 +1,9 @@ { config, lib, ... }: let - cfg = config.khscodes.infrastructure.mailserver; + cfg = config.khscodes."mx.kaareskovgaard.net"; in { - config = lib.mkIf cfg.enable { + config = { khscodes.infrastructure.provisioning.pre.modules = [ { khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: { diff --git a/nix/modules/nixos/infrastructure/mailserver/tls-rpt.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/tls-rpt.nix similarity index 80% rename from nix/modules/nixos/infrastructure/mailserver/tls-rpt.nix rename to nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/tls-rpt.nix index d4418a3..1ffb0a4 100644 --- a/nix/modules/nixos/infrastructure/mailserver/tls-rpt.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/tls-rpt.nix @@ -1,9 +1,9 @@ { config, lib, ... }: let - cfg = config.khscodes.infrastructure.mailserver; + cfg = config.khscodes."mx.kaareskovgaard.net"; in { - config = lib.mkIf cfg.enable { + config = { khscodes.infrastructure.provisioning.pre.modules = [ { khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: { diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/testuser.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/testuser.nix deleted file mode 100644 index daa53e7..0000000 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/testuser.nix +++ /dev/null @@ -1,71 +0,0 @@ -{ - khscodes.infrastructure.vault-server-approle.policy = { - "mail.kaareskovgaard.net/data/users/*" = { - capabilities = [ "read" ]; - }; - }; - khscodes.services.vault-agent.templates = [ - { - contents = '' - {{- with secret "mail.kaareskovgaard.net/data/users/test" -}} - {{ .Data.data.hashed_password }} - {{- end -}} - ''; - destination = "/run/secret/mailserver/users/test.passwd.hash"; - perms = "0600"; - owner = "rspamd"; - group = "rspamd"; - restartUnits = [ - "rspamd.service" - "postfix.service" - ]; - } - ]; - systemd.services.postfix = { - unitConfig = { - ConditionPathExists = [ "/run/secret/mailserver/users/test.passwd.hash" ]; - }; - }; - - systemd.services.rspamd = { - unitConfig = { - ConditionPathExists = [ "/run/secret/mailserver/users/test.passwd.hash" ]; - }; - }; - khscodes.infrastructure.provisioning.pre.modules = [ - ( - { config, ... }: - { - terraform.required_providers.random = { - source = "hashicorp/random"; - version = "3.7.2"; - }; - provider.random = { }; - - resource.random_password.test = { - length = 48; - numeric = true; - lower = true; - upper = true; - special = false; - }; - - resource.vault_kv_secret_v2.test = { - mount = config.khscodes.vault.output.mount."mail.kaareskovgaard.net".path; - name = "users/test"; - data_json = '' - { - "hashed_password": ''${ jsonencode(resource.random_password.test.bcrypt_hash) }, - "password": ''${ jsonencode(resource.random_password.test.result) } - } - ''; - }; - } - ) - ]; - mailserver.loginAccounts = { - "test@agerlin-skovgaard.dk" = { - hashedPasswordFile = "/run/secret/mailserver/users/test.passwd.hash"; - }; - }; -} diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix new file mode 100644 index 0000000..b7920d8 --- /dev/null +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix @@ -0,0 +1,11 @@ +{ + "khs" = { + isLdapAccount = true; + name = "Kaare Agerlin Skovgaard"; + aliases = [ + "kaare@agerlin-skovgaard.dk" + "kaare@agerlinskovgaard.dk" + ]; + quota = "10G"; + }; +}