March 30, 2008

Deploying OpenLDAP under FreeBSD 7.0

Categories: FreeBSD, Sysadmin.

Adrien Reboisson and Géraud de Mareschal are just about to launch Astase as a professional-grade software engineering company. While I will not be part of the adventure, I am pleased to help them as far as I can in order to make this launch a great success!

They have just bought a computer to act as their main server, and decided to run FreeBSD on it. As a FreeBSD expert (according to them), I was asked to provide some expertise in the setup of the machine. Surprisingly, it was the path of great challenges I had not experienced before: while considering the services that would be running on the computer, I realised an LDAP directory would make it easier to work on the system (with a single centralised password), to get in touch with customers (LDAP is a directory, right?), and would provide a reliable infrastructure for the future.

Requirements

At first sight, at least 3 branches are necessary for managing the directory entries:

customers
Customers information, accessible with read/write permissions by the company's staff.
services
Internal accounts for services (e.g. Apache httpd). Services will generally only need a read-only access to parts of the directory.
staff
Adrien and Géraud (and others soon).

We so have the following structure:

dc=astase,dc=com
|-> ou=customers
| `-> ...
|-> ou=services
| `-> ...
`-> ou=staff
  `-> ...

Installation

I chose to install OpenLDAP 2.3 (although version 2.4 is available) because the port of pam_ldap (and probably other tools) depends on that version of OpenLDAP and I did not have enough time to try to compile it against version 2.4.

# portinstall net/openldap23-server

The file /usr/local/ext/openldap/slapd.conf has to be configured according to the server's environment. At least, the suffix and rootdn of the database have to be changed. I also added the following schemes:

include         /usr/local/etc/openldap/schema/cosine.schema
include         /usr/local/etc/openldap/schema/inetorgperson.schema

While editing this file, permissions can be fixed:

access to attrs=userPassword by self write
                             by anonymous auth

access to * by self write
            by dn.children="ou=staff,dc=astase,dc=com" write
            by users read
            by anonymous auth

Before running the server, it is required to import the bare minimal into the directory database. I wrote astase.ldif as so:

dn: dc=astase,dc=com
dc: astase
objectClass: dcObject
objectClass: organization
o: Astase

dn: ou=staff,dc=astase,dc=com
ou: staff
objectClass: organizationalUnit

dn: ou=customers,dc=astase,dc=com
ou: customers
objectClass: organizationalUnit

dn: ou=services,dc=astase,dc=com
ou: services
objectClass: organizationalUnit

This file can be imported in the database using slapadd(8C) :

# slapadd -v -l astase.ldif

The server can now be launched. As usual in the FreeBSD world, /etc/rc.conf has to be edited to allow slapd(8C) to start:

# echo 'slapd_enable="YES"' >> /etc/rc.conf
# /usr/local/etc/rc.d/slapd start

It is then possible to fill in the directory using a cute editor. My personal preference goes to ldapvi, but you might prefer GQ...

Logging-in

Interesting things begin here!

We have an LDAP directory containing users with logins and passwords on a system containing users ... with logins and passwords! The first thing we may want to do is to associate both so that a single user only have a single password, in other word identify users using the information stored in the LDAP directory.

Under FreeBSD (and GNU/Linux), this can be done using PAM.

Pluggable Authentication Module (PAM)

FreeBSD default setup provides various PAM modules (you can list them issuing ls /usr/lib/pam_* in a terminal), but does not provide PAM module for LDAP. Such a module is provided as a third party software package called pam_ldap:

# portinstall pam_ldap

As any third party package, files are installed in $LOCALBASE (defaults to /usr/local) and PAM will not be able to find the library pam_ldap.so. This can be fixed by symlinking the library in /usr/lib:

# ln -s /usr/local/lib/pam_ldap.so /usr/lib

The file /usr/local/etc/ldap.conf has to be edited to configure how the PAM module is supposed to search information in the LDAP directory. You can refer to /usr/local/etc/ldap.conf.dist (installed by pam_ldap) for details about all available options. My ldap.conf file looks like this:

host 127.0.0.1
base dc=astase,dc=org
binddn cn=pam_ldap,ou=services,dc=astase,dc=com
bindpw secret
scope sub
pam_password exop
nss_base_passwd ou=staff,dc=astase,dc=com
nss_base_shadow ou=staff,dc=astase,dc=com

The pam_ldap distinguish name refer to the user account PAM will use to query the LDAP directory. This user of the services organisational unit has been created with ldapvi like so:

add cn=pam_ldap,ou=services,dc=astase,dc=com
cn: pam_ldap
objectClass: top
objectClass: inetOrgPerson
sn: PAM
userPassword: secret

Everything is now ready. We can now tweak the PAM configuration. But beware! Mistakes with PAM will either prevent you from being able to login anymore, or grant you access whatever password you type in (considering a password is asked). It is so recommended to have a physical access to the machine, and be logged in on many terminals before breaking everything.

ssh(1)

The file /etc/pam.d/sshd describes the behaviour of remote logins via SSH. If we consider that only real users can remotely log-in (i.e. no ssh root@..., in other word the default under FreeBSD), we can safely replace unix auth with ldap.

# auth
auth            sufficient      pam_opie.so             no_warn no_fake_prompts
auth            requisite       pam_opieaccess.so       no_warn allow_local
#auth           sufficient      pam_krb5.so             no_warn try_first_pass
#auth           sufficient      pam_ssh.so              no_warn try_first_pass
auth            required        pam_ldap.so
#auth            required        pam_unix.so             no_warn try_first_pass

# account
account         required        pam_nologin.so
#account        required        pam_krb5.so
account         required        pam_login_access.so
account         required        pam_ldap.so
#account         required        pam_unix.so

# session
#session        optional        pam_ssh.so
session         sufficient      pam_ldap.so
session         required        pam_permit.so

# password
#password       sufficient      pam_krb5.so             no_warn try_first_pass
password        required        pam_ldap.so
#password        required        pam_unix.so             no_warn try_first_pass

passwd(1)

Now that it is possible to log-in using the password stored in LDAP, it is helpful to tell passwd(1) to change the password of the user in the LDAP directory instead of the one in the shadow file. This is done by editing /etc/pam.d/passwd like this:

# password
#password	requisite	pam_passwdqc.so		enforce=users
###password	required	pam_unix.so		no_warn try_first_pass nullok
password	required	pam_ldap.so

su(1)

su(1)'s PAM configuration file (/etc/pam.d/su, you guessed it) reference the /etc/pam.d/system file. I would advise to keep the unix auth as a fallback so that it is still possible to switch to another user not in the LDAP directory (e.g. root):

#
# System-wide defaults
#

# auth
auth            sufficient      pam_opie.so             no_warn no_fake_prompts
auth            requisite       pam_opieaccess.so       no_warn allow_local
#auth           sufficient      pam_krb5.so             no_warn try_first_pass
#auth           sufficient      pam_ssh.so              no_warn try_first_pass
auth            sufficient      pam_ldap.so
auth            required        pam_unix.so             no_warn try_first_pass nullok

# account
#account        required        pam_krb5.so
account         required        pam_login_access.so
account         sufficient      pam_ldap.so
account         required        pam_unix.so

# session
#session        optional        pam_ssh.so
session         required        pam_ldap.so
session         required        pam_lastlog.so          no_fail

# password
#password       sufficient      pam_krb5.so             no_warn try_first_pass
password        required        pam_unix.so             no_warn try_first_pass

sudo(8)

Just like su(1), sudo(8) relies on the /etc/pam.d/system configuration file so it is already configured.

But sudo can do better! All sudo rules can be stored in the LDAP directory. The FreeBSD port of sudo has a WITH_LDAP knob to enable this feature. A schema is then installed and can be copied with the others...

# cp /usr/local/share/doc/sudo/schema.OpenLDAP /usr/local/etc/openldap/schema/sudo.schema

... added to the list of included schemas ...

include         /usr/local/etc/openldap/schema/sudo.schema

... before rebooting the server to take the changes into account...

# /usr/local/etc/rc.d/slapd restart

The LDAP directory then needs a new node to store sudo rules. ldapvi is still my friend:

add ou=sudoers,ou=services,dc=astase,dc=com
ou: sudoers
objectClass: organizationalUnit

An existing sudoers file can then be easily imported in the LDAP directory:

# setenv SUDOERS_BASE ou=sudoers,ou=services,dc=astase,dc=com
# /usr/local/share/doc/sudo/sudoers2ldif /usr/local/etc/sudoers > /tmp/sudoers.ldif
# ldapadd -f /tmp/sudoers.ldif -h dev.astase.com -D 'cn=Manager,dc=astase,dc=com' -W -x
Enter LDAP Password: 
adding new entry "cn=defaults,ou=sudoers,ou=services,dc=astase,dc=com"
adding new entry "cn=root,ou=sudoers,ou=services,dc=astase,dc=com"
adding new entry "cn=%wheel,ou=sudoers,ou=services,dc=astase,dc=com"
#

sudo(8)'s LDAP base has to be configured in /usr/local/etc/ldap.conf:

sudoers_base   ou=sudoers,ou=services,dc=astase,dc=com

The file /usr/local/etc/sudoers is then redundant. I recommend emptying it in order to avoid to have rules in the LDAP directory and the sudoers file. Using visudo, I wrote the following in the sudoers file:

# This file intentionally left empty.
#
# SUDO is configured in the LDAP directory under
# ou=sudoers,ou=services,dc=astase,dc=com

Last step: tell sudo to read it's configuration from LDAP, not files, adding this line to /etc/nsswitch.conf:

sudoers: ldap

Apache httpd

The Apache web server provide two LDAP modules: mod_ldap and mod_authnz_ldap. The former act as a cache for LDAP queries, and the later allows basic authentication.

These modules are available as options for apache (disabled by default). If you have compiled these LDAP modules, you should file them typing-in:

# ls /usr/local/libexec/apache22/ | grep ldap
mod_authnz_ldap.so
mod_ldap.so
# 

A quick way to test both modules is to setup a special page that displays LDAP cache statistics (mod_ldap) protected by a basic LDAP authentication (mod_authnz_ldap). This was inspired by the Apache documentation:

LDAPSharedCacheSize 200000
LDAPCacheEntries 1024
LDAPCacheTTL 600
LDAPOpCacheEntries 1024
LDAPOpCacheTTL 600

<Location /ldap-status>
        SetHandler ldap-status
        Order deny,allow
        Deny from all
        Allow from 192.168

        AuthType basic
        AuthBasicProvider ldap
        AuthName "Apache httpd LDAP Cache"

        AuthLDAPURL ldap://127.0.0.1/ou=staff,dc=astase,dc=com?uid
        AuthLDAPBindDN cn=httpd,ou=services,dc=astase,dc=com
        AuthLDAPBindPassword secret

        Require valid-user
</Location>

The httpd user was created with ldapvi:

add cn=httpd,ou=services,dc=astase,dc=com
cn: httpd
objectClass: top
objectClass: inetOrgPerson
sn: Apache httpd
userPassword: secret

AddressBook

Configuring an AddressBook can be really quick. can because I have just tried to setup Evolution and that was really easy (two much?). Evolution use its own schema that you have to copy on the OpenLDAP server and include in the configuration. That's all!

The schema is part of evolution-data-server and installed as:

/usr/local/share/evolution-data-server-2.22/evolutionperson.schema

Backing-up the LDAP directory

At the time this article was written, slapcat(8) could not operate on a running OpenLDAP server. This shell script was a workaround to prevent disasters, but you SHALL use slapcat(8) to do backup now!

We now have a complete setup. The last step (and not the least important one!) is to setup automatic backup of the directory. I like to do this backup just like PostgreSQL ports do it: every day, databases are dumped and a one-week history is kept. This can be implemented in /usr/local/etc/periodic/daily/100.openldap with the following shell-script:

#!/bin/sh

LDAPSEARCH="/usr/local/bin/ldapsearch"
BACKUPDIR="/var/backups/openldap"
PASSWDFILE="/root/.ldap-backup-password"
HOST="127.0.0.1"
BINDDN="cn=backup,ou=services,dc=astase,dc=com"
BASE="dc=astase,dc=com"
FIND="/usr/bin/find"
BZIP2="/usr/bin/bzip2"

umask 077
echo
echo "OpenLDAP maintenance"
if [ '!' -d  "${BACKUPDIR}" ]; then mkdir "${BACKUPDIR}" ; fi
"${LDAPSEARCH}" -L -H "ldap://${HOST}" -D "${BINDDN}" -y "${PASSWDFILE}" -b "${BASE}" | \
  ${BZIP2} - > "${BACKUPDIR}/${BASE}-`date +%F`.ldif.bz2"

# Clean up
if [ $? -eq 0 ]; then
	${FIND} "${BACKUPDIR}" -type f -ctime +1w -delete
fi

One more time, an entry has to be added to the directory for the backup service. One more time ldapvi can do this:

add cn=backup,ou=services,dc=astase,dc=com
cn: backup
objectClass: top
objectClass: inetOrgPerson
sn: backup
userPassword: secret

Here I chose to put the password in another file (just to mention the -n flag witch is required... if missing the password will end with a carriage return and authentication of the backup user will fail):

# umask 077
# echo -n 'secret' > /root/.ldap-backup-password

Last, the access rule for user passwords has to be changed so that the backup user can effectively save all user passwords:

access to attrs=userPassword by self write
                             by dn="cn=backup,ou=services,dc=astase,dc=com" read
                             by anonymous auth

cron(8) will now backup the LDAP directory every night at 3am. If no backup occurs, check that the following line exists in /etc/periodic.conf:

local_periodic="/usr/local/etc/periodic"

Overview

That's it! All the LDAP infrastructure is ready. The directory tree is finally like this (not far away from what I though at the beginning):

dc=astase,dc=com
|-> ou=customers
| `-> ...
|-> ou=services
| |-> cn=backup
| |-> cn=httpd
| |-> cn=pam_ldap
| `-> ou=sudoers
|   |-> cn=%wheel
|   |-> cn=defaults
|   `-> cn=root
`-> ou=staff
  |-> cn=Adrien Reboisson
  `-> cn=Géraud de Mareschal

Hope that help! Good luck boys!

Comments

On March 30, 2008, Adrien Reboisson wrote:

A great, great, great work :-)
Thank you very much for the time you spend !
(pourquoi j'écris en Anglais moi ? ^^)

On April 5, 2010, Joost wrote:

Hi

I get this error doing the above

/usr/local/etc/openldap/slapd.conf: line 15: suffix <dc=uwaterloo,dc=ca> not allowed in frontend database. slapadd: bad configuration file!

Do you know what it means and how this can be solved? Thanks in advance.

On April 5, 2010, Romain Tartière wrote:

Hey!

Maybe a base-dn error. Difficult to say without seeing the slapd.conf file. /var/log/debug.log may also contain useful details ;-)

Comments RSS feed | Leave a Reply…

top