8817bbefdb
eb90d9700958aefbc7b886f2b524c6d04dc1d80d broke nslcd, as /run/nslcd was created/chowned as root user, while nslcd wants to do parts as nslcd user. This commit changes the nslcd to run with the proper uid/gid from the start (through User= and Group=), so the RuntimeDirectory has proper permissions, too. In some cases, secrets are baked into nslcd's config file during startup (so we don't want to provide it from the store). This config file is normally hard-wired to /etc/nslcd.conf, but we don't want to use PermissionsStartOnly anymore (#56265), and activation scripts are ugly, so redirect /etc/nslcd.conf to /run/nslcd/nslcd.conf, which now gets provisioned inside ExecStartPre=. This change requires the files referenced to in users.ldap.bind.passwordFile and users.ldap.daemon.rootpwmodpwFile to be readable by the nslcd user (in the non-nslcd case, this was already the case for users.ldap.bind.passwordFile) fixes #57783
396 lines
15 KiB
Nix
396 lines
15 KiB
Nix
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";
|
|
dbAdminDn = "cn=admin,${dbSuffix}";
|
|
dbAdminPwd = "admin-password";
|
|
# NOTE: slappasswd -h "{SSHA}" -s '${dbAdminPwd}'
|
|
dbAdminPwdHash = "{SSHA}i7FopSzkFQMrHzDMB1vrtkI0rBnwouP8";
|
|
ldapUser = "test-ldap-user";
|
|
ldapUserId = 10000;
|
|
ldapUserPwd = "user-password";
|
|
# NOTE: slappasswd -h "{SSHA}" -s '${ldapUserPwd}'
|
|
ldapUserPwdHash = "{SSHA}v12XICMZNGT6r2KJ26rIkN8Vvvp4QX6i";
|
|
ldapGroup = "test-ldap-group";
|
|
ldapGroupId = 10000;
|
|
|
|
mkClient = useDaemon:
|
|
{ lib, ... }:
|
|
{
|
|
virtualisation.memorySize = 256;
|
|
virtualisation.vlans = [ 1 ];
|
|
security.pam.services.su.rootOK = lib.mkForce false;
|
|
users.ldap.enable = true;
|
|
users.ldap.daemon = {
|
|
enable = useDaemon;
|
|
rootpwmoddn = "cn=admin,${dbSuffix}";
|
|
rootpwmodpwFile = "/etc/nslcd.rootpwmodpw";
|
|
};
|
|
users.ldap.loginPam = true;
|
|
users.ldap.nsswitch = true;
|
|
users.ldap.server = "ldap://server";
|
|
users.ldap.base = "ou=posix,${dbSuffix}";
|
|
users.ldap.bind = {
|
|
distinguishedName = "cn=admin,${dbSuffix}";
|
|
passwordFile = "/etc/ldap/bind.password";
|
|
};
|
|
# NOTE: passwords stored in clear in Nix's store, but this is a test.
|
|
environment.etc."ldap/bind.password".source = pkgs.writeText "password" dbAdminPwd;
|
|
environment.etc."nslcd.rootpwmodpw".source = pkgs.writeText "rootpwmodpw" dbAdminPwd;
|
|
};
|
|
in
|
|
|
|
{
|
|
name = "ldap";
|
|
meta = with pkgs.stdenv.lib.maintainers; {
|
|
maintainers = [ montag451 ];
|
|
};
|
|
|
|
nodes = {
|
|
|
|
server =
|
|
{ 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 = "/var/db/openldap";
|
|
services.openldap.configDir = "/var/db/slapd";
|
|
services.openldap.urlList = [
|
|
"ldap:///"
|
|
"ldapi:///"
|
|
];
|
|
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
|
|
client2 = mkClient false; # use nss_ldap and pam_ldap
|
|
|
|
};
|
|
|
|
testScript = ''
|
|
$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");
|
|
|
|
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}");
|
|
$client1->succeed("test \"\$(id -g -n '${ldapUser}')\" = '${ldapGroup}'");
|
|
$client2->succeed("test \"\$(id -u '${ldapUser}')\" -eq ${toString ldapUserId}");
|
|
$client2->succeed("test \"\$(id -u -n '${ldapUser}')\" = '${ldapUser}'");
|
|
$client2->succeed("test \"\$(id -g '${ldapUser}')\" -eq ${toString ldapGroupId}");
|
|
$client2->succeed("test \"\$(id -g -n '${ldapUser}')\" = '${ldapGroup}'");
|
|
};
|
|
|
|
subtest "PAM", sub {
|
|
$client1->succeed("echo ${ldapUserPwd} | su -l '${ldapUser}' -c true");
|
|
$client2->succeed("echo ${ldapUserPwd} | su -l '${ldapUser}' -c true");
|
|
};
|
|
'';
|
|
})
|