Cover V13, i05
may2004.tar

Configuring Linux for LDAP Name Service and Authentication

John D'Emic

Securely distributing account information is a common challenge faced by systems administrators. It is a tedious chore -- even in a small environment -- to individually log into multiple servers to change a single user's password. In the past, mechanisms such as NIS and NIS+ were used to fill this role. These services allowed the administrator to control user access from a single location, allowing him to quickly add accounts, change passwords, manage groups, and delete users. NIS and NIS+, however, have both fallen from favor because of security issues, compatibility concerns, or supportability. For Linux hosts, OpenLDAP is quickly emerging as the basis for centralized name service. Coupled with PADL software's PAM and NSS libraries, a complete open source suite of distributing user management is available for Linux.

Overview

The term "name service" refers to abstracting flat data -- like an /etc/hosts file or (more relevant to this discussion) an /etc/passwd file -- onto a single facility that is able to share the data to all entities that need it. The best and most successful example of this is the Domain Name Service (DNS). Before DNS, administrators had to manually keep the /etc/hosts file on all of their servers up to date. Today, administrators just point their servers at a DNS server and the system is able to perform hostname resolution. We ultimately want to be able to do the same thing with /etc/passwd, /etc/shadow, and /etc/group entries. The facilities that allow us to do this are OpenLDAP, nss_ldap, PAM, and pam_ldap.

OpenLDAP is a free implementation of the LDAP protocol, including libraries, utilities, clients, and servers. This article assumes some familiarity with basic LDAP terminology and concepts. You can refer to previous articles in Sys Admin magazine for more details. I will present the information currently provided by /etc/passwd, /etc/shadow, and /etc/group as LDAP directory entries served by the LDAP server, called slapd, provided with OpenLDAP.

nss_ldap, provided by PADL software, is an open source name service switching library that utilizes LDAP. nss_ldap allows you to configure a system's name service switching (via /etc/nsswitch.conf) to look up (among other things) passwd, group, and shadow data in an LDAP directory.

PAM (Pluggable Authentication Modules) is a facility to provide authentication mechanisms to different programs. pam_ldap, also freely provided by PADL software, lets you use LDAP binding as an authentication mechanism for such thing as system login, ssh logins, and sudo.

In this article, I will describe how to configure slapd, the OpenLDAP directory server, to serve authentication data. Then, by using nss_ldap and pam_ldap, I will show how to configure clients to retrieve naming and authentication information from the LDAP server without relying on local files.

Installing the Software

The three major software packages needed to accomplish the above tasks are OpenLDAP 2.1, pam_ldap, and nss_ldap. Your Linux distribution likely comes shipped with some or all of the above, and you are encouraged to use your distro's packaged versions of them. There can be library dependencies and specific patches for your operating system (especially for TLS and database-backened configuration) that your distro may have already worked out for you. If you must do it yourself, here's how:

Configuring OpenLDAP

Download the most recent stable sources from:

http://www.openldap.org
Unpack and configure as follows:

./configure --with-slapd \
            --with-slurpd --without-ldapd --enable-local \
            --disable-rlookups --with-tls --with-cyrus-sasl \
            --enable-wrappers --enable-passwd --enable-shell \
            --enable-cleartext --enable-crypt --enable-spasswd
You'll need to choose a backend database, which will likely be either ldbm or bdb, depending on what is installed on your system. Assuming you want to configure with LDBM and Berkeley DB support, add the following configure lines to the above:

--enable-bdb --enable-ldbm
Once that's done:

make depend
make
make install
Use groupadd and useradd to add a group and an account to the system to run slapd as:

addgroup ldap
useradd -g ldap -s /bin/false -d /var/run ldap
Configuring nss_ldap and pam_ldap

Again, you are encouraged to use your distro's packaged versions of pam_ldap and nss_ldap, but if you must compile them on your own, here's how:

Download nss_ldap.tgz from:

http://www.padl.com/OSS/nss_ldap.html
Unpack, then:

./configure --enable-rfc2307bis \
            --with-ldap-lib=openldap \
            --with-ldap-conf-file=/etc/libnss-ldap.conf \
            --enable-schema-mapping \
            --enable-ssl
 make
 make
 install
Download pam_ldap.tgz from:

http://www.padl.com/OSS/pam_ldap.html
Then:

 ./configure --with-ldap-lib=openldap \
             --with-ldap-conf-file=/etc/pam_ldap.conf \
             --enable-ssl
 make
 make install
 
Planning the Directory

Planning the logical layout of the directory is an important first step. While this article only covers LDAP from a naming service and authentication perspective, LDAP can be used to store a myriad of other data. You may be required eventually to store users' telephone numbers, addresses, and mail delivery information in LDAP, for example, and it's important to bear that in mind.

In particular, think carefully about how your organizational units will be structured to make name space collisions (different objects with the same name) less frequent. If your organization is relatively small, it might be sufficient to have a single organizational unit (ou) for all users (e.g., "ou=people,dc=example,dc=net"). However, if you're setting up the directory for a large organization with multiple departments, you may want to have an organizational unit for each department. For example, if you were setting up authentication for your engineering group, your sales group, and one of your customers, you would probably have the following (see Figure 1):

"ou=engr,ou=emp,dc=example,dc=net"
"ou=sales,ou=emp,dc=example,dc=net"
"ou=acme,ou=cust,dc=example,dc=net"
In the following examples, we'll be looking at the imaginary "example.net" domain. The base domain will be "dc=example,dc=net". To keep things simple, we will create two organizational units -- one called "users" and another called "group". All of the user data (the data currently in /etc/passwd) will be stored under the "users" ou, and all of the group data (the data currently in /etc/group) will be stored under the "group" ou (see Figure 2):

"ou=users,dc=example,dc=net"
"ou=group,dc=example,dc=net"
Configuring slapd

As mentioned above, slapd is the directory server provided with OpenLDAP. We'll need to configure this before we can populate our user accounts. The server you choose to run this on should be as secure as possible, as it will be housing user passwords. Although the passwords are stored hashed, an attacker able to compromise the directory could attempt to launch a brute force attack on the hashes.

The configuration file for slapd is called slapd.conf. The default location for it is usually /etc/openldap or /etc/ldap. A very minimal slapd.conf file is provided in Listing 1.

The first four lines of the listing define the schema files needed to represent user and group data in the directory. The "security" keyword on line 6 defines the minimal security level we are allowing for client binds. This line will guarantee 128-bit encryption for anonymous binds between the client and the server. This will ensure that pam_ldap will communicate to the server over an encrypted channel.

The next two lines define the TLS certificate and key files. You're encouraged to put the certificate and key in the same file and call it something like slapd.pem. chown the file to user/group ldap:ldap and give it a permissions mask of 400. Guaranteeing encryption between the clients and the server makes it harder for an attacker to snoop authentication traffic.

By default, slapd is set up to allow anyone to read any entry in the directory. This has obvious implications for authentication data. Our goal is to mirror the filesystem permissions of traditional passwd, group, and shadow files as ACLs in slapd.conf. The passwd and group data should be readable by anybody. Shadow data (passwords) should only be readable by the rootdn. The following is the minimum set of ACLs that accomplish this:

access to attrs=userPassword
    by dn="cn=Manager,dc=example,dc=net" write
    by self write
    by * auth

access to *
    by * read
The above will restrict the userPassword attribute to the user who owns the record and to the rootdn (so either can change the password) and for everybody else to "auth" with. The latter means that a user not bound to the LDAP server can compare a hash of a password against the password hash in the LDAP directory. slapd returns a true value if the hashes match. This is how the system determines whether the password entered by a user is correct, without transmitting the cleartext password over the wire. Because the ACLs in slapd.conf are evaluated in a top-down manner, the second access stanza allows access to the rest of the directory (i.e., passwd and group information).

The next two lines define the backend database that slapd will use to store the directory records. Your backend database will likely be either bdb or ldbm. If you (or your distribution) configured OpenLDAP to use dynamic modules, you will need to uncomment the "moduleload" line and have it point to the proper module to support your backend database.

The "suffix" keyword indicates the base dn of the directory that slapd will be serving. For our purposes, it is "dc=example,dc=net".

The rootdn is defined next. This is the equivalent of a superuser account for the directory. You will be binding as this user later to add accounts. The next line sets a password for the rootdn. You can put a plaintext password here, but you're encouraged to use the slappasswd command to generate a hashed one:

slappasswd -s "password"
You can then cut and paste the output into the file.

The directory line defines the path to the backend database files. You want this directory to be user and group owned by user "ldap", with a permission mask of 400. The last two lines indicate the indices that need to be maintained.

At this point, your slapd configuration should be good to go. You can start slapd either by running something like /sbin/service ldap start or by manually doing /usr/bin/slapd -u ldap.

If the startup fails, you can invoke slapd with debugging mode to see what is going on:

/usr/sbin/slapd -u ldap -d99 -h ldap:///
Once slapd is running, use ldapsearch to make sure you can bind to it:

ldapsearch -Z -x -h ldap.example.net
You should get something back like "result: 32 No such object". This is fine because the directory is empty. The -Z argument above tells ldapsearch to use TLS to connect to the ldapserver. Let's leave that out and see what happens:

ldapsearch -x -h ldap.example.net
If you've configured everything correctly, you should get "confidentiality required" back from slapd. slapd is rejecting your bind because the connection is not encrypted. Finally, the -x flag says you want to do a simple bind to the directory server. Since we're not specifying a user, this is called an anonymous bind.

Populating the Directory

Now that slapd is running, we can add entries to the directory. We will set up the layout scheme discussed before, with our base dn being "dc=example,dc=net" and one organizational unit called "users":

ldapadd -Z -x -D "cn=Manager,dc=example,dc=net" -W -h
ldap.example.net Enter LDAP Password: <rootdn password typed here>

dn: dc=example,dc=net
objectClass: dcObject
objectClass: organization
o: example
dc: example

<cntrl-d>

dn: ou=users,dc=example,dc=net
objectClass: organizationalUnit
ou: users

<cntrl-d>

dn: ou=group,dc=example,dc=net
objectClass: organizationalUnit
ou: group

<cntrl-d>

<cntrl-c>
Note that you could have put the above LDIFs (a textual representation of an ldap directory entry) into a file and passed that to ldapadd:

ldapadd -Z -x -D "cn=Manager,dc=example,dc=net -W -h ldap.example.net
  -f ./my.ldif
The -D argument to ldapadd specifies the dn of the user we want to bind as. Since we're modifying the directory, we used the rootdn. Remember that we gave the rootdn a hard-coded password in the slapd.conf file. We need to pass the -W flag to indicate that we will interactively be entering that password. We could have alternatively used -w "rootdn password here", specifying the password on the command line. Once you've done the above, use ldapsearch to confirm the entries were added.

Adding Users to the Directory

Now that the top-level organizational units are set up, we can add accounts to the directory. I'll describe how to manually add an account first, then look at batch adding accounts from existing /etc/passwd entries.

Creating a user, whether in flat files or in LDAP, requires the following pieces of data:

Username UID
GID
Homedirectory
Gecos Field
A hashed password

Next, we'll create an account for "Joe User". His username will be "juser", he will have a uid of "503", his default group will have a gid of "503", and his login shell will be "/bin/tcsh". His home directory will be /home/juser, and his gecos field will be "Joe User". We'll use the slappasswd command as before to generate a password for him. This system supports MD5-hashed passwords, so we'll pass that to slappasswd to get:

slappasswd -h "{MD5}" -s "mypassword"
That gives us:

{MD5}NIGde+6ruSYKXIVLyFs+RA==
for Joe User's password. (If your system does not support MD5 passwords, replace MD5 with CRYPT above.)

Next, we want to represent the above information in LDIF. In this example, I'll add the following to a file called juser.ldif, which will define juser's account and group data:

dn: uid=juser,ou=users,dc=example,dc=net
uid: juser
cn: juser
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
uidNumber: 503
gidNumber: 503
homeDirectory: /home/juser
gecos: Joe User
userPassword: {MD5}NIGde+6ruSYKXIVLyFs+RA==

dn: cn=juser,ou=group,dc=example,dc=net
objectClass: top
objectClass: posixGroup
cn: juser
gidNumber: 503
memberUid: juser
Once that's done, add the user to the directory:

ldapadd -Z -x -D "cn=Manager,dc=example,dc=net -W -h
ldap.example.net -f juser.ldif
Next, use ldapsearch to confirm Joe's account is there:

ldapsearch -Z -x "uid=juser"
Notice that Joe's hashed password is not being displayed. This is because we restricted the userPassword attribute in our ACLs. If you search again, but bind as the rootdn, you should see the password:

ldapsearch -Z -x -W -D "cn=Manager,dc=example,dc=net"
"uid=juser"
Batch Populating the Directory

You could manually add all your existing users and groups using the method above, or you could batch populate the directory using existing flat files. PADL software publishes a suite of Perl scripts that make doing this fairly simple. They are available from:

http://www.padl.com/OSS/MigrationTools.html
We will use the migrate_passwd.pl and migrate_group.pl files to generate an LDIF file that will then be used to populate the directory with the /etc/passwd and /etc/group data. Once you've untar'ed the MigrationTools, copy your /etc/passwd and /etc/group files to another directory. Edit the copies and remove the users and groups that you want to maintain in the flat files. Leave system accounts, such as root, daemon, sys, etc., in the flat files so the system can still function properly if it is in single-user mode or if the directory server cannot be reached. Once this is done, back up and open the migrate_common.ph file in an editor. Find the line that reads:

$NAMINGCONTEXT{'passwd'} = "ou=People";
Change "People" to reflect the ou under which we're placing our users. In this example, the ou is called "users", so we'll change that line to look like this:

$NAMINGCONTEXT{'passwd'} = "ou=users";
Next find the $DEFAULT_BASE variable and change it to reflect your base dn:

$DEFAULT_BASE = "dc=example,dc=net"
Finally, run the Perl scripts against your edited copies of /etc/passwd and /etc/group:

perl migrate_passwd.pl ./passwd_edited passwd.ldif perl
migrate_group.pl ./group_edited group.ldif
Your flat /etc/passwd and /etc/group data should now be in LDIF format in the passwd.ldif and group.ldif files. Now we can populate the directory with this data:

ldapadd -Z -x -D "cn=Manager,dc=example,dc=net" -W -h \
ldap.example.net -f ./passwd.ldif 

ldapadd -Z -x -D \
"cn=Manager,dc=example,dc=net" -W -h ldap.example.net -f \
./group.ldif
Once your user data is in LDAP, you can remove the user and group entries from /etc/passwd, /etc/shadow, and /etc/group. And, after the directory is populated, you can start configuring clients.

Configuring the Client

We're now ready to set up pam_ldap and nss_ldap on our clients.

nsswitch.conf

This determines the methods and order that the system's resolver libraries use to gather name service data. We want to append the "ldap" keyword to the passwd, shadow, and group definitions as follows:

passwd: files ldap
shadow: files ldap
group:  files ldap
(Your system may have "compat" instead of "files" -- this is fine.) Listing 2 contains a sample /etc/nsswitch.conf file.

nss_ldap/pam_ldap

Depending on your Linux distribution, you may have separate config files for pam_ldap and nss_ldap. On other systems, they both use the same file. On Red Hat, for example, /etc/ldap.conf contains the configuration for both pam_ldap and nss_ldap. On Debian, they are broken out in /etc/pam_ldap.conf and /etc/libnss_ldap.conf. In either case, the contents of the file are the same for our purposes. Listing 3 contains the config file.

Now we can test name service switching. At this point, we should be able to do finger juser and get back the user's account information from ldap. Go ahead and try this. Also try fingering the accounts that have been batch added. Once this is working, we just need to finish the PAM configuration and we can call it a day.

Completing the PAM Configuration

Every service in the system that wants to take advantage of PAM will have a conf file in /etc/pam.d. Some systems have a global PAM conf file sourced by other files in this directory. Red Hat is a good example of the latter; its PAM configuration is centralized in /etc/pam.d/system-auth. Debian also uses this method, except it sources the /etc/pam.d/common-* files. Listing 4 contains a working /etc/pam.d/system-auth for Red Hat systems, and Listing 5 contains working /etc/pam.d/common-auth, /etc/pam.d/common-account, /etc/pam.d/common-password, and /etc/pam.d/common-session for Debian.

Once the PAM configurations are in place, you can test ssh:

ssh juser@localhost
This should log you into the server (if it doesn't work, you may need to restart your ssh daemon). If you need to troubleshoot further, you can put the "debug" keyword after the pam_ldap.so line in the PAM configs as follows:

auth sufficient pam_ldap.so try_first_pass debug
Next, try changing the password of the "juser" account:

passwd juser Enter login(LDAP) password:
New password:
Re-enter new password:
LDAP password information changed
for juser passwd: password updated successfully
Try ssh'ing into the account to make sure the password change was successful.

Conclusion

This article has provided an introduction to centralizing account information on an LDAP directory. Once you're comfortable with your configuration, you may also want to investigate directory replication with slurpd, Kerberos integration, SASL binds, shadow extensions, and other things that I was unable to cover here due to space constraints.

John D'Emic has been a systems administrator and on again, off again programmer for the past 7 years. He has a computer science degree from St. John's University and is currently employed by Opsource, Inc, splitting his time between system engineering and coding. He can be reached at: jdemic@opsource.net.