diff --git a/default.nix b/default.nix index 575cc6c..554f07d 100644 --- a/default.nix +++ b/default.nix @@ -25,6 +25,7 @@ let inherit (lib) literalExpression literalMD + mkChangedOptionModule mkEnableOption mkOption mkOptionType @@ -783,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 { @@ -1513,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."; }; @@ -1715,5 +1743,12 @@ in ) (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/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/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/dovecot.nix b/mail-server/dovecot.nix index 83c4859..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}}" } @@ -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/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 b660da3..39fa055 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -34,15 +34,6 @@ with (import ./common.nix { let cfg = config.mailserver; - vmail_user = { - name = cfg.vmailUserName; - isSystemUser = true; - uid = cfg.vmailUID; - home = cfg.mailDirectory; - createHome = true; - group = cfg.vmailGroupName; - }; - virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" # bash @@ -57,7 +48,7 @@ let # Create directory to store user sieve scripts if it doesn't exist if (! test -d "${cfg.sieveDirectory}"); then mkdir "${cfg.sieveDirectory}" - chown "${cfg.vmailUserName}:${cfg.vmailGroupName}" "${cfg.sieveDirectory}" + chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}" chmod 770 "${cfg.sieveDirectory}" fi @@ -69,13 +60,13 @@ let '' if (! test -d "${cfg.sieveDirectory}/${name}"); then mkdir -p "${cfg.sieveDirectory}/${name}" - chown "${cfg.vmailUserName}:${cfg.vmailGroupName}" "${cfg.sieveDirectory}/${name}" + chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}" chmod 770 "${cfg.sieveDirectory}/${name}" fi cat << 'EOF' > "${cfg.sieveDirectory}/${name}/default.sieve" ${sieveScript} EOF - chown "${cfg.vmailUserName}:${cfg.vmailGroupName}" "${cfg.sieveDirectory}/${name}/default.sieve" + chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}/default.sieve" '' else '' @@ -113,16 +104,18 @@ in ) ); - # set the vmail gid to a specific value - users.groups = { - "${cfg.vmailGroupName}" = { - gid = cfg.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 f6ea83c..dfba41d 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -27,6 +27,7 @@ options = json.load(f) groups = [ "mailserver.accounts", "mailserver.x509", + "mailserver.storage", "mailserver.dkim", "mailserver.srs", "mailserver.dmarcReporting", diff --git a/tests/internal.nix b/tests/internal.nix index 15ada06..e6c1b17 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -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 fd9711f..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 = { @@ -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"):