|
Doing authentication with LDAP is hard, mainly because the information you
need is scattered through a whole set of documents, and some important
details are not covered at all. As a result, I spent a week of hard work
figuring out how to do it.
I did the work on Red Hat Linux. The results should carry over to UNIX
systems fairly well. There should be some useful information here to people
using MS Windows and other operating systems.
Some useful references: http://www.openldap.org contains an overview of LDAP
and the openLDAP implementation. The introductory sections explain the
principles of LDAP very well.
OpenLDAP is based on software from the University of Michigan.
Irritatingly, Michigan publishes its own guide, which contains some vital
extra information if you want to write your own backend software. This is
"The slad and slurpd administrator's guide"
http://www.umich.edu/~dirsvcs/ldap/doc/guides/slapd/toc.html
The book "Implementing LDAP" by Mark Wilcox (Wrox publishing) gives a good
overview. It contains some detailed information that I couldn't find in the
online docs, but not enough to do the job I wanted. Good, but expensive.
Worth reading if somebody else pays for it.
General Principles
Traditionally, UNIX authentication is done by looking up the entries in
/etc/passwd, /etc/shadow and /etc/group. Each line in /etc/passwd describes
one user using a set of colon-separated entries, for example:
boris:x:1101:100:Boris Morris:/home/boris:/bin/bash
the fields are:
user name
password ("x" means look in /etc/shadow)
user id (files belonging to this user are stamped with this)
group id (ditto)
personal information (typically, the user's real name)
the user's home directory
the shell (the program used to obey commands on login)
The personal information is called "the GECOS field" for some reason.
The equivalent entry in /etc/shadow is:
boris:<encrypted password>:11226:0:99999:7:-1:-1:134538484
the extra fields contain things like the date that boris last changed his
password, which is used by software which bullies users into changing their
password regularly.
To connect to the system, Boris supplies his user name (boris) and his
plain-text password. The software doing the authentication encrypts the
password and checks it against the one in /etc/shadow. The encrypted
passwords can be be decrypted by a lot of brute force, so only software
running as the superuser root can read /etc/shadow.
Lots of software read the information in /etc/passwd. For example, the
filing system stamps file with the numeric user id of the owner, not the
user name. The "ls" tool lists information about files including the user
name of the owner. To do that, it searches /etc/passwd to convert the user
id to the user name.
Each file is also stamped with a group id. This is used for sharing access
to files. Files can be made accessible by their owner, their group or all
users. If a file is stamped with the group "users" and is readable by users
in its group, and Boris is a member of that group, he can read the file.
/etc/group contains entries like this:
users::100:
meaning that the group "users" has group id 100. Boris is a member of that
group, because of the group id specified by his entry in /etc/passwd.
Any system that does authentication on behalf of a UNIX system needs to
store the same set of data - user names, user ids, group names, group ids
and so on. Windows NT has a similar but different structure of
authentication data. A system that handles authentication for UNIX and
Windows needs to hold all the data used by both systems.
Authenticating a login involves checking the user's password and then
handing the control data for that user to the system - the user's user id,
group id, login shell and so on.
PAM
Originally, each piece of software that did authentication (the telnet
server, the ftp server, the web server or whatever) had special code built
in. Nowadays we use Pluggable Authentication Modules (PAM). The PAM module
is called by the software that is doing the authentication, using a shared
library. In an LDAP environment, the PAM module uses the LDAP server to
authenticate the user. PAM is supported by UNIX and Windows systems and PAM
modules are available off the shelf.
Linux authenticates using PAM by default and it has a set of modules for
telnet, ftp and so on. There's also a PAM user guide in HTML format, but
it's quite well hidden. In the distribution of Linux that I use, it's in
/usr/doc/pam-0.66/html.
The PAM LDAP module is not part of LINUX PAM. It's available from
www.padl.com. The documentation is scant.
There is a PAM module for each type of authentication. For example
the su command lets one UNIX user act as another user. To use it, you need
to supply the other user's name and password, so the command does
authentication. It has its own PAM module /etc/pam.d/su:
#%PAM-1.0
auth sufficient /lib/security/pam_ldap.so
auth required /lib/security/pam_unix_auth.so use_first_pass
account sufficient /lib/security/pam_ldap.so debug
account required /lib/security/pam_unix_acct.so
password required /lib/security/pam_cracklib.so
password sufficient /lib/security/pam_ldap.so
password required /lib/security/pam_pwdb.so use_first_pass
session required /lib/security/pam_unix_session.so
The module is used at different stages. The "auth" lines handle the actual
authentication. The first line says to authenticate using the shared
library pam_ldap.so. This collects the user name and password. If the LDAP
server is available, it checks the password. If not, the code drops through
to the module specified on the second line. This picks up the user name and
password from the first module (because of the "use_first_pass" argument)
and checks it against the local password file /etc/passwd. That's a very
useful backup feature, particularly when testing a new LDAP server.
The "sufficient" keyword in the first line means that if the LDAP password
check works, no more authentication modules are called. If the LDAP server
rejects the password, the su attempt fails. the PAM module only carries on
to use the local password file if the LDAP server does not respond.
If you take the second line out, then the authentication will rely on the
LDAP server. If it's not running, you can't use the su command.
The other lines define what happens at other times, for example, when the
user tries to change their password.
You have to be very careful when editing a PAM module that you don't break
it and then lock yourself out. Keep a window logged in as root while you
are working. Make sure that you can still log in as root before you shut
the system down, otherwise you won't be able to get back in to fix it next
time. (The PAM manual explains how to get out of this hole, but you need to
print the instructions before you start work. After you've broken
something, you might not be able to read those files.)
When I installed the PAM LDAP module on my Linux system, it didn't work at
first, because the runtime linker couldn't see the shared libraries. The
system makes a list of shared libraries which you have to update. I had to
add a line to /etc/ld.so.conf:
/usr/local/lib
which is where my PAM LDAP shared libraries are installed, then I have to
do:
/sbin/ldconfig -v
This is done on every reboot, but it's nice to avoid a reboot.
If you are using a UNIX system rather than Linux, you may not have to do
this. See the manual entry for ldconfig (if there is such a command on your
system).
You get the source code for the PAM LDAP module, but it's not easy to
follow. In the final section of this note, I describe some software I wrote
to figure out what it does.
LDAP
LDAP is a general-purpose database management system, optimised for use as a
directory server. Authentication is one of the jobs it was designed to do.
The LDAP server can be remote from the system which you are trying to log
into, possibly on another site, or it can be on the same computer. the
server receives requests across the network and responds to them.
The openldap web site mentioned earlier has a free LDAP server. I compiled
and installed it without difficulty, EXCEPT that the make file expects two
files to be present and bombs out if they are not there. (One is called
NEWS, I can't remember the other.) Creating empty files (with the touch
command) keeps it happy.
To authenticate UNIX logins, you have to build a suitable set of data. Data
is imported and exported in LDIF format. This defines a set of attributes
and values.
The LDAP database is hierarchical. Each set of data falls into a domain,
which can relate to an internet domain. My domain is "home.sys", which only
exists within my home network.
The attributes supported by an LDAP database are defined by a set of
schemas. The schema for an attribute defines its format - whether it is
text, integer, positive integer or whatever. There are a set of ready-made
schemas for authentication, but they are NOT imported by default. If your
LDIF file contains an attribute that is not defined by the schemas you have
imported, the data will not load.
Some of the schemas are wrong, as explained below.
The configuration file slapd.conf defines various things including the
schemas to import, the root user (for the LDAP software) and that user's
password. In a live version the password should be encrypted. In my
isolated test system, I used plain text. One less thing to go wrong. I
created a dummy user "noris" to be the LDAP root user.
In the configuration file, a line starting with a # is a comment. Comments
marked SAR are mine:
# $OpenLDAP: pkg/ldap/servers/slapd/slapd.conf,v 1.8.8.4 2000/08/26 17:06:18
kurt Exp $
#
# 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/cosine.schema
include /usr/local/etc/openldap/schema/nis.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.
#referral ldap://root.openldap.org
pidfile /usr/local/var/slapd.pid
argsfile /usr/local/var/slapd.args
# log function calls (1) and connection mgmnt (8)
loglevel 9
# I don't know what this next bit is about - I can't find these files in
# my OpenLDAP distribution. SAR 21 Sept 2000.
# Load dynamic backend modules:
# modulepath /usr/local/libexec/openldap
# moduleload back_ldap.la
# moduleload back_ldbm.la
# moduleload back_passwd.la
moduleload back_shell.la
#######################################################################
# ldbm database definitions
#######################################################################
database ldbm
#suffix "o=My Organization Name, c=US"
#rootdn "cn=Manager, o=My Organization Name, c=US"
suffix "o=home, c=sys"
rootdn "cn=noris, o=home, c=sys"
# Cleartext passwords, especially for the rootdn, should
# be avoid. See slappasswd(8) and slapd.conf(5) for details.
# Use of strong authentication encouraged.
rootpw n0risn
# 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
There's another config file ldap.conf. It also defines the root user and
password and THE ENTRIES IN THE TWO CONFIG FILES MUST MATCH. This is my
ldap.conf file. Note the commented-out entry which shows how to represent
an encrypted password:
# $OpenLDAP: pkg/ldap/libraries/libldap/ldap.conf,v 1.4.8.6 2000/09/05
17:54:38 kurt Exp $
#
# LDAP Defaults
#
# See ldap.conf(5) for details
# This file should be world readable but not world writable.
#BASE dc=example, dc=com
#URI ldap://ldap.example.com ldap://ldap-master.example.com:666
#SIZELIMIT 12
#TIMELIMIT 15
#DEREF never
# Not sure what this does. Changing it doesn't seem to make any
# difference. (SAR)
directory /usr/local/var/openldap-ldbm
# MUST match settings in slapd.conf
suffix "o=home, c=sys"
rootdn "cn=noris, o=home, c=sys"
rootpw n0risn
# rootpw {crypto}$1$.Hl1/zRt$k9a.62WXw7i7GnL.RUbqZ/
index cn, sn, uid, gidnumber pres, eq, approx
index objectclass pres,eq
dbcachesize 500000
index default none
There are a set of tools on the www.padl.com website which take the data
from /etc/passwd, /etc/shadow and /etc/group and produce LDIF data files.
You can then import these into your database. However, the tools assume
that a few attributes are already set up, so you have to do this first.
This is the domain and the high-level classes of data "Groups" and "eople".
I defined these in a file "init.ldif":
# initial attributes for LDAP authentication database
# Specify root value, Group and People. We can then import the
# attributes from /etc/group, /etcpasswd and /etc/shadow.
dn: o=home, c=sys
objectclass: top
objectclass: organization
o: home
dn: ou=Group, o=home, c=sys
objectclass: top
objectclass: organizationalUnit
ou: Group
dn: ou=People, o=home, c=sys
objectclass: top
objectclass: organizationalUnit
ou: People
Now you can import the group data. The attributes for the group "users"
look like this:
dn: cn=users,ou=Group,o=home,c=sys
objectClass: posixGroup
objectClass: top
cn: users
gidNumber: 100
Finally, you can import the user data. The entry for boris looks like this:
dn: uid=boris,ou=People,o=home,c=sys
uid: boris
cn: Boris Morris
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword: {crypt}$1$VCBun4.2$CHSPciCw.tkoI1McHIMYo/
shadowLastChange: 11226
shadowMax: 99999
shadowWarning: 7
shadowFlag: 134538484
loginShell: /bin/bash
uidNumber: 1101
gidNumber: 100
homeDirectory: /home/boris
gecos: Boris Morris
However, there is a discrepency between some of the data and the schemas.
In the entry for boris in /etc/shadow, the shadowExpire and shadowInactive
entries are both -1, but the schema says that they can't be negative. The
correct solution is to use the right schema. As a quick fix, I filtered the
negative entries out of the data.
So, finally, we have a user boris in the group users in the domain home.sys.
We could have more than one domain, and there could be another user boris
with different attributes within each one - one LDAP server can serve
several independent domains.
This is the script I used to create a fresh LDAP database. For debugging,
it puts the group and password data into separate files.
#!/bin/bash
# script to build an LDAP authentication database from /etc/group,
# /etc/passwd and /etc/shadow. Must be run as root so that
# migrate-passwd.pl can read /etc/shadow. Server must not already be
# running - caches database, so clearing out the files isn't enough.
# Stop server, clear any existing database and start server.
kill -TERM `cat /usr/local/var/slapd.pid`
rm -fr /usr/local/var/openldap-ldbm/*
/usr/local/libexec/slapd
sleep 10 # server takes a short while to be ready
# Import initial attributes
/usr/local/bin/ldapadd -f init.ldif -D "cn=noris, o=home, c=sys" -w n0risn
# Import groups
/usr/local/bin/ldap/migrate_group.pl /etc/group >group.ldif
/usr/local/bin/ldapadd -f group.ldif -D "cn=noris, o=home, c=sys" -w n0risn
# Import passwd. (Imports shadow automatically when run by root).
# Remove any shadowInactive attributes with a negative value. The
# schema is faulty and doesn't allow them.
/usr/local/bin/ldap/migrate_passwd.pl /etc/passwd |
fgrep -v "shadowExpire: -" |
fgrep -v "shadowInactive: -" >passwd.ldif
/usr/local/bin/ldapadd -f passwd.ldif -D "cn=noris, o=home, c=sys" -w n0risn
Error handling is not good on imports. If it doesn't recognise an
attribute, it tells you which entry it is importing, but not which
attribute. I had to make a special file containing just the entry for
boris, and import it over and over, removing different attributes each time
(and of course, certain attributes must be present). Once I knew which
attribute was not recognised, I searched for a suitable definition in the
standard schema files and supplied that feature.
(Once you get it to accept a dummy entry with some attributes missing, you
have to do an update rather than a create to get the real data in. That's
why I destroy the database and create it afresh each time.)
To test my LDAP database, I can search it for the user boris:
/usr/local/bin/ldapsearch -b "o=home, c=sys" "uid=boris"
which produces this:
version: 2
#
# filter: uid=boris
# requesting: ALL
#
# boris,People,home,sys
dn: uid=boris,ou=People,o=home,c=sys
uid: boris
cn: Boris Morris
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword:: e2NyeXB0fSQxJFZDQnVuNC4yJENIU1BjaUN3LnRrb0kxTWNISU1Zby8=
shadowLastChange: 11226
shadowMax: 99999
shadowWarning: 7
shadowFlag: 134538484
loginShell: /bin/bash
uidNumber: 1101
gidNumber: 100
homeDirectory: /home/boris
gecos: Boris Morris
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
Having set up the database, I changed the password for boris using the
passwd command. The encrypted password in /etc/passwd is now different from
the one in my LDAP database, so I can see which is being used.
I replaced the PAM module that handles the su command with the PAM LDAP
version, and tried to su to boris, supplying the old password. It worked.
Next, I supplied the new password. The authentication failed.
Finally, I shut down the LDAP server and tried again. This time, the new
password worked and the old password failed. If the LDAP server is running,
you have to use its password, if not, you have the one in the local password
file.
Once you have a working LDAP database, replace the authentication modules in
/etc/pam.d with the LDAP versions one by one, testing each service as you
go.
Writing a Backend
This is based on the information in the Michigan University document.
A backend allows you to intercept an LDAP query and break out into your own
code to process it. The feature is designed to let you query other
databases using the LDAP server as a front end. The LDAP server
communicates with the backend program via its standard input and standard
output channels.
To test all this, I wrote a set of shell scripts to act as backend programs.
I added entries to slapd.conf to call them for each query on the home.sys
domain:
#######################################################################
# shell backend definitions
#######################################################################
database shell
suffix "o=home, c=sys"
bind /home/simon/ldap/shdb/bind
unbind /home/simon/ldap/shdb/unbind
search /home/simon/ldap/shdb/search
compare /home/simon/ldap/shdb/compare
modify /home/simon/ldap/shdb/modify
modrdn /home/simon/ldap/shdb/modrdn
add /home/simon/ldap/shdb/add
delete /home/simon/ldap/shdb/delete
abandon /home/simon/ldap/shdb/abandon
All the scripts are the same (they are soft links to one script). The
script looks at the name it was called by, so it can respond appropriately.
It logs its name, its input and its output in a log file. the backends are
dummies. They are only capable of authenticating boris and they don't check
the password.
#! /bin/bash
# testbed script to respond to OpenLDAP shell backend requests. Just
# print name and args, then return a result that says we've worked.
# Authentication will work regardless of password and we will see results
# in the log file.
log=/home/simon/ldap/shdb/Log
op=`basename $0`
echo `date`: $0 ${op} $* >>${log}
echo DEBUG: `date`: $0 ${op} $*
case ${op} in
search)
cat >>${log}
echo >>${log}
echo dn: cn=boris,o=home,c=sys >>${log}
echo dn: cn=boris,o=home,c=sys
echo cn: boris >>${log}
echo userPassword: encrypted.string
echo userPassword: encrypted.string >>${log}
echo cn: boris
echo cn: Boris Norris >>${log}
echo cn: Boris Norris
echo sn: boris >>${log}
echo sn: boris
echo uid: boris >>${log}
echo uid: boris
echo >>${log}
echo
echo RESULT >>${log}
echo RESULT
echo code: 0 >>${log}
echo code: 0
exit 0
;;
bind)
cat >>${log}
echo >>${log}
echo RESULT >>${log}
echo RESULT
echo code: 0 >>${log}
echo code: 0
exit 0
;;
unbind)
# don't need to respond to an unbind request
cat >>${log}
exit 0
;;
*)
cat >>${log}
exit 0
;;
esac
exit 0
By running this LDAP configuration, I can track what the PAM LDAP module
does. I typed "su boris" in a command window and supplied the plain-text
password "password.for.boris". The PAM module first does a SEARCH to check
that the user exists and to get his details. The result includes the
encrypted password("encrypted.string"), but the PAM module ignores that. It
does a BIND as that user, supplying the plain-text password that I gave it.
The LDAP server checks the password before allowing the BIND, so if it
succeeds, the password is correct. The BIND is just a stunt to check the
password, so the PAM module does an UNBIND immediately.
If the user wants to change his password, the appropriate PAM module would
do a BIND and then issue suitable update commands before unbinding.
Here is a log of an authentication, with a couple of annotations (marked
with "<-"):
Wed Oct 4 12:49:47 BST 2000: /home/simon/ldap/shdb/search search
SEARCH
msgid: 2
suffix: o=home,c=sys
base: o=home, c=sys
scope: 2
deref: 0
sizelimit: 1
timelimit: 0
filter: (uid=boris) <- search for details for boris
attrsonly: 0
attrs: all
dn: cn=boris,o=home,c=sys <- backend sends details for boris
cn: boris
userPassword: encrypted.string
cn: Boris Norris
sn: boris
uid: boris
RESULT
code: 0
Wed Oct 4 12:49:47 BST 2000: /home/simon/ldap/shdb/bind bind
BIND
msgid: 3
suffix: o=home,c=sys
dn: cn=boris,o=home,c=sys <- server sends user name ...
method: 128
credlen: 18
cred: password.for.boris <- ... and password
RESULT <- backend sends result of bind (OK)
code: 0
Wed Oct 4 12:49:48 BST 2000: /home/simon/ldap/shdb/unbind unbind
UNBIND
msgid: 5
suffix: o=home,c=sys
dn:
The queries, including the bind request, are dealt with by calling the
backend scripts, so I can see from the log what passed back and forth. The
PAM module supplied the password "password.for.boris", the LDAP server
passed that to the bind script, the bind script replied with a zero result,
which means OK, so the LDAP server was happy. It then sent an "OK" result
to the PAM module, so it was happy, and allowed me to become boris.
For this simple test I used shell scripts, but the backends can be written
in any language, perl, C, Java or whatever. They just have to read their
standard input channel and respond appropriately. |
|