diff --git a/default.nix b/default.nix index 030355f..cc97d20 100644 --- a/default.nix +++ b/default.nix @@ -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 = [ diff --git a/mail-server/common.nix b/mail-server/common.nix index 04ab71d..d3de0d9 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -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 = diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 13f68bb..d3d0722 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -121,7 +121,11 @@ let cat < ${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 diff --git a/mail-server/users.nix b/mail-server/users.nix index 334efa7..93a543d 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -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 diff --git a/tests/internal.nix b/tests/internal.nix index e62eb32..36bc4b3 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -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 ''" ) + 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)