Imagine this: your boss has just told you that you have to roll out a network over the next two weeks which is to span 6 states, to do email (smtp and imap and pop), samba (acting both as a PDC and as a file server), ftp, radius (driving portslave) and webmail. Probably so far you'd be saying "that's not so hard.", you'd be right. Here's the killer, all of these services are to take their user list and authentication information out of a single database which is to be replicated across all 6 states. Furthermore, the 12 computers involved in this 6 state network are all to be able to communicate securely over the internet and compress the data as it travels to accomodate a 150 person company communicating on 64k ISDN lines.
Not so long ago that was the situation I was faced with. This is a document explaining how we set it up.
This is the software we used in setting all this up:
Right, so now that you've downloaded the source code to all that software, you're probably wondering what to do with it all. So I guess it's here that I should explain what we're planning to do.
LDAP is effectively a database. It's unlike a lot of databases you can get these days (mysql for example) in that it operates on trees, not rows. This is probably a bit of a simplification (it can end up with cyclic graph structures, but that should be avoided). The way this relates to us is that we want to model a company inside a database. Companies tend to have structures in them taht are quite effectively modelled as trees. But enough abstraction, we intend to make a database for the company, have a section in that database called "People" and have a record in this "People" section for each employee in the company. The way you identify a single record in ldap is with a dn, or distinguished name. The dn for an employee with a username "trevor" in a company called "h4x0rs r u5", in Australia would be
uid=trevor,ou=People,o=h4x0rs r u5,c=AU
That is user id is trevor, organisational unit is People, organisation is h4x0rs r u5, country is Australia. I won't say too much more about the way ldap works, except where it is relevant to understanding this document. In this leaf of the tree we'd also have other "fields" like userPassword, cn, sn and the like. These fields are called attributeTypes. When you're trying to figure out where to put a piece of data, I highly recommend a piece of software called GQ, which is a nice pointy-clicky program that lets you look at your database as a tree, but to me the more useful feature is the ability to look at the ldap schema's that you have loaded in your database. If you're a bit shakey on how ldap works, I suggest you get this program, because it will give you a tangible representation of the way it organises data and lets you play with it to get a feel for it all.
So, back on track, we're going to use this ldap database to replace the standard unix /etc/passwd and /etc/shadow files. The advantage of this is that it then means that your user database can be located anywhere, and can be accessed using standard tcp/ip connections. Which also means that when you add a user in one state, that user is automatically visible in all states, sendmail immediately knows about them, and better yet, everyone with a mailing client on your network will automatically have an address book entry on their computer pointing to this new user.
The first thing you'll need to do is to get ldap up and running and put a user database in there to test against. You might as well use the users that you're planning on having on your network. So first up, building ldap. I'll assume that you've grabbed the source tarball and have unpacked it to some place sensible like /usr/local/src. So change to your openldap source directory and run configure, make dep, make, make test, make install. We used the following flags to configure in building it:
./configure --with-tls --without-sasl --enable-ldbm --with-threads --enable-rlookup
Observant readers will notice that we built a fair bit of other stuff to get that far (openssl, ldbm etc). We haven't listed them, because they are options, and there are plenty of places to get documentation on building ldap. We chose to not use sasl (in fact we're not even using ssl at this stage in the project because it's only a single server that has been rolled out thus far), because to set up sasl means you have to have another database of users. It is more secure, but for our purposes, connections on the loopback device were considered reasonably safe from ethernet sniffers, and we always have the option of just throwing the switch to go to ssl if we ever need to. One thing that caught us out was that after we built and were running the tests, it was taking literally hours to complete (and this was not a small system, PIII-800 IIRC). It turned out to be a bad hub, which meant that it was unable to do name lookups on our network at the office. If ldap for some reason isn't working, or if it's running really really slow, then the first place to look is your network. Eliminate anything that might be problematic there, and then if you're still stuck, the openldap-users mailing list is actually very helpful if you ask the right questions and you've done your research.
Now you've built and installed ldap. The next place to look is the configuration file. The file you are interested in lives in /usr/local/etc/openldap/slapd.conf. Here's what we've got there:
# $OpenLDAP: pkg/ldap/servers/slapd/slapd.conf,v 1.8.8.4 2000/08/26 17:06:18 kur # # See slapd.conf(5) for details on configuration options. # This file should NOT be world readable. # include /usr/local/etc/openldap/schema/core.schema include /usr/local/etc/openldap/schema/corba.schema include /usr/local/etc/openldap/schema/cosine.schema include /usr/local/etc/openldap/schema/inetorgperson.schema include /usr/local/etc/openldap/schema/java.schema include /usr/local/etc/openldap/schema/krb5-kdc.schema include /usr/local/etc/openldap/schema/misc.schema include /usr/local/etc/openldap/schema/nadf.schema include /usr/local/etc/openldap/schema/nis.schema include /usr/local/etc/openldap/schema/openldap.schema # Define global ACLs to disable default read access. # Do not enable referrals until AFTER you have a working directory # service AND an understanding of referrals. pidfile /usr/local/var/slapd.pid argsfile /usr/local/var/slapd.args ####################################################################### # ldbm database definitions ####################################################################### database ldbm suffix "o=h4x0rs r u5, c=AU" rootdn "cn=Manager, o=h4x0rs r u5, c=AU" # Cleartext passwords, especially for the rootdn, should # be avoid. See slappasswd(8) and slapd.conf(5) for details. # Use of strong authentication encouraged. rootpw secret # The database directory MUST exist prior to running slapd AND # should only be accessable by the slapd/tools. Mode 700 recommended. directory /usr/local/var/openldap-ldbm # Indices to maintain index objectClass eq index cn,sn,uid,mail pres,eq,sub defaultaccess none # Access Control Lists access to attr=cn,sn,mail,uid,mailHost,mailRoutingAddress,entry by dn="cn=Manager,o=h4x0rs r u5,c=AU" write by self write by anonymous read by * read access to attr=userPassword,entry by dn="cn=Manager,o=h4x0rs r u5,c=AU" write by self write by anonymous auth access to attr=loginShell,uidNumber,gidNumber,homeDirectory,uid,cn,gecos,sn,entr by dn="cn=Manager,o=h4x0rs r u5,c=AU" write by self read by users read access to dn="uid=.*,ou=People,o=h4x0rs r u5,c=AU" by dn="cn=Manager,o=h4x0rs r u5,c=AU" write by self write by users read
Now, a tip: Don't start off with that access control list. Start off with the server completely open. ie
defaultaccess write access to * by * write
The first thing I tried to do when setting this up was to figure out ACLs. Once you get the basic idea, they aren't too hard, but set everything else up first and then go back and play with ACLs. If you're like me, and try to get ACLs first, you'll find a whole lot of stuff just won't work.
Now, the first bit loads schemas. Schemas in ldap are descriptions of objectClasses and attributeTypes. You almost certainly won't need all of them, but it will make someone elses life easier in the future if you add all the schemas that come with the distribution. An objectClass is a group of attributeTypes. The neat thing is that you have multiple inheritance in ldap. That means that if you need attributeTypes (fields) from multiple objectClasses for a particular record, then you just specify a set of objectClasses that contain the particular attributeTypes you need, and then that record will inherit all the types from all of the objectClasses you specified. Unless you've had an education in object oriented programming, that probably won't mean a whole lot to you, but just keep it in mind for the time being. It will all become clear when we start trying to add entries to the database.
The stuff about pidfiles and the like are all pretty self explanatory, the couple of options to note are the index lines, and the stuff about rootdn. Firstly, the index lines specify what attributeTypes are to be searchable, and what sort of searches are to be allowed. The entries we put in there allow people to search by userid, first name, last name and email address. Intially we just had uid, since that was all we needed for authentication. The others were added to facilitate the address book software that used ldap. The rootdn is the base of the database. It's sort of like a root user. The suffix line is somewhat like a subnet. It defines what entries are part of this database.
The next step is to fill your database with users. In short, you do this with ldif files. Here's an example of the sort of ldif files we used:
dn: uid=trevor,ou=People,o=h4x0rs r u5,c=AU uid: trevor cn: trevor sn: user objectClass: account objectClass: posixAccount objectClass: top objectClass: shadowAccount objectClass: organizationalPerson objectClass: inetOrgPerson objectClass: inetLocalMailRecipient userid: trevor userPassword: mysecretpassword loginShell: /bin/false uidNumber: 1000 gidNumber: 511 homeDirectory: /home/trevor gecos: trevor user mailRoutingAddress: trevor@nsw.h4x0rsru5.com.au mailHost: mail.nsw.h4x0rsru5.com.au description: m4573r h4x0r homePhone: 9355-1337 mail: trevor@nsw.h4x0rsru5.com.au mobile: 0412-133-757 telephoneNumber: 133-7357
Now, as you can probably imagine, generating a separate ldif file for each and every user is just too much work to do by hand. And the good news is that there's no need to. PADL have a few migration scripts available to convert /etc/passwd and /etc/shadow files to an ldif file. In our case, these weren't much good, because the users didn't already exist. Instead, we wrote a simple little perl script that read in a comma separated file (which was converted from the excel spreadsheet we were given) and then piped an ldif file directly into ldapadd - the program which adds entries to an ldap database. Here's the script:
#!/usr/bin/perl
my $infile = $ARGV[0];
my $ou = "ou=People";
my $o = "o=h4x0rs r u5";
my $c = "c=AU";
my $state = "nsw";
my $uidnumber = 1000;
my $gidnumber = 1000;
my $homebase = "/home";
my %names = {};
my $password = "secret";
$| = 1; # kill buffering.
open IN, "$infile" or die "Cannot open input file '$infile'";
print "User list opened.\n";
while (<IN>) {
s/\"//g;
print ".";
chomp;
($_, $title, $phone, $mobile, undef, $email, undef, $homephone) = split /,/;
s/\s/\ /g;
($last, $first) = split;
$uid = lc($first . (split //, $last)[0]);
$uid =~ s/\W//g;
if ($names{$uid}++) {
$uid .= $names{$uid};
}
system ("ldapdelete -x -D 'cn=Manager,$o,$c' -w $password 'uid=$uid,$ou,$o,$c'");
open OUT, "|ldapmodify -a -r -D 'cn=Manager,$o,$c' -w $password -x";
$uidnumber++;
$userpwd = reverse $uid;
print OUT
qq{dn: uid=$uid,$ou,$o,$c
uid: $uid
cn: $first
sn: $last
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: inetLocalMailRecipient
userid: $uid
userPassword: $userpwd
loginShell: /bin/false
uidNumber: $uidnumber
gidNumber: $gidnumber
homeDirectory: $homebase/$uid
gecos: $first $last
mailRoutingAddress: $uid\@$state.h4x0rsru5.com.au
mailHost: mail.$state.h4x0rsru5.com.au
};
print OUT "description: $title\n" if $title;
print OUT "homePhone: $homephone\n" if $homephone;
print OUT "mail: $uid\@$state.h4x0rsru5.com.au\n" if $email;
print OUT "mobile: $mobile\n" if $mobile;
print OUT "telephoneNumber: $phone\n" if $phone;
print OUT "\n";
close OUT;
print ".";
system ("ldappasswd -D 'cn=Manager,$o,$c' -w $password -x -s $userpwd 'uid=$uid,$ou,$o,$c'");
print "$uid\n";
}
close IN;
Again, there's plenty of documentation on the usage of ldapadd et al, so I won't go much into their usage just now, but there are a couple of things to note. The first thing is that as I said before, ldap is a tree. As such, if you try to add an entry to a node that doesn't exist, then it just won't work. The simple solution is that before adding all your users you need to ensure that there is a node of the tree for your organisation, and also one for the group "People" that you are adding all your users to. We used an ldif file like this one:
dn: o=h4x0rs r u5,c=AU objectClass: top objectClass: organization o: h4x0rs r u5 dn: ou=People,o=h4x0rs r u5,c=AU objectClass: top objectClass: organizationalUnit ou: People
We put that into a file called "base.ldif", and added it like so:
ldapadd -W -x -D 'cn=Manager,o=h4x0rs r u5,c=AU' -f base.ldif
Notice that we first add the root node "dn: o=h4x0rs r u5, c=AU", then we add the People group "dn: ou=People,o=h4x0rs r u5,c=AU", and then we go adding the users "dn: uid=user,ou=People,o=h4x0rs r u5,c=AU". We're constructing the tree from the base upward. It must be done in this order.
Note: Use the ldappasswd program to set passwords for your users after you have created them. Plaintext passwords are bad news.
The next thing to do is to set up PAM to authenticate against the ldap database. There are essentially two steps in this. The first is to set up /etc/ldap.conf to contain information that will allow the PAM module to bind to the ldap database. Our /etc/ldap.conf file looks like this:
# # $Id: ldap.conf,v 2.11 2000/11/13 23:24:44 lukeh Exp $ # # This is the configuration file for the LDAP nameservice # switch library and the LDAP PAM module. # # PADL Software # http://www.padl.com # # If the host and base aren't here, then the DNS RR # _ldap._tcp.<defaultdomain> will be resolved. <defaultdomain> # will be mapped to a distinguished name and the target host # will be used as the server. # Your LDAP server. Must be resolvable without using LDAP. host 127.0.0.1 # The distinguished name of the search base. base ou=People,o=h4x0rs r u5,c=AU # The LDAP version to use (defaults to 2) #ldap_version 3 # The distinguished name to bind to the server with. # Optional: default is to bind anonymously. binddn cn=Manager,o=h4x0rs r u5,c=AU # The credentials to bind with. # Optional: default is no credential. bindpw secret # The port. # Optional: default is 389. #port 389 # The search scope. #scope sub #scope one #scope base # The following options are specific to nss_ldap. # The hashing algorith your libc uses. # Optional: default is des #crypt md5 #crypt sha #crypt des # The following options are specific to pam_ldap. # Filter to AND with uid=%s pam_filter objectclass=account # The user ID attribute (defaults to uid) pam_login_attribute uid # Search the root DSE for the password policy (works # with Netscape Directory Server) #pam_lookup_policy yes # Group to enforce membership of #pam_groupdn cn=PAM,ou=Group,dc=padl,dc=com # Group member attribute #pam_member_attribute uniquemember # Hash password locally; required for University of # Michigan LDAP server, and works with Netscape # Directory Server if you're using the UNIX-Crypt # hash mechanism and not using the NT Synchronization # service. #pam_crypt local # RFC2307bis naming contexts #nss_base_passwd ou=People,dc=padl,dc=com #nss_base_shadow ou=People,dc=padl,dc=com nss_base_group ou=Group,o=h4x0rs r u5,c=AU #nss_base_hosts ou=Hosts,dc=padl,dc=com #nss_base_services ou=Services,dc=padl,dc=com #nss_base_networks ou=Networks,dc=padl,dc=com #nss_base_protocols ou=Protocols,dc=padl,dc=com #nss_base_rpc ou=Rpc,dc=padl,dc=com #nss_base_ethers ou=Ethers,dc=padl,dc=com #nss_base_netmasks ou=Networks,dc=padl,dc=com #nss_base_bootparams ou=Ethers,dc=padl,dc=com #nss_base_aliases ou=Aliases,dc=padl,dc=com #nss_base_netgroup ou=Netgroup,dc=padl,dc=com
You can grab a copy of that file from the pam_ldap and nss_ldap distributions. It's basically a case of filling in your own details. If you've been following us this far it should suffice to replace your organisation and country with your own where it appears. The last thing to note is the nss_base_group. This option specifies the group in the ldap database that contains information about your unix groups. It's not of particular consequence just yet, so set it and then forget about it.
The next part is to configure the services you want to authenticate against ldap. We used ftp to test against. The pam_ldap distribution comes wiht a whole slew of configuration files which are almost ready to use. In some configurations they will just slot straight in and everything will start working straight off. However, that wasn't the case for us. I suggest that when trying to set up a service to authenticate against ldap you take the appropriate config file from the pam_ldap distribution and then remove any of the modules that aren't ldap. So, for example, from /usr/local/src/pam_ldap-82/pam.d/ftp:
#%PAM-1.0 auth required /lib/security/pam_listfile.so item=user sense=deny file=/etc/ftpusers onerr=succeed auth required /lib/security/pam_shells.so 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 session sufficient /lib/security/pam_ldap.so session required /lib/security/pam_pwdb.so
We then remove all the non-ldap lines and we're left with this:
#%PAM-1.0 auth sufficient /lib/security/pam_ldap.so account sufficient /lib/security/pam_ldap.so session sufficient /lib/security/pam_ldap.so
The reason we recommend this is that if you try mixing authentication without previously configuring your system specifically with that in mind, then you'll end up with ldap authenticating the request and some other authentication method denying it meaning that it will be denied. There are reasons you might want to mix authentication methods, so if you're interested in things like user lists and the like you might want to look at this document - they have a nice explanation of how PAM works and some of the cool things you can do with it.
At this point if everything has gone well you should be able to just ftp to your computer and be authenticated by ldap.
This is generally the point where things start going wrong, so I thought I'd throw in a couple of suggestions on how to diagnose the common problems you get. First of all you have to realise that there are effectively three stages involved. The first thing is that your server software has to ask pam for authentication, then pam has to ask ldap for authentication, and then finally, ldap has to actually authenticate. The first thing you need to check is have you built your server software to use pam? The way to do this is to check what libraries you server software links against (note that this isn't really definitive, since the software might link and then require an option in a config file to actually activate PAM support, but I assume you'll know about this). The easiest way is to run ldd over it. For example, on my system:
[root@localhost sbin]# ldd wu.ftpd
libcrypt.so.1 => /lib/libcrypt.so.1 (0x4000f000)
libnsl.so.1 => /lib/libnsl.so.1 (0x4003d000)
libresolv.so.2 => /lib/libresolv.so.2 (0x40054000)
libpam.so.0 => /lib/libpam.so.0 (0x40063000)
libdl.so.2 => /lib/libdl.so.2 (0x4006b000)
libc.so.6 => /lib/libc.so.6 (0x4006e000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
[root@localhost sbin]#
Notice the line that says libpam.so.0. That means that this executable links against the PAM libraries, and so it probably has PAM support. If you don't see a line like that, then you'll need to rebuild that service to support PAM, or you just won't get it working. It's probably also worth checking that the pam libraries are in fact accessible. The easiest way to check this is with ldconfig. Like so:
[root@localhost root]# ldconfig -v | grep pam
libpam_misc.so.0 => libpam_misc.so.0.66
libpam.so.0 => libpam.so.0.66
[root@localhost root]#
Again, you'll see libpam mentioned. So, with some luck we'll have eliminated problems with software not built correctly. The next thing to check is if PAM is using ldap. One way to do this is with stat. stat will report (amongst other things) the last time a file was accessed on a linux filesystem. It stands to reason then that if the pam_ldap module is being used that it will have it's access date modified every time you try to authenticate that service. So, open two windows. In one of them type:
watch stat /lib/security/pam_ldap.so
That will give you an update on the last access date for the file every two seconds. Now, in your other window, try to access that service (eg ftp to your server if you're testing against ftp). If ldap is being asked to authenticate, you should see the access date on the file change whilst you are doing this. If this doesn't happen then you either have the wrong config file in /etc/pam.d or you haven't configured your software for use with PAM.
The last thing that can go wrong is that ldap won't authenticate you. Unfortunately this is a little harder to debug. The place to start though, is by running slapd in debugging mode. To do this you first kill all copies of the server that are running, and then invoke it like this:
/usr/local/libexec/slapd -d 4095
Then try authenticating again. Two things: first of all if it is actually trying to authenticate against ldap, it should print pages of garbage every time you try authenticating. If that doesn't happen then go back a few steps - you've missed something. In the case that it does print out lots of garbage, then read through said garbage and try to figure out if the problem is in binding to the ldap database, or if it's actually failed your authentication. If it's the latter, a common cause is neglecting to use ldappasswd to set your user passwords, another one is ACLs. If you can't make sense of it then try the ldap mailing list.