From fb3210b9326b81dd24c6092a3da1a1168dff0b0d Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 00:12:02 +0200 Subject: [PATCH] ldap: do not write password to the Nix store --- default.nix | 6 +-- mail-server/common.nix | 21 ++++++++++ mail-server/dovecot.nix | 85 ++++++++++++++++++++++------------------- mail-server/postfix.nix | 53 +++++++++++++++++-------- tests/ldap.nix | 10 ++++- 5 files changed, 114 insertions(+), 61 deletions(-) diff --git a/default.nix b/default.nix index 86b436d..f98b4ad 100644 --- a/default.nix +++ b/default.nix @@ -240,11 +240,11 @@ in ''; }; - password = mkOption { + passwordFile = mkOption { type = types.str; - example = "not$4f3"; + example = "/run/my-secret"; description = '' - Password required to authenticate against the LDAP servers. + A file containing the password required to authenticate against the LDAP servers. ''; }; }; diff --git a/mail-server/common.nix b/mail-server/common.nix index e8beb7a..236530b 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -45,4 +45,25 @@ in if value.hashedPasswordFile == null then builtins.toString (mkHashFile name value.hashedPassword) else value.hashedPasswordFile) cfg.loginAccounts; + + # Appends the LDAP bind password to files to avoid writing this + # password into the Nix store. + appendLdapBindPwd = { + name, file, prefix, passwordFile, destination + }: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' + #!${pkgs.stdenv.shell} + set -euo pipefail + + baseDir=$(dirname ${destination}) + if (! test -d "$baseDir"); then + mkdir -p $baseDir + chmod 755 $baseDir + fi + + cat ${file} > ${destination} + echo -n "${prefix}" >> ${destination} + cat ${passwordFile} >> ${destination} + chmod 600 ${destination} + ''; + } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 33dc3c8..92f587a 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -22,9 +22,10 @@ let cfg = config.mailserver; passwdDir = "/run/dovecot2"; - passdbFile = "${passwdDir}/passdb"; + passwdFile = "${passwdDir}/passwd"; userdbFile = "${passwdDir}/userdb"; - + # This file contains the ldap bind password + ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; bool2int = x: if x then "1" else "0"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; @@ -58,6 +59,41 @@ let ''; }; + + ldapConfig = pkgs.writeTextFile { + name = "dovecot-ldap.conf.ext.template"; + text = '' + ldap_version = 3 + uris = ${lib.concatStringsSep " " cfg.ldap.uris} + ${lib.optionalString cfg.ldap.startTls '' + tls = yes + ''} + tls_require_cert = hard + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + dn = ${cfg.ldap.bind.dn} + sasl_bind = no + auth_bind = yes + base = ${cfg.ldap.searchBase} + scope = ${mkLdapSearchScope cfg.ldap.searchScope} + ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' + user_attrs = ${cfg.ldap.dovecot.user_attrs} + ''} + user_filter = ${cfg.ldap.dovecot.userFilter} + ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' + pass_attrs = ${cfg.ldap.dovecot.passAttrs} + ''} + pass_filter = ${cfg.ldap.dovecot.passFilter} + ''; + }; + + setPwdInLdapConfFile = appendLdapBindPwd { + name = "ldap-conf-file"; + file = ldapConfig; + prefix = "dnpass = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapConfFile; + }; + genPasswdScript = pkgs.writeScript "generate-password-file" '' #!${pkgs.stdenv.shell} @@ -75,7 +111,7 @@ let fi done - cat < ${passdbFile} + cat < ${passwdFile} ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" ) cfg.loginAccounts)} @@ -90,7 +126,7 @@ let ) cfg.loginAccounts)} EOF - chmod 600 ${passdbFile} + chmod 600 ${passwdFile} chmod 600 ${userdbFile} ''; @@ -233,7 +269,7 @@ in passdb { driver = passwd-file - args = ${passdbFile} + args = ${passwdFile} } userdb { @@ -245,12 +281,12 @@ in ${lib.optionalString cfg.ldap.enable '' passdb { driver = ldap - args = /etc/dovecot/dovecot-ldap.conf.ext + args = ${ldapConfFile} } userdb { driver = ldap - args = /etc/dovecot/dovecot-ldap.conf.ext + args = ${ldapConfFile} default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} } ''} @@ -317,37 +353,6 @@ in ''; }; - environment.etc = lib.optionalAttrs (cfg.ldap.enable) { - "dovecot/dovecot-ldap.conf.ext" = { - mode = "0600"; - uid = config.ids.uids.dovecot2; - gid = config.ids.gids.dovecot2; - text = '' - ldap_version = 3 - uris = ${lib.concatStringsSep " " cfg.ldap.uris} - ${lib.optionalString cfg.ldap.startTls '' - tls = yes - ''} - tls_require_cert = hard - tls_ca_cert_file = ${cfg.ldap.tlsCAFile} - dn = ${cfg.ldap.bind.dn} - dnpass = ${cfg.ldap.bind.password} - sasl_bind = no - auth_bind = yes - base = ${cfg.ldap.searchBase} - scope = ${mkLdapSearchScope cfg.ldap.searchScope} - ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' - user_attrs = ${cfg.ldap.dovecot.user_attrs} - ''} - user_filter = ${cfg.ldap.dovecot.userFilter} - ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' - pass_attrs = ${cfg.ldap.dovecot.passAttrs} - ''} - pass_filter = ${cfg.ldap.dovecot.passFilter} - ''; - }; - }; - systemd.services.dovecot2 = { preStart = '' ${genPasswdScript} @@ -358,10 +363,10 @@ in ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" done chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' - ''; + '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); }; - systemd.services.postfix.restartTriggers = [ genPasswdScript ]; + systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]); systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) { description = "Optimize dovecot indices for fts_xapian"; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 576b8f7..ad7ce35 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -133,13 +133,13 @@ let smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = "$myhostname"; smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMap}"}"; + smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}"; smtpd_sender_restrictions = "reject_sender_login_mismatch"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; cleanup_service_name = "submission-header-cleanup"; }; - commonLdapConfig = lib.optionalString (cfg.ldap.enable) '' + commonLdapConfig = '' server_host = ${lib.concatStringsSep " " cfg.ldap.uris} start_tls = ${if cfg.ldap.startTls then "yes" else "no"} version = 3 @@ -151,26 +151,47 @@ let bind = yes bind_dn = ${cfg.ldap.bind.dn} - bind_pw = ${cfg.ldap.bind.password} ''; - ldapSenderLoginMap = lib.optionalString (cfg.ldap.enable) - (pkgs.writeText "ldap-sender-login-map.cf" '' - ${commonLdapConfig} - query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.mailAttribute} - ''); + ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.mailAttribute} + ''; + ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; + appendPwdInSenderLoginMap = appendLdapBindPwd { + name = "ldap-sender-login-map"; + file = ldapSenderLoginMap; + prefix = "bind_pw = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapSenderLoginMapFile; + }; - ldapVirtualMailboxMap = lib.optionalString (cfg.ldap.enable) - (pkgs.writeText "ldap-virtual-mailbox-map.cf" '' - ${commonLdapConfig} - query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.uidAttribute} - ''); + ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''; + ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf"; + appendPwdInVirtualMailboxMap = appendLdapBindPwd { + name = "ldap-virtual-mailbox-map"; + file = ldapVirtualMailboxMap; + prefix = "bind_pw = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapVirtualMailboxMapFile; + }; in { config = with cfg; lib.mkIf enable { + systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { + preStart = '' + ${appendPwdInVirtualMailboxMap} + ${appendPwdInSenderLoginMap} + ''; + restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; + }; + services.postfix = { enable = true; hostname = "${sendingFqdn}"; @@ -202,7 +223,7 @@ in virtual_mailbox_maps = [ (mappedFile "valias") ] ++ lib.optionals (cfg.ldap.enable) [ - "ldap:${ldapVirtualMailboxMap}" + "ldap:${ldapVirtualMailboxMapFile}" ]; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients diff --git a/tests/ldap.nix b/tests/ldap.nix index 6c8308d..172a77d 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -28,6 +28,8 @@ pkgs.nixosTest { ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ '')]; + environment.etc.bind-password.text = bindPassword; + services.openldap = { enable = true; settings = { @@ -45,7 +47,7 @@ pkgs.nixosTest { "olcMdbConfig" ]; olcDatabase = "{1}mdb"; - olcDbDirectory = "/var/lib/openldap"; + olcDbDirectory = "/var/lib/openldap/example"; olcSuffix = "dc=example"; }; }; @@ -96,7 +98,7 @@ pkgs.nixosTest { ]; bind = { dn = "cn=mail,dc=example"; - password = bindPassword; + passwordFile = "/etc/bind-password"; }; searchBase = "ou=users,dc=example"; searchScope = "sub"; @@ -141,6 +143,10 @@ pkgs.nixosTest { machine.succeed("doveadm user -u alice@example.com") machine.succeed("doveadm user -u bob@example.com") + with subtest("Files containing secrets are only readable by root"): + machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'") + machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'") + with subtest("Test account/mail address binding"): machine.fail(" ".join([ "mail-check send-and-read",