Group storage and vmail user options at mailserver.storage

Create a nicer option structure that deals with the mail storage and its
owner, uid, group and gid. Also includes the directory layout as a
property of how mails are stored..
This commit is contained in:
Martin Weinelt
2026-03-20 01:49:25 +01:00
parent 6826d11c58
commit e13736db67
11 changed files with 139 additions and 110 deletions
+71 -36
View File
@@ -25,6 +25,7 @@ let
inherit (lib) inherit (lib)
literalExpression literalExpression
literalMD literalMD
mkChangedOptionModule
mkEnableOption mkEnableOption
mkOption mkOption
mkOptionType mkOptionType
@@ -783,55 +784,82 @@ in
default = [ ]; default = [ ];
}; };
vmailUID = mkOption { storage = {
type = types.int; path = mkOption {
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`.
'';
};
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.
'';
};
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.
'';
};
mailDirectory = mkOption {
type = types.path; type = types.path;
default = "/var/vmail"; default = "/var/vmail";
description = '' description = ''
Where to store the mail. Path on disk where mail home directories are stored.
''; '';
}; };
useFsLayout = mkOption { directoryLayout = mkOption {
type = types.bool; type = types.enum [
default = false; "fs"
"maildir++"
];
default = "maildir++";
description = '' description = ''
Sets whether dovecot should organize mail in subdirectories: Sets whether dovecot should organize mail in subdirectories:
- /var/vmail/example.com/user/.folder.subfolder/ (default layout) - /var/vmail/example.com/user/.folder.subfolder/ (Maildir++ layout)
- /var/vmail/example.com/user/folder/subfolder/ (FS layout) - /var/vmail/example.com/user/folder/subfolder/ (FS layout)
See <https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#directory-layout>
See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details. See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details.
''; '';
}; };
uid = mkOption {
type = types.ints.positive;
default = 5000;
description = ''
The user id assigned to the vmail user.
This user owns the mail storage files and directories and is used by
services accessing the mail store.
:::{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 { useUTF8FolderNames = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@@ -1513,8 +1541,8 @@ in
locations = mkOption { locations = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
default = [ cfg.mailDirectory ]; default = [ cfg.storage.path ];
defaultText = literalExpression "[ config.mailserver.mailDirectory ]"; defaultText = literalExpression "[ config.mailserver.storage.path ]";
description = "The locations that are to be backed up by borg."; description = "The locations that are to be backed up by borg.";
}; };
@@ -1715,5 +1743,12 @@ in
) )
(mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ]) (mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ])
(mkRenamedOptionModule [ "mailserver" "loginAccounts" ] [ "mailserver" "accounts" ]) (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++"
))
]; ];
} }
+7 -8
View File
@@ -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 where you have the server config (but that is already in a git
repository right?) repository right?)
Next you need to backup ``/var/vmail`` or whatever you have specified Next you need to backup ``/var/vmail`` or whatever you have specified for the
for the option ``mailDirectory``. This is where all the mails reside. option :option:`mailserver.storage.path`. This is where all the mails reside.
Good options are a cron job with ``rsync`` or ``scp``. But really Good options are a cron job with ``rsync`` or ``scp``. But really anything
anything works, as it is simply a folder with plenty of files in it. If works, as it is simply a folder with plenty of files in it. If your backup
your backup solution does not preserve the owner of the files dont solution does not preserve the owner of the files dont forget to ``chown`` them
forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them to ``virtualMail:virtualMail`` if you copy them back (or whatever you specified
back (or whatever you specified as ``vmailUserName``, and as :option:`mailserver.storage.owner`, and :option:`mailserver.storage.group`).
``vmailGroupName``).
If you enabled ``enableManageSieve`` then you also may want to backup If you enabled ``enableManageSieve`` then you also may want to backup
``/var/sieve`` or whatever you have specified as ``sieveDirectory``. ``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
+3 -3
View File
@@ -40,18 +40,18 @@ best practices to mailserver management.
systemctl stop dovecot.service 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. you can restore should anything go wrong.
4. Run the migration script and pass the required arguments to enable LDAP lookups: 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. 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 script will not modify your data unless called with ``--execute``.
The migration script finds all Dovecot home directories in 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``. for example that of bob at ``/var/vmail/ldap/bob@example.com``.
It then takes ``bob@example.com`` and queries the LDAP server for It then takes ``bob@example.com`` and queries the LDAP server for
``mail=bob@example.com`` to retrieve the UUID attribute. Finally ``mail=bob@example.com`` to retrieve the UUID attribute. Finally
+3 -5
View File
@@ -98,16 +98,14 @@ in
) config.mailserver.dkim.domains ) config.mailserver.dkim.domains
) )
) )
++ ++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.storage.path != "/var/vmail") [
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
[
{ {
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2; assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
message = '' message = ''
Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.storage.path`.
Remediation: Remediation:
- Stop the `dovecot.service` - Stop the `dovecot.service`
- Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - Move `/var/vmail/ldap` below your `mailserver.storage.path`
- Increase the `stateVersion` to 2. - 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.
+10 -10
View File
@@ -50,7 +50,7 @@ let
}) attrs }) attrs
); );
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirLayoutAppendix = lib.optionalString (cfg.storage.directoryLayout == "fs") ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; 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 # 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} scope = ${mkLdapSearchScope cfg.ldap.scope}
user_attrs = \ user_attrs = \
${ldapUuidAttribute}=${ldapUuidAttribute}, \ ${ldapUuidAttribute}=${ldapUuidAttribute}, \
=home=${cfg.mailDirectory}/ldap/%{ldap:${ldapUuidAttribute}}, \ =home=${cfg.storage.path}/ldap/%{ldap:${ldapUuidAttribute}}, \
=mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${ =mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{ldap:${ldapUuidAttribute}}" lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{ldap:${ldapUuidAttribute}}"
} }
@@ -228,8 +228,8 @@ in
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl; enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
enablePAM = false; enablePAM = false;
enableQuota = true; enableQuota = true;
mailGroup = cfg.vmailGroupName; mailGroup = cfg.storage.group;
mailUser = cfg.vmailUserName; mailUser = cfg.storage.owner;
mailLocation = dovecotMaildir; mailLocation = dovecotMaildir;
sslServerCert = x509CertificateFile; sslServerCert = x509CertificateFile;
sslServerKey = x509PrivateKeyFile; sslServerKey = x509PrivateKeyFile;
@@ -371,7 +371,7 @@ in
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} 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 # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
ssl = required ssl = required
@@ -431,9 +431,9 @@ in
driver = passwd-file driver = passwd-file
args = ${userdbFile} args = ${userdbFile}
default_fields = \ default_fields = \
home=${cfg.mailDirectory}/%{domain}/%{username} \ home=${cfg.storage.path}/%{domain}/%{username} \
uid=${builtins.toString cfg.vmailUID} \ uid=${builtins.toString cfg.storage.uid} \
gid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.storage.uid}
} }
${lib.optionalString cfg.ldap.enable '' ${lib.optionalString cfg.ldap.enable ''
@@ -446,8 +446,8 @@ in
driver = ldap driver = ldap
args = ${ldapConfFile} args = ${ldapConfFile}
override_fields = \ override_fields = \
uid=${toString cfg.vmailUID} \ uid=${toString cfg.storage.uid} \
gid=${toString cfg.vmailUID} gid=${toString cfg.storage.uid}
} }
''} ''}
+1 -1
View File
@@ -61,7 +61,7 @@ in
retain hourly ${toString cfg.backup.retain.hourly} retain hourly ${toString cfg.backup.retain.hourly}
retain daily ${toString cfg.backup.retain.daily} retain daily ${toString cfg.backup.retain.daily}
retain weekly ${toString cfg.backup.retain.weekly} retain weekly ${toString cfg.backup.retain.weekly}
backup ${cfg.mailDirectory}/ localhost/ backup ${cfg.storage.path}/ localhost/
''; '';
}; };
}; };
+2 -2
View File
@@ -46,7 +46,7 @@ in
preStart = preStart =
let let
directories = lib.strings.escapeShellArgs ( directories = lib.strings.escapeShellArgs (
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir [ cfg.storage.path ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
); );
in in
'' ''
@@ -55,7 +55,7 @@ in
# Prevent world-readable paths, even temporarily. # Prevent world-readable paths, even temporarily.
umask 007 umask 007
mkdir -p ${directories} mkdir -p ${directories}
chgrp "${cfg.vmailGroupName}" ${directories} chgrp "${cfg.storage.group}" ${directories}
chmod 02770 ${directories} chmod 02770 ${directories}
''; '';
}; };
+14 -21
View File
@@ -34,15 +34,6 @@ with (import ./common.nix {
let let
cfg = config.mailserver; cfg = config.mailserver;
vmail_user = {
name = cfg.vmailUserName;
isSystemUser = true;
uid = cfg.vmailUID;
home = cfg.mailDirectory;
createHome = true;
group = cfg.vmailGroupName;
};
virtualMailUsersActivationScript = virtualMailUsersActivationScript =
pkgs.writeScript "activate-virtual-mail-users" pkgs.writeScript "activate-virtual-mail-users"
# bash # bash
@@ -57,7 +48,7 @@ let
# Create directory to store user sieve scripts if it doesn't exist # Create directory to store user sieve scripts if it doesn't exist
if (! test -d "${cfg.sieveDirectory}"); then if (! test -d "${cfg.sieveDirectory}"); then
mkdir "${cfg.sieveDirectory}" mkdir "${cfg.sieveDirectory}"
chown "${cfg.vmailUserName}:${cfg.vmailGroupName}" "${cfg.sieveDirectory}" chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}"
chmod 770 "${cfg.sieveDirectory}" chmod 770 "${cfg.sieveDirectory}"
fi fi
@@ -69,13 +60,13 @@ let
'' ''
if (! test -d "${cfg.sieveDirectory}/${name}"); then if (! test -d "${cfg.sieveDirectory}/${name}"); then
mkdir -p "${cfg.sieveDirectory}/${name}" 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}" chmod 770 "${cfg.sieveDirectory}/${name}"
fi fi
cat << 'EOF' > "${cfg.sieveDirectory}/${name}/default.sieve" cat << 'EOF' > "${cfg.sieveDirectory}/${name}/default.sieve"
${sieveScript} ${sieveScript}
EOF EOF
chown "${cfg.vmailUserName}:${cfg.vmailGroupName}" "${cfg.sieveDirectory}/${name}/default.sieve" chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}/default.sieve"
'' ''
else else
'' ''
@@ -113,16 +104,18 @@ in
) )
); );
# set the vmail gid to a specific value users.groups.${cfg.storage.group} = {
users.groups = { inherit (cfg.storage) gid;
"${cfg.vmailGroupName}" = {
gid = cfg.vmailUID;
}; };
}; users.users.${cfg.storage.owner} = lib.mkForce {
inherit (cfg.storage)
# define all users group
users.users = { uid
"${vmail_user.name}" = lib.mkForce vmail_user; ;
name = cfg.storage.owner;
isSystemUser = true;
home = cfg.storage.path;
createHome = true;
}; };
systemd.services.activate-virtual-mail-users = { systemd.services.activate-virtual-mail-users = {
+1
View File
@@ -27,6 +27,7 @@ options = json.load(f)
groups = [ groups = [
"mailserver.accounts", "mailserver.accounts",
"mailserver.x509", "mailserver.x509",
"mailserver.storage",
"mailserver.dkim", "mailserver.dkim",
"mailserver.srs", "mailserver.srs",
"mailserver.dmarcReporting", "mailserver.dmarcReporting",
+6 -3
View File
@@ -109,8 +109,11 @@ in
"user2@example.com" = "user1@example.com"; "user2@example.com" = "user1@example.com";
}; };
vmailGroupName = "vmail"; storage = {
vmailUID = 5000; gid = 5000;
group = "vmail";
};
indexDir = "/var/lib/dovecot/indices"; indexDir = "/var/lib/dovecot/indices";
enableImap = false; enableImap = false;
@@ -218,7 +221,7 @@ in
with subtest("Check dovecot maildir and index locations"): with subtest("Check dovecot maildir and index locations"):
# If these paths change we need a migration # 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'") 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"): with subtest("mail to send only accounts is rejected"):
+4 -4
View File
@@ -125,7 +125,7 @@ in
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" ]; domains = [ "example.com" ];
localDnsResolver = false; localDnsResolver = false;
mailDirectory = "/var/lib/dovecot/vmail"; storage.path = "/var/lib/dovecot/vmail";
indexDir = "/var/lib/dovecot/indices"; indexDir = "/var/lib/dovecot/indices";
aliases = { aliases = {
@@ -214,10 +214,10 @@ in
machine.succeed("doveadm user -u alice") machine.succeed("doveadm user -u alice")
machine.log(machine.succeed("doveadm user -u bob")) 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 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.vmailUID}") 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'") 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"): with subtest("Files containing secrets are only readable by root"):