Merge branch 'unhashed-password' into 'master'

Add support for plaintext password files

See merge request simple-nixos-mailserver/nixos-mailserver!474
This commit is contained in:
Martin Weinelt
2026-03-05 11:16:40 +00:00
5 changed files with 67 additions and 5 deletions
+13
View File
@@ -178,6 +178,19 @@ in
'';
};
passwordFile = mkOption {
type =
with types;
nullOr (pathWith {
inStore = false;
});
default = null;
example = "/run/keys/user1-password";
description = ''
A file containing the user's plain text password. The value will be hashed at runtime.
'';
};
aliases = mkOption {
type = with types; listOf types.str;
example = [
+9 -2
View File
@@ -45,12 +45,19 @@ rec {
in
lib.mapAttrs (
name: value:
if value.hashedPasswordFile == null then
if value.hashedPasswordFile != null then
value.hashedPasswordFile
else if value.hashedPassword != null then
builtins.toString (mkHashFile name value.hashedPassword)
else
value.hashedPasswordFile
value.passwordFile
) cfg.loginAccounts;
# Collect accounts with plain text passwords that require hashing
accountsWithPlaintextPasswordFiles = lib.filter (
name: cfg.loginAccounts.${name}.passwordFile != null
) (builtins.attrNames cfg.loginAccounts);
# Appends the LDAP bind password to files to avoid writing this
# password into the Nix store.
appendLdapBindPwd =
+5 -1
View File
@@ -121,7 +121,11 @@ let
cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
name: _:
if lib.elem name accountsWithPlaintextPasswordFiles then
"${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' pkgs.dovecot "doveadm"} pw)"}::::::"
else
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts
)}
EOF
+9 -2
View File
@@ -90,8 +90,15 @@ in
config = lib.mkIf enable {
# assert that all accounts provide a password
assertions = map (acct: {
assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
message = "${acct.name} must provide either a hashed password or a password hash file";
assertion =
lib.length (
lib.filter (value: value != null) [
acct.hashedPassword
acct.hashedPasswordFile
acct.passwordFile
]
) == 1;
message = "Login account ${acct.name} must provide exactly one of password file, hashed password, or hashed password file";
}) (lib.attrValues loginAccounts);
# warn for accounts that specify both password and file
+31
View File
@@ -69,6 +69,15 @@ in
netcat
]);
systemd.tmpfiles.settings."mailserver-test-passwords" = {
"/run/passwords/user3" = {
f = {
argument = "my-password";
mode = "0600";
};
};
};
mailserver = {
enable = true;
fqdn = "mail.example.com";
@@ -86,6 +95,9 @@ in
hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ];
};
"user3@example.com" = {
passwordFile = "/run/passwords/user3";
};
"send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only";
sendOnly = true;
@@ -223,6 +235,25 @@ in
"set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
)
with subtest("user with plaintext password file can send and receive"):
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--imap-host localhost",
"--imap-username user3@example.com",
"--from-addr user3@example.com",
"--to-addr user3@example.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
with subtest("imap port 143 is closed and imaps is serving SSL"):
machine.wait_for_closed_port(143)
machine.wait_for_open_port(993)