From 65cfba23af3ca07a973cbf41b31057dea49a1afd Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Tue, 8 Jan 2019 17:01:01 +0100 Subject: [PATCH] nixos/tests: test LDAP password changing through nslcd NOTE: slapd.conf is deprecated, hence use cn=config. --- nixos/doc/manual/release-notes/rl-1903.xml | 16 +- nixos/modules/config/ldap.nix | 45 ++- nixos/modules/security/pam.nix | 2 +- nixos/tests/ldap.nix | 381 ++++++++++++++++++--- 4 files changed, 381 insertions(+), 63 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-1903.xml b/nixos/doc/manual/release-notes/rl-1903.xml index 89d9f48aedd..0fec5accccd 100644 --- a/nixos/doc/manual/release-notes/rl-1903.xml +++ b/nixos/doc/manual/release-notes/rl-1903.xml @@ -331,17 +331,29 @@ The pam_unix account module is now loaded with its control field set to required instead of - sufficient, so that later pam account modules that + sufficient, so that later PAM account modules that might do more extensive checks are being executed. Previously, the whole account module verification was exited prematurely in case a nss module provided the account name to pam_unix. The LDAP and SSSD NixOS modules already add their NSS modules when - enabled. In case your setup breaks due to some later pam account module + enabled. In case your setup breaks due to some later PAM account module previosuly shadowed, or failing NSS lookups, please file a bug. You can get back the old behaviour by manually setting .text]]>. + + + + The pam_unix password module is now loaded with its + control field set to sufficient instead of + required, so that password managed only + by later PAM password modules are being executed. + Previously, for example, changing an LDAP account's password through PAM + was not possible: the whole password module verification + was exited prematurely by pam_unix, + preventing pam_ldap to manage the password as it should. + diff --git a/nixos/modules/config/ldap.nix b/nixos/modules/config/ldap.nix index 18823221990..f65a3fc50d5 100644 --- a/nixos/modules/config/ldap.nix +++ b/nixos/modules/config/ldap.nix @@ -38,6 +38,8 @@ let bind_timelimit ${toString cfg.bind.timeLimit} ${optionalString (cfg.bind.distinguishedName != "") "binddn ${cfg.bind.distinguishedName}" } + ${optionalString (cfg.daemon.rootpwmoddn != "") + "rootpwmoddn ${cfg.daemon.rootpwmoddn}" } ${optionalString (cfg.daemon.extraConfig != "") cfg.daemon.extraConfig } ''; }; @@ -126,6 +128,26 @@ in the end of the nslcd configuration file (nslcd.conf). '' ; } ; + + rootpwmoddn = mkOption { + default = ""; + example = "cn=admin,dc=example,dc=com"; + type = types.str; + description = '' + The distinguished name to use to bind to the LDAP server + when the root user tries to modify a user's password. + ''; + }; + + rootpwmodpw = mkOption { + default = ""; + example = "/run/keys/nslcd.rootpwmodpw"; + type = types.str; + description = '' + The path to a file containing the credentials with which + to bind to the LDAP server if the root user tries to change a user's password + ''; + }; }; bind = { @@ -203,9 +225,11 @@ in system.activationScripts = mkIf insertLdapPassword { ldap = stringAfter [ "etc" "groups" "users" ] '' if test -f "${cfg.bind.password}" ; then - echo "bindpw "$(cat ${cfg.bind.password})"" | cat ${ldapConfig.source} - > /etc/ldap.conf.bindpw - mv -fT /etc/ldap.conf.bindpw /etc/ldap.conf - chmod 600 /etc/ldap.conf + umask 0077 + conf="$(mktemp)" + printf 'bindpw %s\n' "$(cat ${cfg.bind.password})" | + cat ${ldapConfig.source} - >"$conf" + mv -fT "$conf" /etc/ldap.conf fi ''; }; @@ -232,11 +256,16 @@ in wantedBy = [ "multi-user.target" ]; preStart = '' - ${optionalString (cfg.bind.distinguishedName != "") '' - if test -s "${cfg.bind.password}" ; then - ln -sfT "${cfg.bind.password}" /run/nslcd/bindpw - fi - ''} + umask 0077 + conf="$(mktemp)" + { + cat ${nslcdConfig.source} + test -z '${cfg.bind.distinguishedName}' -o ! -f '${cfg.bind.password}' || + printf 'bindpw %s\n' "$(cat '${cfg.bind.password}')" + test -z '${cfg.daemon.rootpwmoddn}' -o ! -f '${cfg.daemon.rootpwmodpw}' || + printf 'rootpwmodpw %s\n' "$(cat '${cfg.daemon.rootpwmodpw}')" + } >"$conf" + mv -fT "$conf" /etc/nslcd.conf ''; # NOTE: because one cannot pass a custom config path to `nslcd` diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index b1a0eff98c2..a2eb6b1df10 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -368,7 +368,7 @@ let auth required pam_deny.so # Password management. - password requisite pam_unix.so nullok sha512 + password sufficient pam_unix.so nullok sha512 ${optionalString config.security.pam.enableEcryptfs "password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so"} ${optionalString cfg.pamMount diff --git a/nixos/tests/ldap.nix b/nixos/tests/ldap.nix index 035a8192417..b3fd42e7588 100644 --- a/nixos/tests/ldap.nix +++ b/nixos/tests/ldap.nix @@ -1,41 +1,23 @@ import ./make-test.nix ({ pkgs, lib, ...} : let + unlines = lib.concatStringsSep "\n"; + unlinesAttrs = f: as: unlines (lib.mapAttrsToList f as); + dbDomain = "example.com"; dbSuffix = "dc=example,dc=com"; - dbPath = "/var/db/openldap"; dbAdminDn = "cn=admin,${dbSuffix}"; - dbAdminPwd = "test"; - serverUri = "ldap:///"; + dbAdminPwd = "admin-password"; + # NOTE: slappasswd -h "{SSHA}" -s '${dbAdminPwd}' + dbAdminPwdHash = "{SSHA}i7FopSzkFQMrHzDMB1vrtkI0rBnwouP8"; ldapUser = "test-ldap-user"; ldapUserId = 10000; - ldapUserPwd = "test"; + ldapUserPwd = "user-password"; + # NOTE: slappasswd -h "{SSHA}" -s '${ldapUserPwd}' + ldapUserPwdHash = "{SSHA}v12XICMZNGT6r2KJ26rIkN8Vvvp4QX6i"; ldapGroup = "test-ldap-group"; ldapGroupId = 10000; - setupLdif = pkgs.writeText "test-ldap.ldif" '' - dn: ${dbSuffix} - dc: ${with lib; let dc = head (splitString "," dbSuffix); dcName = head (tail (splitString "=" dc)); in dcName} - o: ${dbSuffix} - objectclass: top - objectclass: dcObject - objectclass: organization - dn: cn=${ldapUser},${dbSuffix} - sn: ${ldapUser} - objectClass: person - objectClass: posixAccount - uid: ${ldapUser} - uidNumber: ${toString ldapUserId} - gidNumber: ${toString ldapGroupId} - homeDirectory: /home/${ldapUser} - loginShell: /bin/sh - userPassword: ${ldapUserPwd} - - dn: cn=${ldapGroup},${dbSuffix} - objectClass: posixGroup - gidNumber: ${toString ldapGroupId} - memberUid: ${ldapUser} - ''; mkClient = useDaemon: { lib, ... }: { @@ -43,13 +25,24 @@ let virtualisation.vlans = [ 1 ]; security.pam.services.su.rootOK = lib.mkForce false; users.ldap.enable = true; - users.ldap.daemon.enable = useDaemon; + users.ldap.daemon = { + enable = useDaemon; + rootpwmoddn = "cn=admin,${dbSuffix}"; + rootpwmodpw = "/etc/nslcd.rootpwmodpw"; + }; + # NOTE: password stored in clear in Nix's store, but this is a test. + environment.etc."nslcd.rootpwmodpw".source = pkgs.writeText "rootpwmodpw" dbAdminPwd; users.ldap.loginPam = true; users.ldap.nsswitch = true; users.ldap.server = "ldap://server"; - users.ldap.base = "${dbSuffix}"; + users.ldap.base = "ou=posix,${dbSuffix}"; + users.ldap.bind = { + distinguishedName = "cn=admin,${dbSuffix}"; + password = "/etc/ldap/bind.password"; + }; + # NOTE: password stored in clear in Nix's store, but this is a test. + environment.etc."ldap/bind.password".source = pkgs.writeText "password" dbAdminPwd; }; - in { @@ -61,28 +54,237 @@ in nodes = { server = - { pkgs, ... }: + { pkgs, config, ... }: + let + inherit (config.services) openldap; + + slapdConfig = pkgs.writeText "cn=config.ldif" ('' + dn: cn=config + objectClass: olcGlobal + #olcPidFile: /run/slapd/slapd.pid + # List of arguments that were passed to the server + #olcArgsFile: /run/slapd/slapd.args + # Read slapd-config(5) for possible values + olcLogLevel: none + # The tool-threads parameter sets the actual amount of CPU's + # that is used for indexing. + olcToolThreads: 1 + + dn: olcDatabase={-1}frontend,cn=config + objectClass: olcDatabaseConfig + objectClass: olcFrontendConfig + # The maximum number of entries that is returned for a search operation + olcSizeLimit: 500 + # Allow unlimited access to local connection from the local root user + olcAccess: to * + by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage + by * break + # Allow unauthenticated read access for schema and base DN autodiscovery + olcAccess: to dn.exact="" + by * read + olcAccess: to dn.base="cn=Subschema" + by * read + + dn: olcDatabase=config,cn=config + objectClass: olcDatabaseConfig + olcRootDN: cn=admin,cn=config + #olcRootPW: + # NOTE: access to cn=config, system root can be manager + # with SASL mechanism (-Y EXTERNAL) over unix socket (-H ldapi://) + olcAccess: to * + by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage + by * break + + dn: cn=schema,cn=config + objectClass: olcSchemaConfig + + include: file://${pkgs.openldap}/etc/schema/core.ldif + include: file://${pkgs.openldap}/etc/schema/cosine.ldif + include: file://${pkgs.openldap}/etc/schema/nis.ldif + include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif + + dn: cn=module{0},cn=config + objectClass: olcModuleList + # Where the dynamically loaded modules are stored + #olcModulePath: /usr/lib/ldap + olcModuleLoad: back_mdb + + '' + + unlinesAttrs (olcSuffix: {conf, ...}: + "include: file://" + pkgs.writeText "config.ldif" conf + ) slapdDatabases + ); + + slapdDatabases = { + "${dbSuffix}" = { + conf = '' + dn: olcBackend={1}mdb,cn=config + objectClass: olcBackendConfig + + dn: olcDatabase={1}mdb,cn=config + olcSuffix: ${dbSuffix} + olcDbDirectory: ${openldap.dataDir}/${dbSuffix} + objectClass: olcDatabaseConfig + objectClass: olcMdbConfig + # NOTE: checkpoint the database periodically in case of system failure + # and to speed up slapd shutdown. + olcDbCheckpoint: 512 30 + # Database max size is 1G + olcDbMaxSize: 1073741824 + olcLastMod: TRUE + # NOTE: database superuser. Needed for syncrepl, + # and used to auth as admin through a TCP connection. + olcRootDN: cn=admin,${dbSuffix} + olcRootPW: ${dbAdminPwdHash} + # + olcDbIndex: objectClass eq + olcDbIndex: cn,uid eq + olcDbIndex: uidNumber,gidNumber eq + olcDbIndex: member,memberUid eq + # + olcAccess: to attrs=userPassword + by self write + by anonymous auth + by dn="cn=admin,${dbSuffix}" write + by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write + by * none + olcAccess: to attrs=shadowLastChange + by self write + by dn="cn=admin,${dbSuffix}" write + by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write + by * none + olcAccess: to dn.sub="ou=posix,${dbSuffix}" + by self read + by dn="cn=admin,${dbSuffix}" read + by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read + olcAccess: to * + by self read + by * none + ''; + data = '' + dn: ${dbSuffix} + objectClass: top + objectClass: dcObject + objectClass: organization + o: ${dbDomain} + + dn: cn=admin,${dbSuffix} + objectClass: simpleSecurityObject + objectClass: organizationalRole + description: ${dbDomain} LDAP administrator + roleOccupant: ${dbSuffix} + userPassword: ${ldapUserPwdHash} + + dn: ou=posix,${dbSuffix} + objectClass: top + objectClass: organizationalUnit + + dn: ou=accounts,ou=posix,${dbSuffix} + objectClass: top + objectClass: organizationalUnit + + dn: ou=groups,ou=posix,${dbSuffix} + objectClass: top + objectClass: organizationalUnit + '' + + lib.concatMapStrings posixAccount [ + { uid=ldapUser; uidNumber=ldapUserId; gidNumber=ldapGroupId; userPassword=ldapUserPwdHash; } + ] + + lib.concatMapStrings posixGroup [ + { gid=ldapGroup; gidNumber=ldapGroupId; members=[]; } + ]; + }; + }; + + # NOTE: create a user account using the posixAccount objectClass. + posixAccount = + { uid + , uidNumber ? null + , gidNumber ? null + , cn ? "" + , sn ? "" + , userPassword ? "" + , loginShell ? "/bin/sh" + }: '' + + dn: uid=${uid},ou=accounts,ou=posix,${dbSuffix} + objectClass: person + objectClass: posixAccount + objectClass: shadowAccount + cn: ${cn} + gecos: + ${if gidNumber == null then "#" else "gidNumber: ${toString gidNumber}"} + homeDirectory: /home/${uid} + loginShell: ${loginShell} + sn: ${sn} + ${if uidNumber == null then "#" else "uidNumber: ${toString uidNumber}"} + ${if userPassword == "" then "#" else "userPassword: ${userPassword}"} + ''; + + # NOTE: create a group using the posixGroup objectClass. + posixGroup = + { gid + , gidNumber + , members + }: '' + + dn: cn=${gid},ou=groups,ou=posix,${dbSuffix} + objectClass: top + objectClass: posixGroup + gidNumber: ${toString gidNumber} + ${lib.concatMapStrings (member: "memberUid: ${member}\n") members} + ''; + in { virtualisation.memorySize = 256; virtualisation.vlans = [ 1 ]; networking.firewall.allowedTCPPorts = [ 389 ]; services.openldap.enable = true; - services.openldap.dataDir = dbPath; + services.openldap.dataDir = "/var/db/openldap"; + services.openldap.configDir = "/var/db/slapd"; services.openldap.urlList = [ - serverUri + "ldap:///" + "ldapi:///" ]; - services.openldap.extraConfig = '' - include ${pkgs.openldap.out}/etc/schema/core.schema - include ${pkgs.openldap.out}/etc/schema/cosine.schema - include ${pkgs.openldap.out}/etc/schema/inetorgperson.schema - include ${pkgs.openldap.out}/etc/schema/nis.schema - - database mdb - suffix ${dbSuffix} - rootdn ${dbAdminDn} - rootpw ${dbAdminPwd} - directory ${dbPath} - ''; + systemd.services.openldap = { + preStart = '' + set -e + # NOTE: slapd's config is always re-initialized. + rm -rf "${openldap.configDir}"/cn=config \ + "${openldap.configDir}"/cn=config.ldif + install -D -d -m 0700 -o "${openldap.user}" -g "${openldap.group}" "${openldap.configDir}" + # NOTE: olcDbDirectory must be created before adding the config. + '' + + unlinesAttrs (olcSuffix: {data, ...}: '' + # NOTE: database is always re-initialized. + rm -rf "${openldap.dataDir}/${olcSuffix}" + install -D -d -m 0700 -o "${openldap.user}" -g "${openldap.group}" \ + "${openldap.dataDir}/${olcSuffix}" + '') slapdDatabases + + '' + # NOTE: slapd is supposed to be stopped while in preStart, + # hence slap* commands can safely be used. + umask 0077 + ${pkgs.openldap}/bin/slapadd -n 0 \ + -F "${openldap.configDir}" \ + -l ${slapdConfig} + chown -R "${openldap.user}:${openldap.group}" "${openldap.configDir}" + # NOTE: slapadd(8): To populate the config database slapd-config(5), + # use -n 0 as it is always the first database. + # It must physically exist on the filesystem prior to this, however. + '' + + unlinesAttrs (olcSuffix: {data, ...}: '' + # NOTE: load database ${olcSuffix} + # (as root to avoid depending on sudo or chpst) + ${pkgs.openldap}/bin/slapadd \ + -F "${openldap.configDir}" \ + -l ${pkgs.writeText "data.ldif" data} + '' + '' + # NOTE: redundant with default openldap's preStart, but do not harm. + chown -R "${openldap.user}:${openldap.group}" \ + "${openldap.dataDir}/${olcSuffix}" + '') slapdDatabases; + }; }; client1 = mkClient true; # use nss_pam_ldapd @@ -91,15 +293,91 @@ in }; testScript = '' - startAll; + $server->start; $server->waitForUnit("default.target"); + + subtest "slapd", sub { + subtest "auth as database admin with SASL and check a POSIX account", sub { + $server->succeed(join ' ', 'test', + '"$(ldapsearch -LLL -H ldapi:// -Y EXTERNAL', + '-b \'uid=${ldapUser},ou=accounts,ou=posix,${dbSuffix}\' ', + '-s base uidNumber |', + 'sed -ne \'s/^uidNumber: \\(.*\\)/\\1/p\' ', + ')" -eq ${toString ldapUserId}'); + }; + subtest "auth as database admin with password and check a POSIX account", sub { + $server->succeed(join ' ', 'test', + '"$(ldapsearch -LLL -H ldap://server', + '-D \'cn=admin,${dbSuffix}\' -w \'${dbAdminPwd}\' ', + '-b \'uid=${ldapUser},ou=accounts,ou=posix,${dbSuffix}\' ', + '-s base uidNumber |', + 'sed -ne \'s/^uidNumber: \\(.*\\)/\\1/p\' ', + ')" -eq ${toString ldapUserId}'); + }; + }; + + $client1->start; $client1->waitForUnit("default.target"); + + subtest "password", sub { + subtest "su with password to a POSIX account", sub { + $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';', + 'spawn su "${ldapUser}"', + 'expect "Password:"', + 'send "${ldapUserPwd}\n"', + 'expect "*"', + 'send "whoami\n"', + 'expect -ex "${ldapUser}" {exit}', + 'exit 1' . "'"); + }; + subtest "change password of a POSIX account as root", sub { + $client1->succeed("chpasswd <<<'${ldapUser}:new-password'"); + $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';', + 'spawn su "${ldapUser}"', + 'expect "Password:"', + 'send "new-password\n"', + 'expect "*"', + 'send "whoami\n"', + 'expect -ex "${ldapUser}" {exit}', + 'exit 1' . "'"); + $client1->succeed('chpasswd <<<\'${ldapUser}:${ldapUserPwd}\' '); + }; + subtest "change password of a POSIX account from itself", sub { + $client1->succeed('chpasswd <<<\'${ldapUser}:${ldapUserPwd}\' '); + $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';', + 'spawn su --login ${ldapUser} -c passwd', + 'expect "Password: "', + 'send "${ldapUserPwd}\n"', + 'expect "(current) UNIX password: "', + 'send "${ldapUserPwd}\n"', + 'expect "New password: "', + 'send "new-password\n"', + 'expect "Retype new password: "', + 'send "new-password\n"', + 'expect "passwd: password updated successfully" {exit}', + 'exit 1' . "'"); + $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';', + 'spawn su "${ldapUser}"', + 'expect "Password:"', + 'send "${ldapUserPwd}\n"', + 'expect "su: Authentication failure" {exit}', + 'exit 1' . "'"); + $client1->succeed("${pkgs.expect}/bin/expect -c '" . join ';', + 'spawn su "${ldapUser}"', + 'expect "Password:"', + 'send "new-password\n"', + 'expect "*"', + 'send "whoami\n"', + 'expect -ex "${ldapUser}" {exit}', + 'exit 1' . "'"); + $client1->succeed('chpasswd <<<\'${ldapUser}:${ldapUserPwd}\' '); + }; + }; + + $client2->start; $client2->waitForUnit("default.target"); - $server->succeed("ldapadd -D '${dbAdminDn}' -w ${dbAdminPwd} -H ${serverUri} -f '${setupLdif}'"); - - # NSS tests - subtest "nss", sub { + subtest "NSS", sub { $client1->succeed("test \"\$(id -u '${ldapUser}')\" -eq ${toString ldapUserId}"); $client1->succeed("test \"\$(id -u -n '${ldapUser}')\" = '${ldapUser}'"); $client1->succeed("test \"\$(id -g '${ldapUser}')\" -eq ${toString ldapGroupId}"); @@ -110,8 +388,7 @@ in $client2->succeed("test \"\$(id -g -n '${ldapUser}')\" = '${ldapGroup}'"); }; - # PAM tests - subtest "pam", sub { + subtest "PAM", sub { $client1->succeed("echo ${ldapUserPwd} | su -l '${ldapUser}' -c true"); $client2->succeed("echo ${ldapUserPwd} | su -l '${ldapUser}' -c true"); };