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)
literalExpression
literalMD
mkChangedOptionModule
mkEnableOption
mkOption
mkOptionType
@@ -783,55 +784,82 @@ 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`.
'';
};
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 {
storage = {
path = mkOption {
type = types.path;
default = "/var/vmail";
description = ''
Where to store the mail.
Path on disk where mail home directories are stored.
'';
};
useFsLayout = mkOption {
type = types.bool;
default = false;
directoryLayout = mkOption {
type = types.enum [
"fs"
"maildir++"
];
default = "maildir++";
description = ''
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)
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.
'';
};
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 {
type = types.bool;
default = false;
@@ -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++"
))
];
}
+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
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 dont
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 dont 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``.
+3 -3
View File
@@ -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
+3 -5
View File
@@ -98,16 +98,14 @@ in
) config.mailserver.dkim.domains
)
)
++
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
[
++ 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.mailDirectory`.
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.mailDirectory`
- 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.
+10 -10
View File
@@ -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}
}
''}
+1 -1
View File
@@ -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/
'';
};
};
+2 -2
View File
@@ -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}
'';
};
+14 -21
View File
@@ -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 = {
+1
View File
@@ -27,6 +27,7 @@ options = json.load(f)
groups = [
"mailserver.accounts",
"mailserver.x509",
"mailserver.storage",
"mailserver.dkim",
"mailserver.srs",
"mailserver.dmarcReporting",
+6 -3
View File
@@ -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"):
+4 -4
View File
@@ -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"):