diff --git a/default.nix b/default.nix index 0c6271c..e6aaf8c 100644 --- a/default.nix +++ b/default.nix @@ -56,10 +56,27 @@ in }; hashedPassword = mkOption { - type = types.str; + type = with types; nullOr str; + default = null; example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; description = '' - Hashed password. Use `mkpasswd` as follows + The user's hashed password. Use `mkpasswd` as follows + + ``` + mkpasswd -m sha-512 "super secret password" + ``` + + Warning: this is stored in plaintext in the Nix store! + Use `hashedPasswordFile` instead. + ''; + }; + + hashedPasswordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/user1-passwordhash"; + description = '' + A file containing the user's hashed password. Use `mkpasswd` as follows ``` mkpasswd -m sha-512 "super secret password" diff --git a/mail-server/common.nix b/mail-server/common.nix index 7e968d9..b20e4c7 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -14,17 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, lib }: +{ config, pkgs, lib }: let cfg = config.mailserver; - # passwd :: [ String ] - passwd = lib.mapAttrsToList - (name: value: "${name}:${value.hashedPassword}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:" - + (if lib.isString value.quota - then "userdb_quota_rule=*:storage=${value.quota}" - else "")) - cfg.loginAccounts; in { # cert :: PATH @@ -45,6 +38,11 @@ in then "/var/lib/acme/${cfg.fqdn}/key.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; - # passwdFile :: PATH - passwdFile = builtins.toFile "passwd" (lib.concatStringsSep "\n" passwd); + passwordFiles = let + mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; + in + lib.mapAttrs (name: value: + if value.hashedPasswordFile == null then + builtins.toString (mkHashFile name value.hashedPassword) + else value.hashedPasswordFile) cfg.loginAccounts; } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index eef241d..f0a370a 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -16,11 +16,14 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config lib; }); +with (import ./common.nix { inherit config pkgs lib; }); let cfg = config.mailserver; + passwdDir = "/run/dovecot2"; + passwdFile = "${passwdDir}/passwd"; + maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; # maildir in format "/${domain}/${user}" @@ -47,6 +50,35 @@ let done ''; }; + + genPasswdScript = pkgs.writeScript "generate-password-file" '' + #!${pkgs.stdenv.shell} + + set -euo pipefail + + if (! test -d "${passwdDir}"); then + mkdir "${passwdDir}" + chmod 755 "${passwdDir}" + fi + + for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do + if [ ! -f "$f" ]; then + echo "Expected password hash file $f does not exist!" + exit 1 + fi + done + + cat < ${passwdFile} + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: + "${name}:${"$(cat ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:" + + (if lib.isString value.quota + then "userdb_quota_rule=*:storage=${value.quota}" + else "") + ) cfg.loginAccounts)} + EOF + + chmod 600 ${passwdFile} + ''; in { config = with cfg; lib.mkIf enable { @@ -165,15 +197,27 @@ in ''; }; - systemd.services.dovecot2.preStart = '' - rm -rf '${stateDir}/imap_sieve' - mkdir '${stateDir}/imap_sieve' - cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/' - for k in "${stateDir}/imap_sieve"/*.sieve ; do - ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" - done - chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' - ''; + systemd.services.gen-passwd-file = { + serviceConfig = { + ExecStart = genPasswdScript; + Type = "oneshot"; + }; + }; + + systemd.services.dovecot2 = { + after = [ "gen-passwd-file.service" ]; + wants = [ "gen-passwd-file.service" ]; + requires = [ "gen-passwd-file.service" ]; + preStart = '' + rm -rf '${stateDir}/imap_sieve' + mkdir '${stateDir}/imap_sieve' + cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/' + for k in "${stateDir}/imap_sieve"/*.sieve ; do + ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" + done + chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' + ''; + }; systemd.services.postfix.restartTriggers = [ passwdFile ]; }; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 06c6af0..b61f038 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -16,7 +16,7 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config lib; }); +with (import ./common.nix { inherit config pkgs lib; }); let inherit (lib.strings) concatStringsSep; diff --git a/mail-server/users.nix b/mail-server/users.nix index f335330..3ab31d5 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -66,6 +66,19 @@ let ''; 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"; + }) (lib.attrValues loginAccounts)); + + # warn for accounts that specify both password and file + warnings = (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.attrValues loginAccounts))); + # set the vmail gid to a specific value users.groups = { "${vmailGroupName}" = { gid = vmailUID; };