Just as using IMAP instead of POP3 allows you to keep your messages in one place (the server) while using as many different email clients as you wish, so too does putting your address book in LDAP free you from having your contacts in only one program, and on only one computer. However, quite unlike using a straightforward protocol like IMAP, actually implementing and then using the Lightweight Directory Access Protocol is anything but easy. Here I share what I've learned in getting LDAP + TLS (SSL) up and working for both Evolution on Ubuntu Linux and Horde on CentOS Linux.
This guide is going to assume that you have two machines, both running Linux. The first will be the server, and host OpenLDAP, phpLDAPadmin, and Horde; the second will be the client, and will be running Evolution. I'm going to be fairly specific in terms of assuming that the server is running CentOS Linux, and the client Ubuntu. If you're running other flavors of Linux, then this guide will probably still work for you, but you may need to adjust certain paths and other settings to match your own specific distribution of Linux.
First, make sure that all the required OpenLDAP-related RPMs are installed on your server:
$ sudo yum install openldap openldap-clients openldap-servers
Make a backup of your shiny new /etc/openldap directory, as we will be heavily modifying its contents:
$ sudo cp -rp /etc/openldap /etc/openldap-backup
Make sure OpenLDAP is configured to start on boot, but is not yet running:
$ sudo chkconfig ldap on $ sudo service ldap stop
Now we need to generate two passwords, one which will be used for Manager, which is basically the "root" user within LDAP; and another for your own personal account that you will use to connect to LDAP for non-administrative use (like looking someone up in the address book.) Ideally the two passwords should be different. First, create the password for the Manager account:
$ slappasswd
Enter your password twice, and you'll be rewarded with a line that looks something like this:
{SSHA}ZmBCh3WwEjMRNhdANh/lOyn3zHSPzFifMake a note of it, and we will need it several times later on in this process. Second, create the password for your own personal LDAP login:
$ slappasswd
Again, make a note of the resulting hash.
Edit your /etc/openldap/slapd.conf file, and change it so that it appears like this (comments removed for simplicity):
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema
pidfile /var/run/openldap/slapd.pid
argsfile /var/run/openldap/slapd.args
TLSCACertificateFile /etc/pki/tls/certs/ca-bundle.crt
TLSCertificateFile /etc/pki/tls/certs/ldap.pem
TLSCertificateKeyFile /etc/pki/tls/private/ldap.key
TLSVerifyClient never
security tls=128
access to *
by self write
by users write
by anonymous auth
database bdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw {SSHA}ZmBCh3WwEjMRNhdANh/lOyn3zHSPzFif
directory /var/lib/ldap
index objectClass eq,pres
index ou,cn,mail,surname,givenname eq,pres,sub
index uidNumber,gidNumber,loginShell eq,pres
index uid,memberUid eq,pres,sub
index nisMapName,nisMapEntry eq,pres,sub
loglevel 256Let's walk through this file, section by section, to understand what it does. The lines I don't mention are the defaults that you should leave as-is and aren't really noteworthy.
The include statements basically describe the internal structure of the LDAP data. The files they point to are analogous to the table structures within a MySQL database; they define “column types” and the expectations of the data format you plan to store there.
The TLS* entries tell OpenLDAP where to find the various files required to encrypt LDAP communication. You'll need to either generate your own SSL certificate, or purchase an SSL certificate from a recognized certificate authority. I went the latter route, and so generating your own SSL certificate is beyond the scope of this guide. There are plenty of good tutorials on the web for SSL.
The TLSVerifyClient line is important, as it instructs the LDAP server to skip trying to verify the client. Successfully setting up client certificates and everything related to this is, in my opinion, far too difficult given the modicum of security gained.
The security line is important, as it forces incoming connections to use TLS with an encryption strength of at least 128 bits. You could set it higher, but then things like Evolution will break. Omitting the line means that non-encrypted traffic will be permitted, and passwords sent in the clear. That's bad, so don't do it.
The access lines configure how LDAP allows users to connect, login, and access data. LDAP handles its own internal authentication (meaning, it doesn't talk to PAM, or consult the /etc/passwd file, etc.) by self write means that the “owner” of a record can modify that record; by users write means that anyone who has successfully logged in to LDAP can read, change, and delete records; and finally, by anonymous auth means that everyone else, who have not yet logged in, can connect so that they can try to login. They cannot do anything else, however.
suffix establishes the “namespace” within which your data will be held. Since you probably have your own domain, substitute it for example.com here and throughout the rest of this guide.
rootdn specifies the special user account that holds root powers over LDAP. It is every bit as powerful, within LDAP, as the root user on your Linux server. You should treat its password with the same level of security as you do the UNIX root user. Since Manager is the default, you might want to change it to something else.
Make sure the hash following the rootpw statement is the one you generated earlier for the Manager account.
The last piece worth noting is the loglevel line. 256 conveys enough information to be useful in troubleshooting but without flooding the log file. However, if you need more information than it provides, you can temporarily increase it to a lower number, such as 1, to gain a ton of debugging output.
OpenLDAP logs via the syslog() service to the local4 facility. To start capturing this output, you'll need to add the following line to your /etc/syslog.conf file:
local4.* /var/log/slapd
Restart syslog by running:
$ sudo service syslog restartBegin tailing the OpenLDAP log file in the background, so later we can see what's going on:
$ sudo tail -f /var/log/slapd &
Now make a copy of the DB_CONFIG.example file and fix the ownership information on it:
$ sudo cp /etc/openldap/DB_CONFIG.example /var/lib/ldap/DB_CONFIG $ sudo chown ldap:ldap /var/lib/ldap/DB_CONFIG
At long last, we can now start the OpenLDAP server:
$ sudo service ldap startVerify that you can login as the Manager user:
$ ldapsearch -ZZ -x -D 'cn=Manager,dc=example,dc=com' -w 'password' -s base '(objectclass=*)' namingContexts
You should get back something like the following:
# extended LDIF # # LDAPv3 # base <> with scope baseObject # filter: (objectclass=*) # requesting: namingContexts # # example.com dn: dc=example,dc=com # search result search: 3 result: 0 Success # numResponses: 2 # numEntries: 1
Within your /var/log/slapd log file there should now be lines like this:
Jan 1 00:00:00 server slapd[10743]: conn=127 fd=17 ACCEPT from IP=192.168.1.1:60320 (IP=0.0.0.0:389) Jan 1 00:00:00 server slapd[10743]: conn=127 op=0 STARTTLS Jan 1 00:00:00 server slapd[10743]: conn=127 op=0 RESULT oid= err=0 text= Jan 1 00:00:00 server slapd[10743]: conn=127 fd=17 TLS established tls_ssf=256 ssf=256 Jan 1 00:00:00 server slapd[10743]: conn=127 op=1 BIND dn="cn=Manager,dc=example,dc=com" method=128 Jan 1 00:00:00 server slapd[10743]: conn=127 op=1 BIND dn="cn=Manager,dc=example,dc=com" mech=SIMPLE ssf=0 Jan 1 00:00:00 server slapd[10743]: conn=127 op=1 RESULT tag=97 err=0 text= Jan 1 00:00:00 server slapd[10743]: conn=127 op=2 SRCH base="dc=example,dc=com" scope=0 deref=0 filter="(objectClass=*)" Jan 1 00:00:00 server slapd[10743]: conn=127 op=2 SRCH attr=namingContexts Jan 1 00:00:00 server slapd[10743]: conn=127 op=2 SEARCH RESULT tag=101 err=0 nentries=1 text= Jan 1 00:00:00 server slapd[10743]: conn=127 op=3 UNBIND Jan 1 00:00:00 server slapd[10743]: conn=127 fd=17 closed
Let's examine a few of these lines. The first, ACCEPT, indicates that our LDAP client (in this case, ldapsearch) successfully attached to the server.
Next, STARTTLS shows that it requested an encrypted connection via TLS, and the next two lines, TLS established shows that it was successful. This is good, as the next line, BIND is when the password for your Manager account was sent to the server, in plain text, but encrypted within the SSL connection.
The remaining lines are the client and server asking and receiving the information that ldapsearch then printed to your screen.
Finally, UNBIND shows the client disconnecting in a graceful fashion.
At this point, we have a working OpenLDAP server, a single “root” login, and verification that TLS encryption works. We can now move on to safely permitting external access to LDAP through iptables.
Note that if you've installed phpLDAPadmin on your server, you will need to edit the /etc/phpldapadmin/config.php file and set the following line to read true instead of false:
/* Use TLS (Transport Layer Security) to connect to the LDAP server. */ $ldapservers->SetValue($i,'server','tls',true);
Let's now enable external access to LDAP. First, save your existing iptables rules by running:
$ sudo service iptables saveEdit the /etc/sysconfig/iptables file and add the following line within the *filter section:
-A INPUT -p tcp -m tcp --dport 389 -m state --state NEW -j ACCEPT
Restart iptables by running:
$ sudo service iptables restartNext we will create the user account you'll use to actually query and update the LDAP address book on a day-to-day basis. You could use the Manager account for this, but it's dangerous and unnecessary.
Create a file somewhere on your server with the following content:
dn:uid=joe,ou=users,dc=example,dc=com
uid: joe
userPassword: {SSHA}eCE23fZaD0W2EjTbXPTUVv6Xy0LBeuLl
objectClass: top
objectClass: account
objectClass: simpleSecurityObjectOf course, change joe to whatever your username is, and make sure to put the second hash you generated earlier in after the userPassword: (don't use your Manager hash!)
Save the file, and then “import” it into LDAP to create the account:
$ ldapadd -ZZ -x -D 'cn=Manager,dc=example,dc=com' -w 'password' -f useraccount.ldif
Note that ldapadd will login as Manager and then use the contents of useraccount.ldif (or whatever you called the file) to create the new user account.
Now test the new user account:
$ ldapsearch -ZZ -x -D 'uid=joe,ou=users,dc=example,dc=com' -w 'password' -b 'ou=users,dc=example,dc=com' 'uid=joe'
You should get back your newly-created LDAP account information.
We are now ready to create the LDAP address book and import a single contact into it for testing purposes.
Create another file somewhere on your server with the following content:
dn:cn=Joe Blow,ou=addressbook,dc=example,dc=com cn: Joe Blow gn: Joe sn: Blow o: Company Name, Inc. l: City street: Street Address st: State postalCode: 12345 pager: 000-000-0000 homePhone: 000-000-0000 telephoneNumber: 000-000-0000 facsimileTelephoneNumber: 000-000-0000 mobile: 000-000-0000 mail: joe@example.com objectClass: top objectClass: inetOrgPerson
Add it to your LDAP server using the same ldapadd command, as above.
Now try searching your new addresss book for this entry:
$ ldapsearch -ZZ -x -D 'cn=joe,dc=example,dc=com' -w 'password' -b 'ou=addressbook,dc=example,dc=com' 'mail=joe*'
This should return the address book contact card for “Joe Blow”, based on the partial match (joe*) within the mail field. Try the search again with different parameters, such as st=State to get a feel for what is going on.
Also note that we're no longer using the Manager account for anything; from here on out we'll always be logging in with your personal account (joe in the above examples.)
With the server now finished, it's time to configure the client and get Evolution talking to the LDAP server.
On the client, backup and then edit the /etc/ldap/ldap.conf file, adding the following line to it:
TLS_REQCERT never
This will prevent the LDAP client (and Evolution) from complaining about the SSL certificate in use on your LDAP server. It's particularly important if you generated your own SSL certificate.
Kill all the Evolution processes running on your client. There are usually at least two, sometimes more:
- /usr/lib/evolution/evolution-data-server
- /usr/lib/evolution/2.26/evolution-exchange-storage
- /usr/lib/evolution/2.26/evolution-alarm-notify
This needs to be done so that they observe the change you just added to the ldap.conf file.
Start up Evolution, and click the Contacts button (or click View, Window, Contacts). In the leftmost pane, right-click and select New Address Book. Configure the dialog box accordingly (substituting values as needed, of course):
- Type: On LDAP Servers
- Name: LDAP Address Book
- Server: server.example.com
- Port: 389
- Use secure connection: TLS encryption
- Login method: Using distinguished name (DN)
- Login: uid=joe,ou=users,dc=example,dc=com
Click the Details tab and change the Search base: to:
ou=addressbook,dc=example,dc=com
Click the OK button to save your changes.
Now expand the On LDAP Servers list, and click on your newly-added LDAP entry.
If it works, you should see the following message within Evolution:
Search for the Contact or double-click here to create a new Contact
If you receive an error message from Evolution, then right-click on your newly-added LDAP server, choose Properties, and change the Use secure connection: to No encryption. Click OK, and then repeat this again, setting the encryption method back to TLS. Evolution will sometimes set itself to not use encryption, while still displaying TLS as the chosen method in the dialog box.
At this point you can add, edit, and view entries within your LDAP address book through Evolution. To configure Horde to use the same LDAP address book, edit the /usr/share/horde/turba/config/conf.php file and change the following line in it as shown:
$conf['client']['addressbook'] = 'ldap';
Next, edit the /usr/share/horde/turba/config/sources.php file and add the following section to it:
$cfgSources['ldap'] = array( 'title' => _("My Address Book"), 'type' => 'ldap', 'params' => array( 'server' => 'server.example.com', 'tls' => true, 'root' => 'ou=addressbook,dc=example,dc=com', 'bind_dn' => 'uid=joe,ou=users,dc=example,dc=com', 'bind_password' => Auth::getCredential('password'), 'dn' => array('uid'), 'objectclass' => array('top', 'person', 'inetOrgPerson', 'organizationalPerson'), 'scope' => 'one', 'charset' => 'utf-8', 'version' => 3 ), 'map' => array( '__key' => 'dn', '__uid' => 'uid', 'name' => 'cn', 'email' => 'mail', 'lastname' => 'sn', 'title' => 'title', 'company' => 'o', 'businessCategory' => 'roomnumber', 'workAddress' => 'postaladdress', 'workPostalCode' => 'postalcode', 'workPhone' => 'telephonenumber', 'fax' => 'facsimiletelephonenumber', 'homeAddress' => 'homepostaladdress', 'homePhone' => 'homephone', 'cellPhone' => 'mobile', 'department' => 'ou', 'nickname' => 'displayname', 'website' => 'labeleduri', ), 'search' => array( 'name', 'email', 'businessCategory', 'title', 'homePhone', 'workPhone', 'cellPhone', 'homeAddress' ), 'strict' => array( 'dn', ), 'approximate' => array( 'cn', ), 'export' => true, 'browse' => true, );
If you end up customizing or creating any of your own schemas within LDAP, you'll need to adjust the field mappings within the map array.
And finally, I searched wide and far for a way to query my own LDAP address book from my Windows Mobile 6 cell phone, but failed to find anything. Not to be deterred, I whipped up a tiny Perl script that runs as a CGI executable on my Apache server, and provides a read-only WTAI-compliant front-end to the LDAP server:
#!/usr/bin/perl -w # # Web front-end to an LDAP address book for mobile browsers # Copyright (C) 2009 Nathan W. Lindstrom # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. use strict; use Net::LDAP; use CGI qw(:standard); my $html; my $name = param ("name") || ""; my $mail = param ("mail") || ""; if (($name ne "") or ($mail ne "")) { my $query = $name; $query = $mail if ($mail ne ""); my $filter = "cn"; $filter = "mail" if ($mail ne ""); my $ldap = Net::LDAP->new ("server.example.com") or die "$@"; my $mesg = $ldap->start_tls (verify => "none"); $mesg = $ldap->bind ( "uid=joe,ou=users,dc=example,dc=com", password => "password", version => 3 ); my $result = $ldap->search ( base => "ou=addressbook,dc=example,dc=com", scope => "sub", filter => "$filter=*$query*" ); my @entries = $result->entries (); foreach my $entr ( @entries ) { my $table; foreach my $attr ( $entr->attributes () ) { unless ($attr eq "objectClass") { my $label = $attr; $label = reverse ($label); $label =~ s/^(.{5}).*/$1.../; $label = reverse ($label); my $data = $entr->get_value ($attr); if ($attr =~ /telephone|mobile/) { my $num = $data; $num =~ s/[^\d]+//g; $data = qq{<a href="wtai://wp/mc;$num">$data</a>}; } if ($attr =~ /URI/) { $data = qq{<a href="$data">$data</a>}; } if ($attr =~ /mail/) { $data = qq{<a href="mailto:$data">$data</a>}; } $table .= Tr ({ valign => "top" }, th ({ align => "right" }, "$label:"), td ($data) ); } } $html .= table ($table); $html .= hr (); } $html =~ s/\$/<br \/>/g; } print header (), start_html ("LDAP Address Book"), $html, start_form (), table ( Tr ( th ({ align => "right" }, "Name"), td (textfield ("name")) ), Tr ( th ({ align => "right" }, "Email"), td (textfield ("mail")) ), Tr ( td ({ colspan => 2 }, submit ()) ) ), end_form (), end_html ();
Feel free to use it on your own LDAP server. Just be sure to secure it using htpasswd and something like this in your httpd.conf Apache configuration file:
<Location /> AddHandler cgi-script .pl Options ExecCGI AuthName "LDAP Address Book" AuthType Basic AuthUserFile /var/www/.htpasswd Require valid-user </Location>
