Using OpenLDAP for User Authentication

NOTE: This is a revision of the previous LDAP authentication article on MandrakeSecure. A second revision is available on that site, but it is also more or less specific to Mandrakelinux whereas this topic will eventually cover as many different Linux and BSD implementations as possible to allow individuals to use a completely mixed environment with one central authentication system.

User authentication for logins is generally a no brainer. You setup users on the local system and off you go… nothing to it. However, if you’re on a LAN and you want to have a centralized “repository” of users, you will likely be looking at some method of distributing user information across the LAN. This has a few distinct advantages, the primary being all user authentication is centralized. This means that users have the same password on each system in the LAN, and if they change their password, the password is seamlessly changed everywhere. This provides the advantage of giving consistency to user authentication on the LAN. Users retain the same userid, groupid, password, and other information. This can be problematic if you assign users different levels of access on different machines, but if you permit the same access on all systems, this is an easy way to do it. Regardless, with sudo, you can fine-tune privileged access on a host-by-host basis as well.

Traditionally, NIS (Network Information Services, aka YP (Yellow Pages)) was used to provide this sort of information. NIS is an RPC-based protocol similar to NFS. And while NIS may work well enough in most cases, it doesn’t work well in all cases (personal experience here has shown NIS to be anything but reliable). However, there is another choice, and that choice is LDAP. Mandrakelinux provides OpenLDAP and this is the starting block of what is required for a distributed authentication system.

There are few tutorials on how to accomplish using LDAP for authentication, and I found them to be difficult to understand or incomplete, and as a result some research and testing was done to setup LDAP-based authentication on Mandrakelinux. This was originally done using Mandrakelinux 8.2, and all later versions of Mandrakelinux operate in the same way. With other distributions or vendors, you may have to tweak a few things (this particular article is specific to Mandrakelinux). The information here should be enough to get you started, if not help you finish everything off. With the latest OpenLDAP update (MDKA-2003:009), you should be able to follow this tutorial completely and have a working authentication system. Users of other distributions may have to patch their OpenLDAP packages in order to get the same results.

Pre-requisites

The first things you need to do is ensure that OpenLDAP is properly installed, along with a few optional packages that will tie our system together. Obviously, the first step is to install OpenLDAP. The packages we need to have installed (on a Mandrakelinux system) are:

  • libldap2
  • openldap
  • openldap-clients
  • openldap-migration
  • openldap-servers
  • nss_ldap
  • pam_ldap

The openldap-servers and openldap-migration packages are only required on the system that will be your authentication server. They are not required on the “client” systems.

The pam_ldap and nss_ldap packages are required for PAM authentication and for NSS information (ie. retrieving group, user, host, etc. information from the LDAP server). Once you have all of these packages installed, you can begin to configure your LDAP server.

Configuring the OpenLDAP Server

The first step in configuring your server is to edit the /etc/openldap/slapd.conf file. There are a few fields you will need to configure. In this topic, we will assume that your domain name to use on the LAN is “mylan.net” and will illustrate our configuration accordingly.

database        ldbm
suffix          "dc=mylan,dc=net"
rootdn          "cn=root,dc=mylan,dc=net"
rootpw          {MD5}zYgLcm4KDb1CN/ENGdpG9A==
directory       /var/lib/ldap
index           objectClass,uid,uidNumber,gidNumber eq
index           cn,mail,surname,givenname           eq,subinitial
password-hash   {crypt}
password-crypt-salt-format      "$1$%.8s"

There is much more in your slapd.conf file, but we’ll only change these options to begin with. Here you are setting your domain, along with the root user and root’s password. You are also setting up some indexes so that queries to the LDAP server do not take so long. These are the default index settings for slapd.conf but you should make note of them. They should be sufficient for your system. Finally, you are setting the default password format for the LDAP server. This tells the LDAP server to use the “{crypt}” (or crypt(3)) hash, with the noted salt format, which translates to a standard crypt(3) MD5 hash. This will allow a user to authenticate to the operating system and the LDAP server with the same password (and same password format).

To obtain the value for the rootpw keyword, use the slappasswd utility like this:

[root@ldap]# slappasswd -h {MD5}

You will be asked for a password and then slappasswd will spit out the MD5 string that corresponds to your chosen password. Cut and paste this string into your file as the rootpw string. There are a few different types of passwords you can use, but I prefer to use MD5 passwords. You can also use SSHA (the default), CRYPT, SMD5, or SHA by specifying the name to the -h parameter, which is the hash parameter. For instance, if you wanted to use crypt passwords you would use “-h {CRYPT}”.

Before configuring your basic ACLs, let’s start slapd and make sure it works.

[root@ldap]# service ldap start

Once you have started the slapd server, you can test it by executing the following query:

[root@ldap]# ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts
version: 2

#
# filter: (objectclass=*)
# requesting: namingContexts
#

#
dn:
namingContexts: dc=mylan,dc=net

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

If you see something similar to the above, ldap is installed and working properly. If not, then go back and ensure you haven’t tinkered too much yet. We currently have the LDAP server running, but not populated. To have slapd start on boot each time, execute:

[root@ldap]# chkconfig ldap on

Configuring Server ACLs

The final step on the server before we begin migrating data is to set the basic ACLs (Access Control Lists) for the LDAP server. This will ensure that people only have access to what they need to have access to, and will allow users to update their passwords, see their passwords, but prevent others from seeing the same.

Once again you need to edit the /etc/openldap/slapd.conf file. If you look in your slapd.conf file, you will see the following near the beginning:

# Define global ACLs to disable default read access.
include /etc/openldap/slapd.access.conf

The /etc/openldap/slapd.access.conf file is as good as any to place your ACLs in. You can either append your ACL rules to the end of the slapd.conf file, or insert them into slapd.access.conf. The choice is entirely up to you. If you do choose to use the access file, remove the example ACLs at the end of the slapd.conf file. Let’s begin with some basic ACLs:

# This is a good place to put slapd access-control directives

access to dn=".*,dc=mylan,dc=net" attr=userPassword
        by dn="cn=root,dc=mylan,dc=net" write
        by self write
        by * auth

access to dn=".*,dc=mylan,dc=net" attr=mail
        by dn="cn=root,dc=mylan,dc=net" write
        by self write
        by * read

access to dn=".*,ou=People,dc=mylan,dc=net"
        by * read

access to dn=".*,dc=mylan,dc=net"
        by self write
        by * read

What this does is restrict access to the userPassword attribute of any entry; that is, any dn in dc=mylan,dc=net. The owner of the entry can modify it, and the owner is defined by someone binding to the server using that dn and it’s associated password. Otherwise, it can only be accessed for authentication/binding purposes, but cannot be viewed. The second entry allows the user to modify their mail attribute (ie. email address). The third entry specifies that any dn in ou=People,dc=mylan,dc=net must be read-only. This is where we protect the system from users deciding to change their username, gid or uid numbers, home directory, and so forth. Because the ACLs are read top down in a “first match wins” order, we have effectively given users access to change their own password and their own email address, but they are unable to touch any other information on their account. Everything else is read-only… to the world, and the user. Finally, the last entry is a catch-all for other parts of the database. This will allow users to make changes to their own address books, for example. If you will not be allowing users to use their own address books on this LDAP server, feel free to remove the “by self write” ACL of the last entry. This will still allow users to read group and hosts information. If you like, you can duplicate the second entry to allow users to modify their loginShell attribute so they can select what shell they wish to use, but I wouldn’t recommend it.

To have the server use the new ACLs, be sure to restart it (service ldap restart on most Linux systems).

Migrating Data

The next step is to begin migrating your data into your LDAP server. This is where things start to get interesting, and also where the openldap-migration package is necessary. Change to the /usr/share/openldap/migration directory and edit the migrate_common.ph file. You will need to modify the following variables to match your system:

$DEFAULT_MAIL_DOMAIN = "mylan.net";
$DEFAULT_BASE = "dc=mylan,dc=net";
$DEFAULT_MAIL_HOST = "mail.mylan.net";
$EXTENDED_SCHEMA = 1;

This sets some defaults for the migrated data. Here we set the default mail domain, in this case “mylan.net” which will assign all users a default email address of “user@mylan.net”. The default base is “dc=mylan,dc=net” which should be identical to the suffix defined in slapd.conf. The default mail host is the SMTP server used to send mail, in this case “mail.mylan.net”. The extended schema is set to 1 to support more general object classes.

Now you have two choices. You can migrate everything on the current system into the LDAP database, including hosts, groups, users, networks, services, etc. A lot of this is relatively unchanging and, in my opinion, should not be included in LDAP. For instance, importing /etc/services seems useless because all systems have it, and the data is very static. Doing lookups from the LDAP server would only slow things down. The only things that I, personally, see as being useful are users, groups, and hosts. Everything else should use the system files, unless, of course, you have modified your /etc/networks file and such, but most people probably have not.

If you want to migrate everything, you will want to use the migrate_all_online.sh script. I suggest that you comment out the migration of protocols and services due to some problems with migration (you will likely have missing entries due to some errors in the source files). You can do this by editing migrate_all_online.sh and commenting out the following lines:

#echo "Migrating protocols..."
#$PERL -I${INSTDIR} ${INSTDIR}migrate_protocols.pl       $ETC_PROTOCOLS >> $DB
#echo "Migrating services..."
#$PERL -I${INSTDIR} ${INSTDIR}migrate_services.pl       $ETC_SERVICES >> $DB

The next step is to execute the script. You will be asked a few questions, but in most cases the defaults should work fine:

[root@ldap]# ./migrate_all_online.sh
Enter the X.500 naming context you wish to import into: [dc=mylan,dc=net]
Enter the name of your LDAP server [ldap]: localhost
Enter the manager DN: [cn=manager,dc=mylan,dc=net]: cn=root,dc=mylan,dc=net
Enter the credentials to bind with: secret
Do you wish to generate a DUAConfigProfile [yes|no]? no

The X.500 naming context is the base domain to use (the default should be fine since it should read this from the suffix in slapd.conf). The LDAP server name should be localhost unless you are configuring a remote LDAP server (which probably will not be the case). The manager DN should be identical to the rootdn in slapd.conf, which is almost the same except we use cn=root instead of cn=manager which is the default. The credentials to bind with is the password you generated and included in slapd.conf as the rootpw. The DUAConfigProfile should be set to no. This will import everything into your LDAP database and may take a few minutes.

However, I personally favour doing things a little more manually to get some finer controls over what is being imported. For instance, there is no need to import root or the other system accounts into the server. While in most cases the information for these accounts should be read from the local system first, there is no need to make the database bigger than required. Since the system apache or rpm user will never login directly, having them included in the LDAP database is unnecessary. I’ve also found that migrating everything, especially user information, will require you to do a little cleanup work afterwards anyways due to some incorrect values being imported. I would suggest you decide what you want imported and obtain the values manually. For this example, we will import /etc/hosts, the user information we only want imported, and likewise with the group information. It is a little more work, but it implies accuracy and a database that only contains necessary information.

What we want to do is execute some migration scripts one by one and create ldif files that we will then use to import into the LDAP database. The first thing you must do is create the base structure for the LDAP database running the migrate_base.pl script:

[root@ldap]# ./migrate_base.pl >base.ldif
[root@ldap]# ldapadd -x -D "cn=root,dc=mylan,dc=net" -W -f base.ldif

This generated the base structure and imports it into the LDAP database. Now we can begin to add actual information to the database. Let’s start with /etc/hosts:

[root@ldap]# ./migrate_hosts.pl /etc/hosts hosts.ldif
[root@ldap]# ldapadd -x -D "cn=root,dc=mylan,dc=net" -W -f hosts.ldif

Let’s assume that one of the entries in your /etc/hosts file was:

10.0.10.23      wrkstation.mylan.net    wrkstation

You can test to make sure the migration worked by executing:

[root@ldap]# ldapsearch -LL -H ldap://localhost -b"dc=mylan,dc=net" -x "(cn=wrkstation)"
version: 1

dn: cn=wrkstation.mylan.net,ou=Hosts,dc=mylan,dc=net
objectClass: top
objectClass: ipHost
objectClass: device
ipHostNumber: 10.0.10.23
cn: wrkstation.mylan.net
cn: wrkstation

Be very careful that you do not have duplicate entires in your /etc/hosts file. While this would be alright for normal operations, when importing the data into LDAP, the import will halt if a duplicate is encountered. In other words, make sure localhost isn’t listed twice, and so on.

Next, migrate the group information. Execute:

[root@ldap]# ./migrate_group.pl /etc/group group.ldif

This will put all groups into the _group.ldif_ file. Since this is redundant (do we really need apache group, rpm group, etc.?), you will want to edit the group.ldif file and remove the unwanted groups. I would only keep user groups, not system groups. I also would not put root into the LDAP database for this. The reason is simple: root is a system account and should be treated as such. It should also be specific to the machine. Having the same root password across machines is not a good idea. If you have one system where a user needs root access and you elect to use su and provide them with the root password, you can restrict them to the one machine by not including root in the LDAP database. And since each system will always have a root account, because we lookup locally first, and then the LDAP database, root should never be referenced by LDAP anyways. Some people may prefer to have the same root password across multiple machines (for example, a very large school lab). While I don’t agree with this personally, others may find it difficult to remember a thousand different root passwords (a good password management scheme can deal with this, however). In this case, you may prefer to keep root in the LDAP directory and authenticate root against the LDAP directory instead of locally. This can be done by putting root in both the LDAP directory and locally, and changing how pam_ldap authenticates (we’ll see about this in a moment). If this is your particular preference, keep root in the group.ldif file.

If you’re setting up a system without any pre-existing users, you may need to create this file from scratch. The file will look like this:

dn: cn=vdanen,ou=Group,dc=mylan,dc=net
objectClass: posixGroup
objectClass: top
cn: vdanen
gidNumber: 1001

dn: cn=joe,ou=Group,dc=mylan,dc=net
objectClass: posixGroup
objectClass: top
cn: joe
gidNumber: 1002

dn: cn=users,ou=Group,dc=mylan,dc=net
objectClass: posixGroup
objectClass: top
cn: users
gidNumber: 1000
memberUid: vdanen
memberUid: joe

You can see what objects are to be referenced here with the objectClass keywords. The cn is the group name, the gidNumber is the group ID number. Finally, the dn is a string that consists of the group name, the ou (which is Group), and the domain information (dc=mylan,dc=net). The last example shows a group with multiple users; in this instance the group is “users” and we assign it a gid of 1000, and add both vdanen and joe to the group by using the memberUid attribute. To import this information into LDAP, use:

[root@ldap]# ldapadd -x -D "cn=root,dc=mylan,dc=net" -W -f group.ldif

Now we come to the users. This can be somewhat time-consuming if you are setting up a lot of users, even if you use the migration script. First, let’s run the migration script like this:

[root@ldap]# ETC_SHADOW=/etc/shadow ./migrate_passwd.pl /etc/passwd \
passwd.ldif

You must set the $ETC_SHADOW environment variable before executing the migrate_passwd.pl script. This is necessary to tell migrate_passwd.pl where /etc/shadow is located. Without this environment variable, none of the shadow information will be included (which means no passwords). As with the groups, you may have to write this file from scratch. If you do, you can simplify creating the file by cutting and pasting the encrypted password string in /etc/shadow and placing it as the value for the user’s userPassword attribute (which is all that migrate_passwd.pl does; there is no kind of conversion of any sort). Taking the above two groups of vdanen and joe, let’s make two entries for them which would look like this in your passwd.ldif file:

dn: uid=vdanen,ou=People,dc=mylan,dc=net
uid: vdanen
cn: Vincent Danen
givenname: Vincent
sn: Danen
mail: vdanen@mylan.net
mailRoutingAddress: vdanen@mail.mylan.net
mailHost: mail.mylan.net
objectClass: mailRecipient
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: kerberosSecurityObject
objectClass: shadowAccount
userPassword: {crypt}$1$dYJI1DcK$r7Uod6DPFgh4XWL7l9GTF/
shadowLastChange: 11761
shadowMin: -1
shadowMax: 99999
shadowWarning: -1
shadowInactive: -1
shadowExpire: -1
shadowFlag: 7100670
krbname: vdanen@MYLAN.NET
loginShell: /bin/bash
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/vdanen
gecos: Vincent Danen

dn: uid=joe,ou=People,dc=mylan,dc=net
uid: joe
cn: Joe User
givenname: Joe
sn: User
mail: joe@mylan.net
mailRoutingAddress: joe@mail.mylan.net
mailHost: mail.mylan.net
objectClass: mailRecipient
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: kerberosSecurityObject
objectClass: shadowAccount
userPassword: {crypt}$1$Ev9HK2ML$IhA3EpyS28SfJ.m74AMvW/
shadowLastChange: 11762
shadowMin: -1
shadowMax: 99999
shadowWarning: -1
shadowInactive: -1
shadowExpire: -1
shadowFlag: 7100670
krbname: joe@MYLAN.NET
loginShell: /bin/bash
uidNumber: 1002
gidNumber: 1002
homeDirectory: /home/joe
gecos: Joe User

Now you will be able to import your passwd.ldif file into LDAP by executing:

[root@ldap]# ldapadd -x -D "cn=root,dc=mylan,dc=net" -W -f passwd.ldif

Finally, let’s test to make sure that the data got imported properly. You can use ldapsearch to search for an account you know exists. In our case, we know that user joe exists, so let’s search for his data:

[root@ldap]# ldapsearch -LL -H ldap://localhost -b"dc=mylan,dc=net" -x "(uid=joe)"

This will return a screen of input which should like identical to what was stored in your ldif file. We have now imported our hosts, group, and user information into LDAP.

Configuring the OpenLDAP Clients

Now you must configure OpenLDAP on each of the client systems. What we have configured previously is just the OpenLDAP server, or the authentication server. Now you must configure the clients. This also includes the server as it will likely be a client unto itself (ie. it will access the LDAP server via localhost to obtain authentication information). To do this, you must edit the /etc/ldap.conf file. The entries we are most interested in are the following:

host 127.0.0.1
base dc=mylan,dc=net
rootbinddn cn=root,dc=mylan,dc=net
scope one
pam_filter objectclass=posixaccount
pam_login_attribute uid
pam_member_attribute gid
pam_password md5
nss_base_passwd         ou=People,dc=mylan,dc=net?one
nss_base_shadow         ou=People,dc=mylan,dc=net?one
nss_base_group          ou=Group,dc=mylan,dc=net?one
nss_base_hosts          ou=Hosts,dc=mylan,dc=net?one

This tells the LDAP client the IP address of the host. For the server, you can use 127.0.0.1 (localhost), although you should really use the fully qualified domain name (a requirement if you’re going to use SSL or TLS). The remote clients must use the domain name (and the IP must be listed in /etc/hosts) or the IP address of the LDAP server (again, using the IP or the FQDN really depends on your SSL settings; more on this in a moment). The base must be the same as the suffix denoted in /etc/openldap/slapd.conf. The pam_password must be set to md5 (it uses the crypt(3) system call, which will use the unix MD5 hash instead of the built-in MD5 which is incompatible). If you use “pam_password crypt” (which was what was previously recommended), when you change passwords using the passwd program, passwords will be stored in crypt format, not MD5 format. The nss_base_* entries must be defined properly; they will be commented in the file so you must uncomment those you want to enable (in our case, just passwd, shadow, group, and hosts), and you must also make sure the base DN is correct. The ending “?one” is the search scope and should remain as illustrated.

The rootbinddn keyword sets who to bind to the server as when the effective user ID is root (or 0). The password must be stored in /etc/ldap.secret, which should be mode 0600 (read/write root, no access for group or other), and should be owned by root.root. This is a clear-text version of the password you specified with the “rootpw” keyword in slapd.conf, and the dn should be that of the root user (ie. cn=root,dc=mylan,dc=net). Please note that you *must* press enter after typing your password into the file. If the file does not end on the second line, connections to the LDAP server will fail. If you use echo to write the file (ie. using ” echo secret >ldap.secret “), you do not have to worry about this.

In the previous revision, you’ll note that this was a special proxy user we had created just for this situation (to read, and possibly write, the userPassword. This is no longer necessary since pam_ldap actually authenticates properly, and uses an anonymous auth bind, which our ACLs permit.

However, you must decide whether or not each client should have a copy of the root password stored in ldap.secret. I would advise against this, and only specify the rootbinddn on the LDAP server itself. With pam_ldap working, normal users can change their own password on any machine. The only time the rootbinddn is really used is if the root user needs to change another user’s password. This should, more often than not, be a seldom situation. By keeping the rootbinddn specified only on the LDAP server, a user with root privileges on that server will be able to change user passwords. root users on other systems, however, will not be able to do so (and depending upon your situation, this may or may not be a good thing). You may want to provide the rootbinddn on a workstation that your LAN administrator users; that way they can change the password directly on their own workstation without logging in on the LDAP server itself. How you implement this is entirely up to you, however I don’t believe it is necessary (and would be a security risk), to specify the rootbinddn and root password on every workstation using the LDAP server for authentication. I think it foolhardy and unnecessary. As long as their is one system that the LAN administrator(s) can reach that has the rootbinddn specified, they will be able to change user passwords as required. Remember, users can change their own passwords without needing a proxy user to accomplish it, so this is only required for administrators who may need to change a user’s password (in the event is compromised, lost, etc.).

Finally, you should have the domain name and IP address of the OpenLDAP server defined in /etc/hosts.

Configuring NSS to use LDAP

The next step is to configure NSS to use LDAP. NSS stands for Name Service Switch and is a means to tell the system what sources you want referenced for certain information. The configuration file is /etc/nsswitch.conf and probably looks something like this:

passwd:     files nisplus nis
shadow:     files nisplus nis
group:      files nisplus nis
hosts:      files nisplus nis dns

This is somewhat abridged, but the point is clear. Here you can see that we are telling the system to use local files, then nisplus, then nis when attempting to reference passwd, shadow, or group information. We are using the same order but append dns to the search order for hosts. This is what tells most applications to use your /etc/hosts file prior to doing a DNS lookup. And, if you were using NIS, the system would already be configured to do local lookups then NIS-based lookups for user information. However, if you look in the file, there is no mention of LDAP at all. That is why we needed the nss_ldap package installed. Now, since we want to do user and host lookups via LDAP, we would modify the above entries to look like this:

passwd:     files ldap
shadow:     files ldap
group:      files ldap
hosts:      files ldap dns

This tells the system to use local files first, then do LDAP lookups, and, in the case of hosts, use DNS last. Now that you’ve made this switch, you can trim your /etc/hosts file, however it should contain the localhost definition and the IP/hostname of your LDAP server. Everything else can be removed and will be looked up via LDAP. Finally, you can test to make sure that this is in fact being done by using the getent tool like this:

[root@ldap]# getent hosts
[root@ldap]# getent group
[root@ldap]# getent passwd
[root@ldap]# getent shadow

getent will return the local information first (because we’ve defined the sort order as being files then ldap), and then the LDAP information. You should see in each instance the LDAP entries at the end. If you have the same user defined locally and in LDAP, you will see two entries for the dual-defined user. If this works as expected then congratulations! You can now setup each client the same way to reference the LDAP server for host and user information.

There is one caveat to this. If you use OpenLDAP to serve host information in place of /etc/hosts on the client systems, you must include the host information on the server in the /etc/hosts file directly. For some strange reason, even if you have OpenLDAP, on the server, defined to use “files ldap dns” for host information, the LDAP server is not referenced for doing reverse-IP lookups on the server. This means that if, for example, the IP address 10.0.10.25 connects to the LDAP server, the LDAP server will spend time trying to determine the hostname of that IP address, but will be unable to obtain it even if it is defined in the LDAP database itself. This will cause delays from 30 seconds to 1 minute. The solution is to have a “fully stocked” /etc/hosts file on the LDAP server that contains the identical information from the LDAP database. To obtain this information, you can use:

[root@ldap]# echo "127.0.0.1 localhost.localdomain localhost" >/etc/hosts
[root@ldap]# echo "10.0.10.20 ldap.mylan.net ldap" >>/etc/hosts
[root@ldap]# getent hosts >>/etc/hosts

Of course, change the IP address and domain name to suit your LDAP server. This will prevent any delays when clients are talking to the server, which is crucial for login information (who wants to sit at a login prompt for an extra 30 seconds?). You may want to do this each time you modify the Hosts database on the LDAP server in order to keep it updated.

Because of this limitation, you may prefer to setup an internal DNS server to handle the LAN as opposed to using LDAP for this. If you use something like BIND or djbdns internally, you can remove all references of host information from the LDAP directory, and remove the call to lookup in LDAP host information from nsswitch.conf.

Configuring PAM to use LDAP

Making PAM use LDAP is very easy. Mandrakelinux, Red Hat Linux, and some other Linux distributions make use of the system-auth facility of PAM, so you will need to modify /etc/pam.d/system-auth to make it LDAP-aware. The following is what your system-auth file should look like to make it LDAP-aware:

#%PAM-1.0
auth        required      /lib/security/pam_env.so
auth        sufficient    /lib/security/pam_unix.so likeauth nullok
auth        sufficient    /lib/security/pam_ldap.so use_first_pass
auth        required      /lib/security/pam_deny.so

account     required      /lib/security/pam_unix.so
account     sufficient    /lib/security/pam_ldap.so

password    required      /lib/security/pam_cracklib.so retry=3 minlen=4 \
dcredit=0 ucredit=0
password    sufficient    /lib/security/pam_unix.so nullok use_authtok \
md5 shadow
password    sufficient    /lib/security/pam_ldap.so use_authtok
password    required      /lib/security/pam_deny.so

session     required      /lib/security/pam_limits.so
session     required      /lib/security/pam_unix.so
session     optional      /lib/security/pam_ldap.so

Please note the two lines that wrap, using the standard “\” character to indicate continuation on a second line. These lines must be a single line in your system-auth file, and should not use the “\” character to wrap the line.

This inserts the pam_ldap.so module into the stack so that it will be referenced if a user is not found locally. This will not affect system users from being able to login and will allow you to have logins from users that are not explicitly defined in /etc/passwd.

If you want to be able to have the system create home directories on the fly (ie. you enter a user into the LDAP database but have not created a home directory for them on the system), you can use the pam_mkhomedir module. Replace the above “session” entries in system-auth with (and note the line wrap):

session     required      /lib/security/pam_mkhomedir.so skel=/etc/skel/ \
umask=0022
session     required      /lib/security/pam_limits.so
session     required      /lib/security/pam_unix.so
session     optional      /lib/security/pam_ldap.so

This will create a home directory for the user, on the fly, using /etc/skel as the skeleton directory (which is the standard when creating new system users on most Linux systems).

One final note. There seems to be issues with the pam_unix.so and pam_pwdb.so modules. On a Mandrakelinux 8.2 or 9.0 system, the above works just fine. However, on a 9.1-based system, you will need to use pam_pwdb.so in place of pam_unix.so in the “auth” section of system-auth. If you use pam_unix.so, using the su tool will segfault. By using pam_pwdb.so, everything should work fine. If you find something doesn’t seem to work as expected (ie. everything but su works, or everything but ssh works, etc.), try fiddling with your system-auth file and interchange pam_unix.so with pam_pwdb.so, change the order that modules are called, etc. There doesn’t seem to be any sure-fire way to indicate what definitive order to use, as PAM modules differ from Mandrakelinux version to Mandrakelinux version (and more than likely vary more from distro to disto). Also note this has, so far, only been tested with Mandrakelinux. As we broaden the scope, we will include notes for other Linux and BSD distributions.

At this point, any program that uses system-auth for authentication will also be using LDAP. This includes services such as SSH, possibly FTP, and others that authenticate against the system using PAM.

Finally, you will also need to modify your /etc/pam.d/passwd file as well in order to use the passwd program to modify passwords in the LDAP database. The file should look like this:

#%PAM-1.0
auth       sufficient   /lib/security/pam_ldap.so
auth       required     /lib/security/pam_pwdb.so shadow nullok

account    sufficient   /lib/security/pam_ldap.so
account    required     /lib/security/pam_pwdb.so

password   required     /lib/security/pam_cracklib.so retry=3 minlen=4  \
dcredit=0  ucredit=0
password   sufficient   /lib/security/pam_ldap.so use_authtok
password   required     /lib/security/pam_pwdb.so use_authtok nullok \
md5 shadow

Please note the two lines that wrap, using the standard “\” character to indicate continuation on a second line. These lines must be a single line in your passwd file, and should not use the “\” character to wrap the line. In Mandrakelinux 9.0+, the passwd file uses system-auth, so you don’t need to change anything.

Changing passwords used to be problematic, and this is due to how OpenLDAP is built by default. OpenLDAP compiles with it’s own MD5 before using the system (crypt(3)) MD5, which makes OpenLDAP look for passwords in a different MD5 format than the crypt(3) MD5 format. Reversing this order fixes the problem and makes OpenLDAP use crypt(3) MD5 first, which means that we can now use pam_ldap to change passwords (the user’s login password will be identical to the LDAP password). This has been patched in the Mandrakelinux OpenLDAP updates in MDKA-2003:009; other distributions may or may not have this patch applied. If you do not, you can download the [openldap-2.0.27-slapd-Makefile.patch openldap-2.0.27-slapd-Makefile. patch] and patch your own OpenLDAP installation (a rebuild would be required).

You can also easily change passwords with Directory Administrator (see the Clients section near the end of this topic).

Host-based Authentication

If you plan to use OpenLDAP to authenticate users in a LAN, you may find situations where users should not be permitted on certain machines. As it stands, if the user supplies a proper password, they will be able to log into any machine that uses OpenLDAP for authentication. Using the pam_mkhomedir module, they will even have a home directory created for them. This may not be what you want. Assume for a moment that you have user Joe and user Jim. Joe has his own system, let’s say workA, and Jim has his own, workB. Joe is a competent user and doesn’t want Jim to have access to his machine, but Jim likes to have Joe on his computer for support. In this case, Joe must have access to workA and workB, whereas Jim should only have access to workB.

This is very easy to accomplish. In your /etc/ldap.conf file, you must include another two keyword attributes:

# check for login rights on the host
pam_check_host_attr yes
pam_filter |(host=this.host.com)(host=\*)

This will tell the pam_ldap module to search the “host” attribute in a user’s record to determine if he has access to the system. There is no way to determine what machine the user is originating from, but you can determine if they should have access to the machine they are attempting to log into. The easiest way to do this is to create another ldif file that looks something like this (following the scenario above):

dn: uid=joe,ou=People,dc=mylan,dc=net
changetype: modify
add: host
host: workA
host: workB

dn: uid=jim,ou=People,dc=mylan,dc=net
changetype: modify
add: host
host: workB

Now execute the ldapmodify command like this:

[root@ldap]# ldapmodify -H ldap://localhost -D "cn=root,dc=mylan,dc=net" \
-x -W -f host-auth.ldif

Assuming that the file containing the above statements is called host-auth.ldif. This will modify the records for both Joe and Jim, allowing Joe access to workA and workB, and Jim access only to workB. You must use the FQDN for each host as the host will call gethostbyname() for it’s own name (you can see the same result using the “hostname” command). If the hostname returned matches one of the host attributes, the user will be allowed to login. If not, PAM will reject the login attempt.

One thing to note: If you do not have the pam_check_host_attr keyword in your ldap.conf, that host will not check the host attribute and will allow any LDAP-authenticated users to login. So if you control your own machine (as root) and are part of a LAN that uses LDAP for authentication, be sure you modify your ldap.conf and enable this. In a LAN situation with, say, 500 users, without this line all 500 users will be able to login to your system. Of course, if your user entry does not contain a host attribute with your hostname, you won’t be able to login either.

You also need to use the pam_filter method noted above to grant access to the host. Here you specify the hostname that must be matched; in the above case it is “this.host.com” and “\*”. If you wish to give a user access to all hosts that are being served by the LDAP server, you can add the entry “hosts: *” to their account, rather than numerous hosts entries. If you do not use the pam_filter you will have to modify your PAM file (we’ve been discussing system-auth in this article, but whaterver PAM file is relevant if you do not use system-auth). The relevant change is to make “account sufficient pam_ldap.so” into “account required pam_ldap.so“, however if you used a mixed system/LDAP environment (ie. if you have root in /etc/passwd and not in LDAP), system users will be unable to login; only LDAP-based users will be able to login. Using pam_filter is a good solution to allow you to keep a mixed system/LDAP environment.

This should also illustrate the importance of minimizing what attributes users can change in their LDAP entries. For instance, our slapd.access.conf indicates that the user can only modify their userPassword and mail attributes. If you had included something like this instead:

access to *
        by self write
        by * read

You would be allowing the user to add their own host entries. This means they can arbitrarily give themself access to any other host, provided they know the FQDN for that host. Even if pam_mkhomedir is not used, they can still obtain a bash prompt on that host, without administrator approval or knowledge.

Configuring Samba to use LDAP

You can also make Samba use LDAP for authentication. In order to do this, you must rebuild Samba so that it stores smbpasswds in LDAP. Unfortunately, Samba can use either LDAP or the smbpasswd file, and not both. By default, it uses the smbpasswd file. To enable the LDAP support, you must rebuild the src.rpm (for Mandrakelinux) using:

[root@ldap]# rpm --rebuild --with ldap samba-2.2.3a-10mdk.src.rpm

The Mandrakelinux Samba packages have support for LDAP, provided you build it this way. You will find the Samba source RPM package on your Sources CD, or on any FTP mirror.

Once you have built Samba with LDAP support, you must modify your /etc/samba/smb.conf file to use LDAP. The important things to include in the [global] section are:

ldap admin dn = cn=root,dc=mylan,dc=net
ldap server = 127.0.0.1
ldap suffix = dc=mylan,dc=net
ldap port = 389
ldap ssl = start tls

By default, TLS/SSL support is enabled anyways so it isn’t necessary to specify the ldap ssl key. If you do not plan on using TLS/SSL, you can disable it by setting it to “off”. TLS/SSL is discussed a little later on. For the purpose of Samba, you should know that by default it will try to talk TLS over port 636, which is the standard LDAPS port (LDAP+SSL). However, if you enable TLS, it uses port 389, which is the standard LDAP port. If you wish to use SSL on port 636, you must have “ssl on” instead of “ssl start_tls” in your slapd.conf file. For the purposes of this tutorial, TLS is started, so you must tell Samba to use port 389 otherwise you will consistently get errors trying to bind to the LDAP server.

You should also have the latest samba.schema included in your slapd.conf file. It will default to the /usr/share/openldap/schema/samba.schema which you do not want to use. You will want add to your slapd.conf file instead:

include /usr/share/doc/samba-doc-2.2.3a/examples/LDAP/samba.schema

In other words, use the schema file that comes with Samba.

The next step is to give Samba a password for the admin dn you defined previously (cn=root,dc=mylan,dc=net). This can be done using smbpasswd:

[root@ldap]# smbpasswd -w [password]

Unfortunately, you have to pass the password on the command line, so be sure to clear your history once you have done so. The admin dn password is stored in the /etc/samba/secrets.tdb file. Now you will have to add Samba accounts. The existing users do not impact the Samba password settings at all, so you will need to add each user manually using:

[root@ldap]# smbpasswd -a [username]

There are some migration scripts in the /usr/share/docs/samba-doc-2.2.3a/examples/LDAP directory but they are not up-to-date and may not work properly. The only attributes that need to be added to existing users are the rid, lmpasswd, ntpasswd, and sambaAccount attributes, which is what smbpasswd should do.

On a side note, if you plan to have Samba authenticate against the LDAP database, you should protect the lmPassword and ntPassword attributes the same as you would the userPassword attribute in slapd.access.conf.

Using SSL/TLS with OpenLDAP

Every example previously shown with ldapadd, ldapmodify, ldapsearch, and so on assumed that the LDAP server was running without any means of encrypting traffic (or basic authentication). On a local system, this would be ok, but using OpenLDAP for authentication obviously shines when many machines are involved. Passing password data in the clear definitely is not a good idea, so OpenLDAP provides the means to access the server using SASL or TLS. Since TLS is the easier to setup, we’ll look at using it instead of SASL.

TLS uses SSL, so it also uses certificate files and keys. TLS provides proof of server identity and protection of data in transit. Because of this, it is almost necessary when plaintext passwords or other data may be transmitted over a network.

To begin using TLS, you must create a certificate and a key file. The ldap initscript in Mandrakelinux will create a self-signed certificate for you to use; the file is /etc/openldap/ldap.pem. To enable TLS support for both the server and the client, you must first modify slapd.conf like this:

TLSCertificateFile /etc/openldap/ldap.pem
TLSCertificateKeyFile /etc/openldap/ldap.pem
TLSCACertificateFile /etc/openldap/ldap.pem

Then, in your /etc/ldap.conf file, comment out the default of turning SSL off, and turn TLS on like this:

ssl start_tls
#ssl off

Once you restart the server, TLS will be used on the standard LDAP port of 389. The LDAP server will handle TLS and unencrypted traffic on the same port.

If you don’t want to use the pre-generated certificate, but generate your own (using a Certificate Authority-signed cert instead of a self-signed cert), the following will illustrate how to accomplish it. Execute on the LDAP server:

[root@ldap]# openssl genrsa -out ldap.key 1024
[root@ldap]# openssl req -new -key ldap.key -out ldap.csr

You will have to fill in all the appropriate information for the CSR (or Certificate Signing Request). For the Common Name field, you should use the exact name that clients will use when contacting the server, usually the FQDN. You can either have a registered Certificate Authority like Thawte sign your CSR or you can create your own CA to sign it. If you already have your own CA, you would sign the CSR using:

[root@ldap]# openssl x509 -req -in ldap.csr -out ldap.cert -CA ca.cert \
-CAkey ca.key -CAcreateserial -days 365

The resulting file, ldap.cert is the certificate for the LDAP server. If you do not have your own CA setup, you can easily do so using:

[root@ldap]# openssl genrsa -des3 -out ca.key 2048
[root@ldap]# openssl req -new -x509 -days 365 -key ca.key \
-out ca.cert

If you wish to examine the contents of the LDAP certificate, you can use:

[root@ldap]# openssl x509 -in ldap.cert -text -noout

The LDAP server needs access to a copy of it’s own certificate and key files, as well as the CA certificate. The key file must only be readable by the server process, so make it mode 0400 and owned by user ldap, group ldap. To make the server aware of these files, edit your slapd.conf file and include:

TLSCertificateFile /etc/ssl/openldap/ldap.cert
TLSCertificateKeyFile /etc/ssl/openldap/ldap.key
TLSCACertificateFile /etc/ssl/openldap/ca.cert

Now you must restart the LDAP server by issuing a service ldap restart command.

This will now provide transport-level encryption for your LDAP traffic, which will keep the data secure across the network.

Using webmin with OpenLDAP

For those of you who use webmin, you can use it to ease some of the user management pains of using ldapmodify on the command line. webmin comes with three LDAP modules in the “Others” section: LDAP Browser, LDAP Manager, and LDAP users and group administration.

In order for these modules to work properly, you must install the perl-ldap package (which comes with Mandrakelinux), and the perl-Convert-ASN1 package (which does not, but it is available in contribs). To install perl-ldap from your installation CDs, just use:

[root@ldap]# urpmi perl-ldap

If you purchased a Power Pack or ProSuite, or can find a contribs mirror for your particular distribution, you can install the perl-Convert-ASN1 module from your installation CDs or from an external mirror site.

Once both modules are installed, you can click on the first module, LDAP Browser. The first time you do this, it will report that it cannot connect to the LDAP server and give you an error. Click on the “Module Config” link to configure the module. The settings you select will look something like this:

LDAP server host name or IP                     127.0.0.1
LDAP server port number                         389
Root DN for your directory tree                 dc=mylan,dc=net
LDAP administrative user credentials            cn=root,dc=mylan,dc=net
LDAP administrative password (clear text)       secret

When you save these changes, you will see a listing of all the Distinguished Name entries in the database and will be able to manipulate these entries.

The next module is the LDAP users and groups administration module. Again, you will get an error when you first click on the module, so enter “Module Config” once more. The settings you will select will look something like this:

LDAP server host name or IP                     127.0.0.1
LDAP server port number                         389
Root DN for your directory tree                 dc=mylan,dc=net
LDAP administrative user credentials            cn=root,dc=mylan,dc=net
LDAP administrative password (clear text)       secret
LDAP users directory                            ou=People,dc=mylan,dc=net
LDAP groups directory                           ou=Group,dc=mylan,dc=net

Once you have saved the configuration, the index page of the module will now list the LDAP users and groups on the system. To make it easy to create new users, you can define a local template. This will allow you to select which attributes new users should have, and allow you to assign some default values to these attributes. You will have to save the template to a file on the webmin server’s filesystem; ie. you could save it as /etc/openldap/ldap-webmin-user.template or something.

Now if you select to create a new user, you will be asked for the new user’s full name, and the attributes you wish to add to the users profile, or the template file to use. Unfortunately, the module always seems to give errors, with or without a template, so it’s not quite ready for use in adding new users. However, it does make a great browser for users and groups, and will allow you to modify a user’s attributes easily enough.

The final module, LDAP Manager, does not work without additional perl modules installed, like the perldap module which currently is not packaged due to a requirement on the Mozilla LDAP SDK. It would be nice if the module was re-written to work with perl-ldap instead.

Other OpenLDAP Clients

Of course, you can also use GUI tools to handle your LDAP data. One tool that comes with Mandrakelinux is called Directory Administrator. To install it from your installation CDs, execute:

[root@ldap]# urpmi directory_administrator

The latest version as of this writing is 1.3.5 and has some good bug fixes. You can rebuild the latest version from cooker or download it from the Directory Administrator website. It has been included in Mandrakelinux since version 8.2.

Another good tool is gq, which is available in contribs. This will allow you to view your entire LDAP database in a tree view and modify all aspects of it. It’s a very comprehensive and powerful GTK-based LDAP tool.

You can also use the MandrakeSoft tool userdrake to modify LDAP users.

Notes on Other Software

Because different software packages manage authentication slightly different, even if relying on PAM, there is a place to note “exceptions to the rule”.

OpenSSH

OpenSSH portable has had a lot of problems with PAM-related issues since privilege separation was implemented. This doesn’t make OpenSSH bad, just a little bit of a challenge to work with. With current versions of OpenSSH, if you are using LDAP for authentication and, at least, the pam_pwdb module, attempting to ssh into an LDAP-controlled box to a user that is in the LDAP directory won’t work. pam_pwdb reports the user as unknown and returns that to OpenSSH unless you disable s/key support by setting the following in your sshd_config:

ChallengeResponseAuthentication no

This may work differently (better?) if you build OpenSSH with s/key support, but I don’t know for sure. However, disabling s/key support in the configuration file will allow you to ssh into a box using the account of an LDAP-controlled user.

Credits and Links

I would be a very bad person if I didn’t give some thanks here. Because this was my first foray into the world of LDAP, a lot of questions were asked and I’d like to thank the folks on the discuss@mandrakesecure.net mailing list, as well as Todd Lyons and Buchan Milne for their help. As well, to all of those who posted great comments on the first revision of the article; those comments are very helpful. Hopefully this revision will incorporate enough of the Mandrake-specific comments and provide a good help to those who are contemplating setting up an LDAP-based authentication system. Finally, I would like to thank Jose Permuy for pointing me to the patch to fix the MD5 issue with OpenLDAP, which was the whole source of the problem with pam_ldap we previously had.

Here are some other OpenLDAP-related links that may help to further your understanding of OpenLDAP, both for authentication purposes and just general usage. Some of them are slightly dated, but a lot of the information is still valid; you may just have to pick and choose what information you apply to anything you wish to accomplish.