Merge pull request #53762 from ju1m/nslcd

Improving integration of `nslcd`, PAM and `openldap`.
This commit is contained in:
Florian Klink 2019-01-30 19:34:40 +01:00 committed by GitHub
commit d3c2ed21d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 389 additions and 66 deletions

View file

@ -331,17 +331,29 @@
<para>
The <literal>pam_unix</literal> account module is now loaded with its
control field set to <literal>required</literal> instead of
<literal>sufficient</literal>, so that later pam account modules that
<literal>sufficient</literal>, 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
<literal>pam_unix</literal>.
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
<literal><![CDATA[security.pam.services.<name?>.text]]></literal>.
</para>
</listitem>
<listitem>
<para>
The <literal>pam_unix</literal> password module is now loaded with its
control field set to <literal>sufficient</literal> instead of
<literal>required</literal>, 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 <literal>pam_unix</literal>,
preventing <literal>pam_ldap</literal> to manage the password as it should.
</para>
</listitem>
<listitem>
<para>

View file

@ -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,21 +256,31 @@ in
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p /run/nslcd
rm -f /run/nslcd/nslcd.pid;
chown nslcd.nslcd /run/nslcd
${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`
# (which is only able to use `/etc/nslcd.conf`)
# changes in `nslcdConfig` won't change `serviceConfig`,
# and thus won't restart `nslcd`.
# Therefore `restartTriggers` is used on `/etc/nslcd.conf`.
restartTriggers = [ nslcdConfig.source ];
serviceConfig = {
ExecStart = "${nss_pam_ldapd}/sbin/nslcd";
Type = "forking";
PIDFile = "/run/nslcd/nslcd.pid";
Restart = "always";
RuntimeDirectory = [ "nslcd" ];
};
};

View file

@ -370,7 +370,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

View file

@ -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");
};