diff --git a/nixos/doc/manual/release-notes/rl-1903.xml b/nixos/doc/manual/release-notes/rl-1903.xml
index 89d9f48aedd3..0fec5accccd5 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 188232219908..f65a3fc50d54 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 b1a0eff98c20..a2eb6b1df109 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 035a81924174..b3fd42e75886 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");
};