Merge branch 'cleanup' into 'master'

Rename loginAccounts and group storage related settings

See merge request simple-nixos-mailserver/nixos-mailserver!501
This commit is contained in:
Martin Weinelt
2026-03-24 22:56:11 +00:00
21 changed files with 250 additions and 198 deletions
+138 -70
View File
@@ -25,7 +25,7 @@ let
inherit (lib) inherit (lib)
literalExpression literalExpression
literalMD literalMD
mkDefault mkChangedOptionModule
mkEnableOption mkEnableOption
mkOption mkOption
mkOptionType mkOptionType
@@ -137,7 +137,7 @@ in
description = "Message size limit enforced by Postfix."; description = "Message size limit enforced by Postfix.";
}; };
loginAccounts = mkOption { accounts = mkOption {
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
{ name, ... }: { name, ... }:
@@ -145,8 +145,13 @@ in
options = { options = {
name = mkOption { name = mkOption {
type = types.str; type = types.str;
default = name;
example = "user1@example.com"; example = "user1@example.com";
description = "Username"; readOnly = true;
internal = true;
description = ''
The login username for this account.
'';
}; };
hashedPassword = mkOption { hashedPassword = mkOption {
@@ -154,24 +159,33 @@ in
default = null; default = null;
example = "$y$j9T$vfGrwkAaXCjCEWtVNMQck1$383uIXQmn2z0hnmVAA8kwFQmjNj78.nYbvWeyNLIaP1"; example = "$y$j9T$vfGrwkAaXCjCEWtVNMQck1$383uIXQmn2z0hnmVAA8kwFQmjNj78.nYbvWeyNLIaP1";
description = '' 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' nix-shell -p mkpasswd --run 'mkpasswd -s'
``` ```
Warning: this is stored in plaintext in the Nix store! :::{note}
Use {option}`mailserver.loginAccounts.<name>.hashedPasswordFile` instead. 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.<name>.hashedPasswordFile`
allows relying on filesystem discretionary access control as
another security boundary.
:::
''; '';
}; };
hashedPasswordFile = mkOption { hashedPasswordFile = mkOption {
type = with types; nullOr path; type = with types; nullOr path;
default = null; default = null;
example = "/run/keys/user1-passwordhash"; example = "/run/keys/user1-pw-hash";
description = '' 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' nix-shell -p mkpasswd --run 'mkpasswd -s'
``` ```
@@ -185,9 +199,13 @@ in
inStore = false; inStore = false;
}); });
default = null; default = null;
example = "/run/keys/user1-password"; example = "/run/keys/user1-pw";
description = '' 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 = [ ]; default = [ ];
description = '' description = ''
A list of aliases of this login account. List of additional mail addresses (aliases) that get routed to this account.
Note: Use list entries like "@example.com" to create a catchAll
that allows sending from all email addresses in these domain. :::{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$/'' ]; example = [ ''/^tom\..*@domain\.com$/'' ];
default = [ ]; default = [ ];
description = '' description = ''
Same as {option}`mailserver.loginAccounts.<name>.aliases` but Same as {option}`mailserver.accounts.<name>.aliases` but
using PCRE (Perl compatible regex). using PCRE (Perl compatible regex).
''; '';
}; };
@@ -224,7 +246,12 @@ in
default = [ ]; default = [ ];
description = '' description = ''
For which domains should this account act as a catch all? 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.<name>.aliases` if that
is required.
:::
''; '';
}; };
@@ -266,9 +293,10 @@ in
default = false; default = false;
description = '' description = ''
Specifies if the account should be a send-only account. 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` Emails sent to send-only accounts will
stating the reason. be rejected with the reason configured in
{option}`mailserver.accounts.<name>.sendOnlyRejectMessage`.
''; '';
}; };
@@ -276,33 +304,38 @@ in
type = types.str; type = types.str;
default = "This account cannot receive emails."; default = "This account cannot receive emails.";
description = '' description = ''
The message that will be returned to the sender when an email is The message returned to the sender for a send-only account.
sent to a send-only account. Only used if the account is marked
as send-only. See {option}`mailserver.accounts.<name>.sendOnly`.
''; '';
}; };
}; };
config.name = mkDefault name;
} }
) )
); );
example = { example = lib.literalExpression ''
{
user1 = { user1 = {
# This password hash leaks into the Nix store
hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5"; hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5";
}; };
user2 = { user2 = {
hashedPassword = "$y$j9T$hZ.ubq0M897Hw.znxnGG9.$14EJBoOwbwKeWt.W4vpnBPEBZC9mYz4fWI9kOCLoZf4"; # Hashed password passed as a file
hashedPasswordFile = "/run/keys/user2-pw-hash";
}; };
user3 = {
# Plaintext password file
passwordFile = "/run/keys/user3-pw";
}; };
}
'';
description = '' description = ''
The login account of the domain. Every account is mapped to a unix user, Attribute set of mail accounts.
e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as
follows
``` Each entry defines a mailbox and login credentials, where the attribute
nix-shell -p mkpasswd --run 'mkpasswd -s' name is used as the login username and optionally routed mail address.
```
Use `mkpasswd` to generate password hashes.
''; '';
default = { }; default = { };
}; };
@@ -652,13 +685,13 @@ in
aliases = mkOption { aliases = mkOption {
type = type =
let let
loginAccount = mkOptionType { account = mkOptionType {
name = "Login Account"; name = "Login Account";
check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts); check = account: builtins.elem account (builtins.attrNames cfg.accounts);
}; };
in in
with types; with types;
attrsOf (either loginAccount (nonEmptyListOf loginAccount)); attrsOf (either account (nonEmptyListOf account));
example = { example = {
"postmaster@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com";
"abuse@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com";
@@ -751,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;
@@ -1481,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.";
}; };
@@ -1682,5 +1742,13 @@ in
[ "mailserver" "ldap" "attributes" "mail" ] [ "mailserver" "ldap" "attributes" "mail" ]
) )
(mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ]) (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 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``.
+1 -1
View File
@@ -13,7 +13,7 @@ domain ``example.com`` and send mails with any address of this domain:
.. code:: nix .. code:: nix
mailserver.loginAccounts = { mailserver.accounts = {
"user@example.com" = { "user@example.com" = {
aliases = [ "@example.com" ]; aliases = [ "@example.com" ];
}; };
+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
+1 -1
View File
@@ -12,7 +12,7 @@ let
mapAttrsToList mapAttrsToList
; ;
mailAccounts = config.mailserver.loginAccounts; mailAccounts = config.mailserver.accounts;
htpasswd = pkgs.writeText "radicale.users" ( htpasswd = pkgs.writeText "radicale.users" (
concatStrings (flip mapAttrsToList mailAccounts (mail: user: "${mail}+:${user.hashedPassword}\n")) concatStrings (flip mapAttrsToList mailAccounts (mail: user: "${mail}+:${user.hashedPassword}\n"))
); );
+1 -1
View File
@@ -10,7 +10,7 @@ Limitations
Radicale since the 3.x release (introduced in NixOS 20.09) does not support Radicale since the 3.x release (introduced in NixOS 20.09) does not support
traditional crypt() password hashes any longer. To establish access for 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 for ``hashedPassword`` needs to be compatible with one of the available
`htpasswd_encryption`_ methods. Such hashes can for example be created using `htpasswd_encryption`_ methods. Such hashes can for example be created using
+1 -1
View File
@@ -21,7 +21,7 @@ NixOS 26.05
is an alternative to hashed passwords that integrates well with workflows is an alternative to hashed passwords that integrates well with workflows
established by `agenix`_/`sops-nix`_ that instead rely on encryption. This established by `agenix`_/`sops-nix`_ that instead rely on encryption. This
option prevents files from leaking in to the Nix store. option prevents files from leaking in to the Nix store.
See :option:`mailserver.loginAccounts.<name>.passwordFile`. See :option:`mailserver.accounts.<name>.passwordFile`.
- LDAP setups require a migration of Dovecot home directories to - LDAP setups require a migration of Dovecot home directories to
`UUID based home directories`_. The exact UUID attribute can be customized `UUID based home directories`_. The exact UUID attribute can be customized
through :option:`mailserver.ldap.attributes.uuid`. through :option:`mailserver.ldap.attributes.uuid`.
+1 -1
View File
@@ -38,7 +38,7 @@
# A list of all login accounts. To create the password hashes, use # A list of all login accounts. To create the password hashes, use
# nix-shell -p mkpasswd --run 'mkpasswd -s' # nix-shell -p mkpasswd --run 'mkpasswd -s'
loginAccounts = { accounts = {
"user1@example.com" = { "user1@example.com" = {
# Reads the password hash from a file on the server # Reads the password hash from a file on the server
hashedPasswordFile = "/a/file/containing/a/hashed/password"; hashedPasswordFile = "/a/file/containing/a/hashed/password";
+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.
+4 -4
View File
@@ -51,12 +51,12 @@ rec {
builtins.toString (mkHashFile name value.hashedPassword) builtins.toString (mkHashFile name value.hashedPassword)
else else
value.passwordFile value.passwordFile
) cfg.loginAccounts; ) cfg.accounts;
# Collect accounts with plain text passwords that require hashing # Collect accounts with plain text passwords that require hashing
accountsWithPlaintextPasswordFiles = lib.filter ( accountsWithPlaintextPasswordFiles = lib.filter (name: cfg.accounts.${name}.passwordFile != null) (
name: cfg.loginAccounts.${name}.passwordFile != null builtins.attrNames cfg.accounts
) (builtins.attrNames cfg.loginAccounts); );
# Appends the LDAP bind password to files to avoid writing this # Appends the LDAP bind password to files to avoid writing this
# password into the Nix store. # password into the Nix store.
+13 -13
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}}"
} }
@@ -115,7 +115,7 @@ let
umask 077 umask 077
for f in ${ for f in ${
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts) builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.accounts)
}; do }; do
if [ ! -f "$f" ]; then if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!" 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)"}::::::" "${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' pkgs.dovecot "doveadm"} pw)"}::::::"
else else
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts ) cfg.accounts
)} )}
EOF EOF
@@ -141,7 +141,7 @@ let
name: value: name: value:
"${name}:::::::" "${name}:::::::"
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
) cfg.loginAccounts ) cfg.accounts
)} )}
EOF EOF
''; '';
@@ -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}
} }
''} ''}
+4 -4
View File
@@ -51,7 +51,7 @@ let
to = name; to = name;
in in
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name) map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
) cfg.loginAccounts ) cfg.accounts
) )
); );
regex_valiases_postfix = mergeLookupTables ( regex_valiases_postfix = mergeLookupTables (
@@ -62,7 +62,7 @@ let
to = name; to = name;
in in
map (from: { "${from}" = to; }) value.aliasesRegexp map (from: { "${from}" = to; }) value.aliasesRegexp
) cfg.loginAccounts ) cfg.accounts
) )
); );
@@ -75,7 +75,7 @@ let
to = name; to = name;
in in
map (from: { "@${from}" = to; }) value.catchAll map (from: { "@${from}" = to; }) value.catchAll
) cfg.loginAccounts ) cfg.accounts
) )
); );
@@ -127,7 +127,7 @@ let
# denied_recipients_postfix :: [ String ] # denied_recipients_postfix :: [ String ]
denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") ( 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" ( denied_recipients_file = builtins.toFile "denied_recipients" (
lib.concatStringsSep "\n" denied_recipients_postfix lib.concatStringsSep "\n" denied_recipients_postfix
+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}
''; '';
}; };
+30 -47
View File
@@ -16,32 +16,13 @@
{ {
config, config,
options,
pkgs, pkgs,
lib, lib,
... ...
}: }:
with (import ./common.nix {
inherit
config
options
lib
pkgs
;
});
with config.mailserver;
let let
vmail_user = { cfg = config.mailserver;
name = vmailUserName;
isSystemUser = true;
uid = vmailUID;
home = mailDirectory;
createHome = true;
group = vmailGroupName;
};
virtualMailUsersActivationScript = virtualMailUsersActivationScript =
pkgs.writeScript "activate-virtual-mail-users" pkgs.writeScript "activate-virtual-mail-users"
@@ -55,10 +36,10 @@ let
umask 007 umask 007
# 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 "${sieveDirectory}"); then if (! test -d "${cfg.sieveDirectory}"); then
mkdir "${sieveDirectory}" mkdir "${cfg.sieveDirectory}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}" chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}"
chmod 770 "${sieveDirectory}" chmod 770 "${cfg.sieveDirectory}"
fi fi
# Copy user's sieve script to the correct location (if it exists). If it # Copy user's sieve script to the correct location (if it exists). If it
@@ -67,30 +48,30 @@ let
{ name, sieveScript }: { name, sieveScript }:
if lib.isString sieveScript then if lib.isString sieveScript then
'' ''
if (! test -d "${sieveDirectory}/${name}"); then if (! test -d "${cfg.sieveDirectory}/${name}"); then
mkdir -p "${sieveDirectory}/${name}" mkdir -p "${cfg.sieveDirectory}/${name}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}" chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}"
chmod 770 "${sieveDirectory}/${name}" chmod 770 "${cfg.sieveDirectory}/${name}"
fi fi
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve" cat << 'EOF' > "${cfg.sieveDirectory}/${name}/default.sieve"
${sieveScript} ${sieveScript}
EOF EOF
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve" chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}/default.sieve"
'' ''
else else
'' ''
if (test -f "${sieveDirectory}/${name}/default.sieve"); then if (test -f "${cfg.sieveDirectory}/${name}/default.sieve"); then
rm "${sieveDirectory}/${name}/default.sieve" rm "${cfg.sieveDirectory}/${name}/default.sieve"
fi fi
if (test -f "${sieveDirectory}/${name}.svbin"); then if (test -f "${cfg.sieveDirectory}/${name}.svbin"); then
rm "${sieveDirectory}/${name}/default.svbin" rm "${cfg.sieveDirectory}/${name}/default.svbin"
fi fi
'' ''
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))} ) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues cfg.accounts))}
''; '';
in in
{ {
config = lib.mkIf enable { config = lib.mkIf cfg.enable {
# assert that all accounts provide a password # assert that all accounts provide a password
assertions = map (acct: { assertions = map (acct: {
assertion = assertion =
@@ -102,27 +83,29 @@ in
] ]
) == 1; ) == 1;
message = "Login account ${acct.name} must provide exactly one of password file, hashed password, or hashed password file"; 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 # warn for accounts that specify both password and file
warnings = warnings =
map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used") 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.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.${cfg.storage.group} = {
users.groups = { inherit (cfg.storage) gid;
"${vmailGroupName}" = {
gid = 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 = {
+2 -1
View File
@@ -25,8 +25,9 @@ f = open(sys.argv[1])
options = json.load(f) options = json.load(f)
groups = [ groups = [
"mailserver.loginAccounts", "mailserver.accounts",
"mailserver.x509", "mailserver.x509",
"mailserver.storage",
"mailserver.dkim", "mailserver.dkim",
"mailserver.srs", "mailserver.srs",
"mailserver.dmarcReporting", "mailserver.dmarcReporting",
+1 -1
View File
@@ -77,7 +77,7 @@
]; ];
virusScanning = true; virusScanning = true;
loginAccounts = { accounts = {
"user1@example.com" = { "user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ]; aliases = [ "postmaster@example.com" ];
+1 -1
View File
@@ -66,7 +66,7 @@
}; };
dmarcReporting.enable = true; dmarcReporting.enable = true;
loginAccounts = { accounts = {
"user1@example.com" = { "user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ]; aliases = [ "postmaster@example.com" ];
+7 -4
View File
@@ -87,7 +87,7 @@ in
]; ];
localDnsResolver = false; localDnsResolver = false;
loginAccounts = { accounts = {
"user1@example.com" = { "user1@example.com" = {
hashedPasswordFile = hashedPasswordFile; hashedPasswordFile = hashedPasswordFile;
}; };
@@ -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"):
+5 -5
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 = {
@@ -133,7 +133,7 @@ in
"frank@example.com" = "mallory@example.com"; "frank@example.com" = "mallory@example.com";
}; };
loginAccounts = { accounts = {
# Colliding local account takes precedence over LDAP account with # Colliding local account takes precedence over LDAP account with
# same address. # same address.
"carol@example.com" = { "carol@example.com" = {
@@ -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"):
+1 -1
View File
@@ -35,7 +35,7 @@ let
fqdn = "mail.${domain}"; fqdn = "mail.${domain}";
domains = [ domain ]; domains = [ domain ];
localDnsResolver = false; localDnsResolver = false;
loginAccounts = { accounts = {
"user@${domain}" = { "user@${domain}" = {
hashedPasswordFile = hashPassword "password"; hashedPasswordFile = hashPassword "password";
}; };