diff --git a/default.nix b/default.nix index 8461eb1..554f07d 100644 --- a/default.nix +++ b/default.nix @@ -25,7 +25,7 @@ let inherit (lib) literalExpression literalMD - mkDefault + mkChangedOptionModule mkEnableOption mkOption mkOptionType @@ -137,7 +137,7 @@ in description = "Message size limit enforced by Postfix."; }; - loginAccounts = mkOption { + accounts = mkOption { type = types.attrsOf ( types.submodule ( { name, ... }: @@ -145,8 +145,13 @@ in options = { name = mkOption { type = types.str; + default = name; example = "user1@example.com"; - description = "Username"; + readOnly = true; + internal = true; + description = '' + The login username for this account. + ''; }; hashedPassword = mkOption { @@ -154,24 +159,33 @@ in default = null; example = "$y$j9T$vfGrwkAaXCjCEWtVNMQck1$383uIXQmn2z0hnmVAA8kwFQmjNj78.nYbvWeyNLIaP1"; description = '' - The user's hashed password. Use `mkpasswd` as follows + The hashed login password for this account. + Use `mkpasswd` to create password hashes: ``` nix-shell -p mkpasswd --run 'mkpasswd -s' ``` - Warning: this is stored in plaintext in the Nix store! - Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. + :::{note} + This is a convenience option, when your threat model allows + storing hashed secrets in the world-readable Nix store. + + Passing the hash through + {option}`mailserver.accounts..hashedPasswordFile` + allows relying on filesystem discretionary access control as + another security boundary. + ::: ''; }; hashedPasswordFile = mkOption { type = with types; nullOr path; default = null; - example = "/run/keys/user1-passwordhash"; + example = "/run/keys/user1-pw-hash"; description = '' - A file containing the user's hashed password. Use `mkpasswd` as follows + The hashed login password for this account read from a file. + Use `mkpasswd to create password hashes: ``` nix-shell -p mkpasswd --run 'mkpasswd -s' ``` @@ -185,9 +199,13 @@ in inStore = false; }); default = null; - example = "/run/keys/user1-password"; + example = "/run/keys/user1-pw"; description = '' - A file containing the user's plain text password. The value will be hashed at runtime. + The plaintext login password for this account read from a file. + + :::{note} + The password is hashed before it is passed on to Dovecot. + ::: ''; }; @@ -199,9 +217,13 @@ in ]; 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. + List of additional mail addresses (aliases) that get routed to this account. + + :::{admonition} Catch-all with sending permissions + :class: tip + Configure `@example.com` to create a catch-all for this domain + that also allows sending from all addresses. + ::: ''; }; @@ -210,7 +232,7 @@ in example = [ ''/^tom\..*@domain\.com$/'' ]; default = [ ]; description = '' - Same as {option}`mailserver.loginAccounts..aliases` but + Same as {option}`mailserver.accounts..aliases` but using PCRE (Perl compatible regex). ''; }; @@ -224,7 +246,12 @@ in default = [ ]; description = '' For which domains should this account act as a catch all? - Note: Does not allow sending from all addresses of these domains. + + :::{warning} + Does not allow sending from all addresses of these domains. + Use {option}`mailserver.accounts..aliases` if that + is required. + ::: ''; }; @@ -266,9 +293,10 @@ in 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. + + Emails sent to send-only accounts will + be rejected with the reason configured in + {option}`mailserver.accounts..sendOnlyRejectMessage`. ''; }; @@ -276,33 +304,38 @@ in type = 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. + The message returned to the sender for a send-only account. + + See {option}`mailserver.accounts..sendOnly`. ''; }; }; - - config.name = mkDefault name; } ) ); - example = { - user1 = { - hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5"; - }; - user2 = { - hashedPassword = "$y$j9T$hZ.ubq0M897Hw.znxnGG9.$14EJBoOwbwKeWt.W4vpnBPEBZC9mYz4fWI9kOCLoZf4"; - }; - }; + example = lib.literalExpression '' + { + user1 = { + # This password hash leaks into the Nix store + hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5"; + }; + user2 = { + # Hashed password passed as a file + hashedPasswordFile = "/run/keys/user2-pw-hash"; + }; + user3 = { + # Plaintext password file + passwordFile = "/run/keys/user3-pw"; + }; + } + ''; description = '' - The login account of the domain. Every account is mapped to a unix user, - e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as - follows + Attribute set of mail accounts. - ``` - nix-shell -p mkpasswd --run 'mkpasswd -s' - ``` + Each entry defines a mailbox and login credentials, where the attribute + name is used as the login username and optionally routed mail address. + + Use `mkpasswd` to generate password hashes. ''; default = { }; }; @@ -652,13 +685,13 @@ in aliases = mkOption { type = let - loginAccount = mkOptionType { + account = mkOptionType { name = "Login Account"; - check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts); + check = account: builtins.elem account (builtins.attrNames cfg.accounts); }; in with types; - attrsOf (either loginAccount (nonEmptyListOf loginAccount)); + attrsOf (either account (nonEmptyListOf account)); example = { "postmaster@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com"; @@ -751,53 +784,80 @@ in default = [ ]; }; - vmailUID = mkOption { - type = types.int; - default = 5000; - description = '' - The unix UID of the virtual mail user. Be mindful that if this is - changed, you will need to manually adjust the permissions of - `mailDirectory`. - ''; - }; + storage = { + path = mkOption { + type = types.path; + default = "/var/vmail"; + description = '' + Path on disk where mail home directories are stored. + ''; + }; - vmailUserName = mkOption { - type = types.str; - default = "virtualMail"; - description = '' - The user name and group name of the user that owns the directory where all - the mail is stored. - ''; - }; + directoryLayout = mkOption { + type = types.enum [ + "fs" + "maildir++" + ]; + default = "maildir++"; + description = '' + Sets whether dovecot should organize mail in subdirectories: - vmailGroupName = mkOption { - type = types.str; - default = "virtualMail"; - description = '' - The user name and group name of the user that owns the directory where all - the mail is stored. - ''; - }; + - /var/vmail/example.com/user/.folder.subfolder/ (Maildir++ layout) + - /var/vmail/example.com/user/folder/subfolder/ (FS layout) - mailDirectory = mkOption { - type = types.path; - default = "/var/vmail"; - description = '' - Where to store the mail. - ''; - }; + See + See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details. + ''; + }; - useFsLayout = mkOption { - type = types.bool; - default = false; - description = '' - Sets whether dovecot should organize mail in subdirectories: + uid = mkOption { + type = types.ints.positive; + default = 5000; + description = '' + The user id assigned to the vmail user. - - /var/vmail/example.com/user/.folder.subfolder/ (default layout) - - /var/vmail/example.com/user/folder/subfolder/ (FS layout) + This user owns the mail storage files and directories and is used by + services accessing the mail store. - See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details. - ''; + :::{warning} + If you change this value you also need to manually adjust the + permissions of your :option:`mailserver.storage.path`. + ::: + ''; + }; + + owner = mkOption { + type = types.str; + default = "virtualMail"; + description = '' + The name of the user that owns the :option:`mailserver.storage.path`. + ''; + }; + + gid = mkOption { + type = types.ints.positive; + default = 5000; + description = '' + The group id of the primary group of the vmail user. + + This group owns the mail storage directories. Access can be delegated + to other users via group membership. + + :::{warning} + If you change this value you also need to manually adjust the + permissions of your :option:`mailserver.storage.path`. + ::: + ''; + }; + + group = mkOption { + type = types.str; + default = "virtualMail"; + description = '' + The primary group name of the user that owns the + :option:`mailserver.storage.path`. + ''; + }; }; useUTF8FolderNames = mkOption { @@ -1481,8 +1541,8 @@ in locations = mkOption { type = types.listOf types.path; - default = [ cfg.mailDirectory ]; - defaultText = literalExpression "[ config.mailserver.mailDirectory ]"; + default = [ cfg.storage.path ]; + defaultText = literalExpression "[ config.mailserver.storage.path ]"; description = "The locations that are to be backed up by borg."; }; @@ -1682,5 +1742,13 @@ in [ "mailserver" "ldap" "attributes" "mail" ] ) (mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ]) + (mkRenamedOptionModule [ "mailserver" "loginAccounts" ] [ "mailserver" "accounts" ]) + (mkRenamedOptionModule [ "mailserver" "vmailUID" ] [ "mailserver" "storage" "uid" ]) + (mkRenamedOptionModule [ "mailserver" "vmailUserName" ] [ "mailserver" "storage" "owner" ]) + (mkRenamedOptionModule [ "mailserver" "vmailGroupName" ] [ "mailserver" "storage" "group" ]) + (mkRenamedOptionModule [ "mailserver" "mailDirectory" ] [ "mailserver" "storage" "path" ]) + (mkChangedOptionModule [ "mailserver" "useFSLayout" ] [ "mailserver" "storage" "directoryLayout" ] ( + config: if config.mailserver.useFSLayout then "fs" else "maildir++" + )) ]; } diff --git a/docs/backup-guide.rst b/docs/backup-guide.rst index caff6bf..3a7306a 100644 --- a/docs/backup-guide.rst +++ b/docs/backup-guide.rst @@ -5,14 +5,13 @@ First off you should have a backup of your ``configuration.nix`` file where you have the server config (but that is already in a git repository right?) -Next you need to backup ``/var/vmail`` or whatever you have specified -for the option ``mailDirectory``. This is where all the mails reside. -Good options are a cron job with ``rsync`` or ``scp``. But really -anything works, as it is simply a folder with plenty of files in it. If -your backup solution does not preserve the owner of the files don’t -forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them -back (or whatever you specified as ``vmailUserName``, and -``vmailGroupName``). +Next you need to backup ``/var/vmail`` or whatever you have specified for the +option :option:`mailserver.storage.path`. This is where all the mails reside. +Good options are a cron job with ``rsync`` or ``scp``. But really anything +works, as it is simply a folder with plenty of files in it. If your backup +solution does not preserve the owner of the files don’t forget to ``chown`` them +to ``virtualMail:virtualMail`` if you copy them back (or whatever you specified +as :option:`mailserver.storage.owner`, and :option:`mailserver.storage.group`). If you enabled ``enableManageSieve`` then you also may want to backup ``/var/sieve`` or whatever you have specified as ``sieveDirectory``. diff --git a/docs/faq.rst b/docs/faq.rst index ef306de..674ee0a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -13,7 +13,7 @@ domain ``example.com`` and send mails with any address of this domain: .. code:: nix - mailserver.loginAccounts = { + mailserver.accounts = { "user@example.com" = { aliases = [ "@example.com" ]; }; diff --git a/docs/migrations.rst b/docs/migrations.rst index 7807f95..1b03e9e 100644 --- a/docs/migrations.rst +++ b/docs/migrations.rst @@ -40,18 +40,18 @@ best practices to mailserver management. systemctl stop dovecot.service -3. Create a backup or snapshot of your :option:`mailserver.mailDirectory`, so +3. Create a backup or snapshot of your :option:`mailserver.storage.path`, so you can restore should anything go wrong. 4. Run the migration script and pass the required arguments to enable LDAP lookups: - The script should be run under the user who owns the :option:`mailserver.mailDirectory`. + The script should be run under the user who owns the :option:`mailserver.storage.path`. If run as root it will automatically switch into the appropriate user by itself. The script will not modify your data unless called with ``--execute``. The migration script finds all Dovecot home directories in - ``/var/vmail/ldap/`` (or any other :option:`mailserver.mailDirectory`), + ``/var/vmail/ldap/`` (or any other :option:`mailserver.storage.path`), for example that of bob at ``/var/vmail/ldap/bob@example.com``. It then takes ``bob@example.com`` and queries the LDAP server for ``mail=bob@example.com`` to retrieve the UUID attribute. Finally diff --git a/docs/radicale.nix b/docs/radicale.nix index e740fb7..6ccecc7 100644 --- a/docs/radicale.nix +++ b/docs/radicale.nix @@ -12,7 +12,7 @@ let mapAttrsToList ; - mailAccounts = config.mailserver.loginAccounts; + mailAccounts = config.mailserver.accounts; htpasswd = pkgs.writeText "radicale.users" ( concatStrings (flip mapAttrsToList mailAccounts (mail: user: "${mail}+:${user.hashedPassword}\n")) ); diff --git a/docs/radicale.rst b/docs/radicale.rst index 244a41e..f594b8e 100644 --- a/docs/radicale.rst +++ b/docs/radicale.rst @@ -10,7 +10,7 @@ Limitations Radicale since the 3.x release (introduced in NixOS 20.09) does not support traditional crypt() password hashes any longer. To establish access for -existing :option:`mailserver.loginAccounts`, the hashing method used +existing :option:`mailserver.accounts`, the hashing method used for ``hashedPassword`` needs to be compatible with one of the available `htpasswd_encryption`_ methods. Such hashes can for example be created using diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 9ee6ccc..e0c2bd8 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -21,7 +21,7 @@ NixOS 26.05 is an alternative to hashed passwords that integrates well with workflows established by `agenix`_/`sops-nix`_ that instead rely on encryption. This option prevents files from leaking in to the Nix store. - See :option:`mailserver.loginAccounts..passwordFile`. + See :option:`mailserver.accounts..passwordFile`. - LDAP setups require a migration of Dovecot home directories to `UUID based home directories`_. The exact UUID attribute can be customized through :option:`mailserver.ldap.attributes.uuid`. diff --git a/docs/setup-example.nix b/docs/setup-example.nix index aba25ed..66a8242 100644 --- a/docs/setup-example.nix +++ b/docs/setup-example.nix @@ -38,7 +38,7 @@ # A list of all login accounts. To create the password hashes, use # nix-shell -p mkpasswd --run 'mkpasswd -s' - loginAccounts = { + accounts = { "user1@example.com" = { # Reads the password hash from a file on the server hashedPasswordFile = "/a/file/containing/a/hashed/password"; diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index a3c2428..d3f002b 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -98,22 +98,20 @@ in ) config.mailserver.dkim.domains ) ) - ++ - lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail") - [ - { - assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2; - message = '' - Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. - Remediation: - - Stop the `dovecot.service` - - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - - Increase the `stateVersion` to 2. + ++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.storage.path != "/var/vmail") [ + { + assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2; + message = '' + Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.storage.path`. + Remediation: + - Stop the `dovecot.service` + - Move `/var/vmail/ldap` below your `mailserver.storage.path` + - Increase the `stateVersion` to 2. - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information. - ''; - } - ] + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information. + ''; + } + ] ++ [ { assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 3; diff --git a/mail-server/common.nix b/mail-server/common.nix index 3ca5c57..95f317b 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -51,12 +51,12 @@ rec { builtins.toString (mkHashFile name value.hashedPassword) else value.passwordFile - ) cfg.loginAccounts; + ) cfg.accounts; # Collect accounts with plain text passwords that require hashing - accountsWithPlaintextPasswordFiles = lib.filter ( - name: cfg.loginAccounts.${name}.passwordFile != null - ) (builtins.attrNames cfg.loginAccounts); + accountsWithPlaintextPasswordFiles = lib.filter (name: cfg.accounts.${name}.passwordFile != null) ( + builtins.attrNames cfg.accounts + ); # Appends the LDAP bind password to files to avoid writing this # password into the Nix store. diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 9c89c4e..ab08b6e 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -50,7 +50,7 @@ let }) attrs ); - maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; + maildirLayoutAppendix = lib.optionalString (cfg.storage.directoryLayout == "fs") ":LAYOUT=fs"; maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; # https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory @@ -79,7 +79,7 @@ let scope = ${mkLdapSearchScope cfg.ldap.scope} user_attrs = \ ${ldapUuidAttribute}=${ldapUuidAttribute}, \ - =home=${cfg.mailDirectory}/ldap/%{ldap:${ldapUuidAttribute}}, \ + =home=${cfg.storage.path}/ldap/%{ldap:${ldapUuidAttribute}}, \ =mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${ lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{ldap:${ldapUuidAttribute}}" } @@ -115,7 +115,7 @@ let umask 077 for f in ${ - builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts) + builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.accounts) }; do if [ ! -f "$f" ]; then echo "Expected password hash file $f does not exist!" @@ -131,7 +131,7 @@ let "${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' pkgs.dovecot "doveadm"} pw)"}::::::" else "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" - ) cfg.loginAccounts + ) cfg.accounts )} EOF @@ -141,7 +141,7 @@ let name: value: "${name}:::::::" + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" - ) cfg.loginAccounts + ) cfg.accounts )} EOF ''; @@ -228,8 +228,8 @@ in enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl; enablePAM = false; enableQuota = true; - mailGroup = cfg.vmailGroupName; - mailUser = cfg.vmailUserName; + mailGroup = cfg.storage.group; + mailUser = cfg.storage.owner; mailLocation = dovecotMaildir; sslServerCert = x509CertificateFile; sslServerKey = x509PrivateKeyFile; @@ -371,7 +371,7 @@ in mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} } - mail_access_groups = ${cfg.vmailGroupName} + mail_access_groups = ${cfg.storage.group} # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 ssl = required @@ -431,9 +431,9 @@ in driver = passwd-file args = ${userdbFile} default_fields = \ - home=${cfg.mailDirectory}/%{domain}/%{username} \ - uid=${builtins.toString cfg.vmailUID} \ - gid=${builtins.toString cfg.vmailUID} + home=${cfg.storage.path}/%{domain}/%{username} \ + uid=${builtins.toString cfg.storage.uid} \ + gid=${builtins.toString cfg.storage.uid} } ${lib.optionalString cfg.ldap.enable '' @@ -446,8 +446,8 @@ in driver = ldap args = ${ldapConfFile} override_fields = \ - uid=${toString cfg.vmailUID} \ - gid=${toString cfg.vmailUID} + uid=${toString cfg.storage.uid} \ + gid=${toString cfg.storage.uid} } ''} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 70ae68e..52dc45a 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -51,7 +51,7 @@ let to = name; in map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name) - ) cfg.loginAccounts + ) cfg.accounts ) ); regex_valiases_postfix = mergeLookupTables ( @@ -62,7 +62,7 @@ let to = name; in map (from: { "${from}" = to; }) value.aliasesRegexp - ) cfg.loginAccounts + ) cfg.accounts ) ); @@ -75,7 +75,7 @@ let to = name; in map (from: { "@${from}" = to; }) value.catchAll - ) cfg.loginAccounts + ) cfg.accounts ) ); @@ -127,7 +127,7 @@ let # denied_recipients_postfix :: [ String ] denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") ( - lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts) + lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.accounts) ); denied_recipients_file = builtins.toFile "denied_recipients" ( lib.concatStringsSep "\n" denied_recipients_postfix diff --git a/mail-server/rsnapshot.nix b/mail-server/rsnapshot.nix index f01ff8d..e898802 100644 --- a/mail-server/rsnapshot.nix +++ b/mail-server/rsnapshot.nix @@ -61,7 +61,7 @@ in retain hourly ${toString cfg.backup.retain.hourly} retain daily ${toString cfg.backup.retain.daily} retain weekly ${toString cfg.backup.retain.weekly} - backup ${cfg.mailDirectory}/ localhost/ + backup ${cfg.storage.path}/ localhost/ ''; }; }; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index b401d6b..21b0c94 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -46,7 +46,7 @@ in preStart = let directories = lib.strings.escapeShellArgs ( - [ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir + [ cfg.storage.path ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir ); in '' @@ -55,7 +55,7 @@ in # Prevent world-readable paths, even temporarily. umask 007 mkdir -p ${directories} - chgrp "${cfg.vmailGroupName}" ${directories} + chgrp "${cfg.storage.group}" ${directories} chmod 02770 ${directories} ''; }; diff --git a/mail-server/users.nix b/mail-server/users.nix index 9863066..163943e 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -16,32 +16,13 @@ { config, - options, pkgs, lib, ... }: -with (import ./common.nix { - inherit - config - options - lib - pkgs - ; -}); - -with config.mailserver; - let - vmail_user = { - name = vmailUserName; - isSystemUser = true; - uid = vmailUID; - home = mailDirectory; - createHome = true; - group = vmailGroupName; - }; + cfg = config.mailserver; virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" @@ -55,10 +36,10 @@ let umask 007 # Create directory to store user sieve scripts if it doesn't exist - if (! test -d "${sieveDirectory}"); then - mkdir "${sieveDirectory}" - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}" - chmod 770 "${sieveDirectory}" + if (! test -d "${cfg.sieveDirectory}"); then + mkdir "${cfg.sieveDirectory}" + chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}" + chmod 770 "${cfg.sieveDirectory}" fi # Copy user's sieve script to the correct location (if it exists). If it @@ -67,30 +48,30 @@ let { name, sieveScript }: if lib.isString sieveScript then '' - if (! test -d "${sieveDirectory}/${name}"); then - mkdir -p "${sieveDirectory}/${name}" - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}" - chmod 770 "${sieveDirectory}/${name}" + if (! test -d "${cfg.sieveDirectory}/${name}"); then + mkdir -p "${cfg.sieveDirectory}/${name}" + chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}" + chmod 770 "${cfg.sieveDirectory}/${name}" fi - cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve" + cat << 'EOF' > "${cfg.sieveDirectory}/${name}/default.sieve" ${sieveScript} EOF - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve" + chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}/default.sieve" '' else '' - if (test -f "${sieveDirectory}/${name}/default.sieve"); then - rm "${sieveDirectory}/${name}/default.sieve" + if (test -f "${cfg.sieveDirectory}/${name}/default.sieve"); then + rm "${cfg.sieveDirectory}/${name}/default.sieve" fi - if (test -f "${sieveDirectory}/${name}.svbin"); then - rm "${sieveDirectory}/${name}/default.svbin" + if (test -f "${cfg.sieveDirectory}/${name}.svbin"); then + rm "${cfg.sieveDirectory}/${name}/default.svbin" fi '' - ) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))} + ) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues cfg.accounts))} ''; in { - config = lib.mkIf enable { + config = lib.mkIf cfg.enable { # assert that all accounts provide a password assertions = map (acct: { assertion = @@ -102,27 +83,29 @@ in ] ) == 1; message = "Login account ${acct.name} must provide exactly one of password file, hashed password, or hashed password file"; - }) (lib.attrValues loginAccounts); + }) (lib.attrValues cfg.accounts); # warn for accounts that specify both password and file warnings = map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used") ( lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) ( - lib.attrValues loginAccounts + lib.attrValues cfg.accounts ) ); - # set the vmail gid to a specific value - users.groups = { - "${vmailGroupName}" = { - gid = vmailUID; - }; + users.groups.${cfg.storage.group} = { + inherit (cfg.storage) gid; }; - - # define all users - users.users = { - "${vmail_user.name}" = lib.mkForce vmail_user; + users.users.${cfg.storage.owner} = lib.mkForce { + inherit (cfg.storage) + group + uid + ; + name = cfg.storage.owner; + isSystemUser = true; + home = cfg.storage.path; + createHome = true; }; systemd.services.activate-virtual-mail-users = { diff --git a/scripts/generate-options.py b/scripts/generate-options.py index c195504..dfba41d 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -25,8 +25,9 @@ f = open(sys.argv[1]) options = json.load(f) groups = [ - "mailserver.loginAccounts", + "mailserver.accounts", "mailserver.x509", + "mailserver.storage", "mailserver.dkim", "mailserver.srs", "mailserver.dmarcReporting", diff --git a/tests/clamav.nix b/tests/clamav.nix index 900dd32..2600416 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -77,7 +77,7 @@ ]; virusScanning = true; - loginAccounts = { + accounts = { "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; aliases = [ "postmaster@example.com" ]; diff --git a/tests/external.nix b/tests/external.nix index a149144..e47013c 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -66,7 +66,7 @@ }; dmarcReporting.enable = true; - loginAccounts = { + accounts = { "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; aliases = [ "postmaster@example.com" ]; diff --git a/tests/internal.nix b/tests/internal.nix index a3cbfb8..e6c1b17 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -87,7 +87,7 @@ in ]; localDnsResolver = false; - loginAccounts = { + accounts = { "user1@example.com" = { hashedPasswordFile = hashedPasswordFile; }; @@ -109,8 +109,11 @@ in "user2@example.com" = "user1@example.com"; }; - vmailGroupName = "vmail"; - vmailUID = 5000; + storage = { + gid = 5000; + group = "vmail"; + }; + indexDir = "/var/lib/dovecot/indices"; enableImap = false; @@ -218,7 +221,7 @@ in with subtest("Check dovecot maildir and index locations"): # If these paths change we need a migration - machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.mailDirectory}/example.com/user1") + machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.storage.path}/example.com/user1") machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'") with subtest("mail to send only accounts is rejected"): diff --git a/tests/ldap.nix b/tests/ldap.nix index 2999bd8..57e90aa 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -125,7 +125,7 @@ in fqdn = "mail.example.com"; domains = [ "example.com" ]; localDnsResolver = false; - mailDirectory = "/var/lib/dovecot/vmail"; + storage.path = "/var/lib/dovecot/vmail"; indexDir = "/var/lib/dovecot/indices"; aliases = { @@ -133,7 +133,7 @@ in "frank@example.com" = "mallory@example.com"; }; - loginAccounts = { + accounts = { # Colliding local account takes precedence over LDAP account with # same address. "carol@example.com" = { @@ -214,10 +214,10 @@ in machine.succeed("doveadm user -u alice") machine.log(machine.succeed("doveadm user -u bob")) - machine.succeed("doveadm user -f uid bob@example.com | grep ${toString nodes.machine.mailserver.vmailUID}") - machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.vmailUID}") + machine.succeed("doveadm user -f uid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}") + machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}") - machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092") + machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.storage.path}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092") machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092'") with subtest("Files containing secrets are only readable by root"): diff --git a/tests/multiple.nix b/tests/multiple.nix index 3f9b5db..d627edd 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -35,7 +35,7 @@ let fqdn = "mail.${domain}"; domains = [ domain ]; localDnsResolver = false; - loginAccounts = { + accounts = { "user@${domain}" = { hashedPasswordFile = hashPassword "password"; };