Adding login for LDAP users to Ubuntu 17.10 client with TLS

I’ve configured an LDAP database with users, now I need to setup a client(fangorn) to log in.  This client is running Ubuntu 17.10.

david@fangorn:~$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 17.10
Release:	17.10
Codename:	artful

Installing the Client LDAP Modules

Using apt, install the libpam and libnss ldap modules.

apt install libpam-ldapd libnss-ldapd

The debconf prompts the user to enter the nslcd daemon configuration data:
set the ldap server to ldap://barad-dur.telperion.org
set ldap search base to dc=telperion,dc=org
select passwd, group, shadow as services to configure

Configuring the Modules for TLS

Unfortunately, that isn’t enough, we also need to configure the nslcd daemon for secure tls access. Edit /etc/nslcd.conf to configure. The nslcd.conf manpage has helpful hints for any further configuration.

# The user and group nslcd should run as.
uid nslcd
gid nslcd

# The location at which the LDAP server(s) should be reachable.
uri ldap://barad-dur.telperion.org

# The search base that will be used for all queries.
base dc=telperion,dc=org

# The LDAP protocol version to use.
ldap_version 3

# force ldap to use start_tls protocol to setup tls connections
ssl start_tls

# force the tls connection to verify the other end
# using a cert signed by the CA
tls_reqcert demand

# path to pem file containing CA and CA intermediate files
tls_cacertfile = /etc/ssl/certs/telperion.org.ca-chain.cert.pem

# path to the client certificate
tls_certfile = /etc/ssl/certs/fangorn.telperion.org.cert.pem

# path to key file containing the private key for the cert
tls_key = /etc/ssl/private/fangorn.telperion.org.key.pem

# uncomment to enable logging to syslog for debugging issues
#log syslog debug

If you experience issues, try adding logging to the nslcd service by adding the following line to /etc/nslcd.conf and restarting nslcd service.

log syslog debug

pam ldap uses the default ldap.conf file located in /etc/ldap. Edit /etc/ldap/ldap.conf to add similar key paths and requirements for verification.

BASE    dc=telperion,dc=org
URI     ldap://barad-dur.telperion.org
TLS_CACERT      /etc/ssl/certs/telperion.org.ca-chain.cert.pem
TLS_CERT        /etc/ssl/certs/fangorn.telperion.org.cert.pem
TLS_KEY         /etc/ssl/private/fangorn.telperion.org.key.pem
TLS_REQCERT     demand

Make sure permissions are setup correctly on the private keys. The private key should be readable by root and ssl-cert group.

root@fangorn:/etc/ssl/private# ls -l fangorn.telperion.org.key.pem 
-r--r----- 1 root ssl-cert 288 Feb 17 10:44 fangorn.telperion.org.key.pem

The user nslcd must be added to the ssl-cert group using the command usermod -a -G ssl-cert nslcd, otherwise start_tls will fail.

Also make sure the certs have the correct permissions:

root@fangorn:/etc/ssl/certs# ls -l | grep telperion
-r--r--r-- 1 root root   1208 Feb 17 10:44 fangorn.telperion.org.cert.pem
-r--r--r-- 1 root root   1836 Feb 17 10:43 fangorn.org.ca-chain.cert.pem

Now that everything is configured, we need to restart the nslcd and nscd services to use the configuration. On ubuntu, run systemctl restart nscd and systemctl restart nslcd.

Testing Configuration

To validate that our configuration is working for nss (using the nslcd daemon installed above), we can use genent passwd to see your ldap users. I recently added a user account for Lobelia Sackville-Baggins as a member of the telperionusers group.

root@fangorn:~# getent passwd | grep lobelia
lobelia:x:10000:5000:Lobelia Sackville-Baggins:/home/lobelia:/bin/bash
root@fangorn:~# getent group | grep telperionusers
telperionusers:*:5000:
root@fangorn:~# getent shadow | grep lobelia
lobelia:*:::::::0

To get pam working out of the box, I didn’t need to do much. Simply restarting the nscd daemon was all that was required.

Running su -l lobelia with the password, I was able to log in.   Lobelia doesn’t yet have a home directory though.   We can configure pam to handle some of these setup items for us… another post.

Secure LDAP Transport Layer With TLS

So we’ve got an OpenLDAP server (slapd) running and we’ve added some users.

Searching for or authenticating with or modifying passwords without TLS is unsafe. The following ldapsearch and wireshark capture shows the password in cleartext.

root@fangorn:~# ldapsearch -D uid=lobelia,ou=people,dc=telperion,dc=org -LLL -H ldap://barad-dur.telperion.org -w  ihatebilbo -b uid=lobelia,ou=people,dc=telperion,dc=org -x  userPassword
dn: uid=lobelia,ou=people,dc=telperion,dc=org
userPassword:: e1NTSEF9dmJ5N0Iwbk5IeEx1VXR0WjBXSWRUQVdrQWlXRlpsSzQ=
wireshark shows cleartext ldap traffic

If we want to use it to authenticate, we should secure the traffic. The easiest way to do that is with SSL.

Setting up a certificate hierarchy is not trivial but it is best to either generate a self signed certificate authority or get a certificate signed by a reputable CA (e.g. LetsEncrypt). Because this sample is on a closed personal network, I’ve chosen to generate a self-signed domain certificate authority and an intermediate. The CA signs the intermediate CA and the intermediate CA signs the cert for each of the nodes in the network.

the root is self signed but installed in a trusted location on each node

The default slapd package in debian stretch uses gnutls to perform it’s crypto operations by default so when pouring over the slapd manpages, keep that in mind (some options are meant for slapd built against Mozilla’s NSS and some are for OpenSSL). gnutls is pretty strict about self-signed CAs. If building the CA with OpenSSL, make sure you use the CA extensions. Also, if using Elliptic Curves, make sure you use named curves. I had trouble getting gnutls to parse OpenSSL generated certs that had curve parameters explicitly defined. Switching to using the named NIST curves solved the problem. Establishing an appropriate PKI can be done with the certtool command (from gnutls) or using OpenSSL’s command line suite. I’ve used OpenSSL to generate a CA PKI, loosely following this guide: OpenSSL Certificate Authority.

Configure the Keys and Certs

Now to configure slapd to use this cert. First copy your certs into an appropriate location. I copied my ca cert chain (a file consisting of the concatenation of the root CA and the intermediate CA) and the server cert into /etc/ssl/certs. I put the private key in /etc/ssl/private. Change the group ownership of the private key to ssl-cert to allow the openldap user (a member of the ssl-cert group) to use the key.

Configure the Server Keys (barad-dur)

Place keys in the appropriate locations and give them the appropriate permissions.

root@barad-dur:~# ls -ltr /etc/ssl/certs | tail -n2
-r--r--r-- 1 root root   1836 Feb  3 16:27 telperion.org.ca-chain.cert.pem
-r--r--r-- 1 root root   1208 Feb  3 16:27 barad-dur.telperion.org.cert.pem
root@barad-dur:~# ls -ltr /etc/ssl/private/ | tail -n1
-r--r----- 1 root ssl-cert  288 Feb  3 16:27 barad-dur.telperion.org.key.pem
root@barad-dur:~# groups openldap
openldap : openldap ssl-cert

Configure the Client Keys (fangorn)

Set up the keys:

root@fangorn:~# ls -ltr /etc/ssl/certs/ | grep telperion
-r--r--r-- 1 root root   1200 Feb  3 17:09 fangorn.telperion.org.cert.pem
-r--r--r-- 1 root root   1836 Feb  3 17:09 telperion.org.ca-chain.cert.pem
root@fangorn:~# ls -ltr /etc/ssl/private/ | grep telperion
-r--r----- 1 root ssl-cert  288 Feb  3 17:09 fangorn.telperion.org.key.pem

Configure LDAP to use TLS

Now that we have a PKI that makes sense, we can tell slapd and libldap to use the keys we’ve provided.
On the client, it is pretty easy. Configure the default ldap configuration file (/etc/ldap/ldap.conf) on the client machine (fangorn in this case) as follows:

# See ldap.conf(5) for details
# This file should be world readable but not world writeable.

BASE    dc=telperion,dc=org
URI     ldap://barad-dur.telperion.org
TLS_CACERT      /etc/ssl/certs/telperion.org.ca-chain.cert.pem
TLS_KEY         /etc/ssl/private/fangorn.telperion.org.key.pem
TLS_CERT        /etc/ssl/certs/fangorn.telperion.org.cert.pem

Configuring the server is a little more involved because slapd uses its own directory as configuration. This means we need to use the ldapmodify command to update the config database. Create an ldif file called update-certs.ldif with the following contents:

dn: cn=config
changetype: modify
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/telperion.org.ca-chain.cert.pem
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/barad-dur.telperion.org.cert.pem
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/barad-dur.telperion.org.key.pem

Use ldapmodify to update the configuration:

root@barad-dur:~# ldapmodify -Y EXTERNAL -H ldapi:/// -f update-certs.ldif 
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "cn=config"

Verify that our addition “took”:

root@barad-dur:~# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config "(objectClass=olcGlobal)"
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: /var/run/slapd/slapd.args
olcLogLevel: none
olcPidFile: /var/run/slapd/slapd.pid
olcToolThreads: 1
olcTLSCACertificateFile: /etc/ssl/certs/telperion.org.ca-chain.cert.pem
olcTLSCertificateFile: /etc/ssl/certs/barad-dur.telperion.org.cert.pem
olcTLSCertificateKeyFile: /etc/ssl/private/barad-dur.telperion.org.key.pem

Now we can use the “startTLS” version of ldapsearch to send ldap results over a secure channel.
Using the following command (-ZZ option forces TLS), we don’t see any cleartext passwords:

root@fangorn:~# ldapsearch -D uid=lobelia,ou=people,dc=telperion,dc=org -LLL -H ldap://barad-dur.telperion.org -w  ihatebilbo -b uid=lobelia,ou=people,dc=telperion,dc=org -x -ZZ  userPassword
dn: uid=lobelia,ou=people,dc=telperion,dc=org
userPassword:: e1NTSEF9dmJ5N0Iwbk5IeEx1VXR0WjBXSWRUQVdrQWlXRlpsSzQ=
wireshark shows encrypted cleartext ldap traffic

Adding Users and Groups to the OpenLDAP directory

Now that we’ve installed the openLDAP server, it is possible to add data to it. Data in a directory is modelled as a tree (called a DIT for directory information tree). We’ve created the root of the tree when we installed and configured slapd. The root or top of the tree is the “base” entry and it has a distinguished name of “dc=telperion,dc=org”. We want to add a user branch (called “people”) and a group branch (called “groups”). Entries in the “people” branch of the DIT are leaves of the tree and contain the uid and other information about users. Entries in the “groups” branch are leaves and contain information about Unix groups.

We’ll create a user named “lobelia” who is a member of the group “telperionusers”. The tree looks like this:

user and group tree for a single user and a single group

If we were to add more users and another special group for privileged users, the tree may expand to look something like this:

feanor may be upset if we let melkor in the noldor user group

To create the user, we’ll create the tree in an LDIF file (we’ll name this one add-user.ldif). A unix user needs more than just a name. Even if Lobelia doesn’t get the keys to Bag End, she’ll at least need a home directory.

dn: ou=people,dc=telperion,dc=org
objectClass: organizationalUnit
ou: people

dn: ou=groups,dc=telperion,dc=org
objectClass: organizationalUnit
ou: groups

dn: cn=telperionusers,ou=groups,dc=telperion,dc=org
objectClass: posixGroup
cn: telperionusers
gidNumber: 5000

dn: uid=lobelia,ou=people,dc=telperion,dc=org
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: lobelia
sn: Sackville-Baggins
givenName: Lobelia
cn: Lobelia Sackville-Baggins
displayName: Lobelia
uidNumber: 10000
gidNumber: 5000
userPassword: lobelialdappassword
gecos: Lobelia Sackville-Baggins
loginShell: /bin/bash
homeDirectory: /home/lobelia

Use ldapadd command to add the user by passing the LDIF file.

root@barad-dur:~# ldapadd -x -D cn=admin,dc=telperion,dc=org -W -f add-user.ldif 
Enter LDAP Password: 
adding new entry "ou=people,dc=telperion,dc=org"

adding new entry "ou=groups,dc=telperion,dc=org"

adding new entry "cn=telperionusers,ou=groups,dc=telperion,dc=org"

adding new entry "uid=lobelia,ou=people,dc=telperion,dc=org"

Now that we’ve added a user, we can lookup that user by using ldapsearch. This ldapsearch command looks in the base tree “dc=telperion,dc-org” for an entry with a “uid” attribute equal to “lobelia”. It also displays the cn, the uidNumber, and the gidNumber.

root@barad-dur:~# ldapsearch -x -LLL -b dc=telperion,dc=org 'uid=lobelia' cn uidNumber gidNumber userPassword
dn: uid=lobelia,ou=people,dc=telperion,dc=org
cn: Lobelia Sackville-Baggins
uidNumber: 10000
gidNumber: 5000

The default access control list does not let a user other than lobelia access her password.  You can see that it was not displayed even though it was requested.  We can now authenticate as lobelia (either local or from another server) to see the password (stored as a hash)

dave@fangorn:~/ldapsetup$ ldapsearch -x -D uid=lobelia,ou=people,dc=telperion,dc=org -W -LLL -H ldap://barad-dur.telperion.org -b dc=telperion,dc=org uid=lobelia userPassword
Enter LDAP Password: <type 'lobelialdappassword'>
dn: uid=lobelia,ou=people,dc=telperion,dc=org
userPassword:: bG9iZWxpYWxkYXBwYXNzd29yZA==

We can see the default access control lists by querying the config file information on database access:

root@barad-dur:~# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b olcDatabase={1}mdb,cn=config olcAccess
dn: olcDatabase={1}mdb,cn=config
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by * none
olcAccess: {1}to attrs=shadowLastChange by self write by * read
olcAccess: {2}to * by * read

Access Control Lists (ACLs) are parsed and the first one found that matches the situation is applied. When we searched earlier for lobelia’s password, the userPassword attribute was found and because we were anonymously logging in, we were given the “none” access. When we logged in as lobelia and requested the password, lobelia matched as “self” and we were given read access to the userPassword attribute value (write implies read access). The second access control statement defines access to the shadowLastChange attribute. A user can update it (self) and all users can read it. The third access control statement permits read access to everything else.

Now that lobelia has access, she probably should change her password. We haven’t configured any front ends to handle this yet and we can use ldapmodify to update the password field but ldap utils provides the ldappasswd command explicitly for this purpose. After changing the password, we can verify that it changed by authenticating with it to perform an ldapsearch.

dave@fangorn:~$ ldappasswd -x -D uid=lobelia,ou=people,dc=telperion,dc=org -H ldap://barad-dur.telperion.org -w lobelialdappassword -a lobelialdappassword -s ihatebilbo
davd@fangorn:~$ ldapsearch -x -D uid=lobelia,ou=people,dc=telperion,dc=org -LLL -H ldap://barad-dur.telperion.org -w  ihatebilbo -b uid=lobelia,ou=people,dc=telperion,dc=org
dn: uid=lobelia,ou=people,dc=telperion,dc=org
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: lobelia
sn: Sackville-Baggins
givenName: Lobelia
cn: Lobelia Sackville-Baggins
displayName: Lobelia
uidNumber: 10000
gidNumber: 5000
gecos: Lobelia Sackville-Baggins
loginShell: /bin/bash
homeDirectory: /home/lobelia
userPassword:: e1NTSEF9dmJ5N0Iwbk5IeEx1VXR0WjBXSWRUQVdrQWlXRlpsSzQ=

We performed the password update from the fangorn client computer. Cool, right? NO. Not cool. All of that traffic was sent in cleartext over an open network. Eventually we can authenticate with SASL and kerberos but first, we can encrypt the transport layer with TLS to prevent little golems from stealing password information. I’ll cover that in another post.

Installing OpenLDAP on Debian Stretch

The goal is to install OpenLDAP with a baseline configuration.

The server is a Debian “Stretch” server. Validate that by running lsb_release.

root@barad-dur:~& lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 9.3 (stretch)
Release:	9.3
Codename:	stretch

First things first, install OpenLDAP daemon (slapd) and the appropriate LDAP utilities.
On the server, run (either as root or with sudo)
apt install slapd ldap-utils
You’ll be prompted to enter an administrative password. Choose one you’ll remember.
To be explicit, re-run the configuration engine with dpkg-reconfigure slapd.
You’ll be prompted to enter the DNS name of the domain a name for the organization. In my case, I chose “telperion.org” for both. This is important because the top of the DIT (the directory information tree) will be created using a distinguished name (dn) of “dc=telperion,dc=org”.

OpenLDAP uses itself for configuration (pretty cool, huh). This allows slapd to reconfigure on the fly without needing to be restarted. Installing and configuring slapd sets up two databases, one for data and one for configuration of slapd itself. Use the slapcat command to dump the contents of the databases.

According to the manpage, the slapd configuration database is always the first created, so to see the contents of the configuration, use the “-n0” option. Run slapcat -n0 to see the current configuration of slapd.

The second database contains information provided by the debian package configuration setup. Run slapd without any options to see the contents of the second database. Running slapcat -n1 would produce the same result (zero indexed for software engineers!). The domain name you typed in the dpkg-reconfigure step above, should be included as a distinguished name. In my case, it is

To see just the entries for each, use ldapsearch with the “dn” filter. There should be just two entries (as seen also by running slapcat | grep ^dn).

root@barad-dur:~# ldapsearch -x -LLL  -b dc=telperion,dc=org  dn
dn: dc=telperion,dc=org

dn: cn=admin,dc=telperion,dc=org

root@barad-dur:~# slapcat  | grep ^dn
dn: dc=telperion,dc=org
dn: cn=admin,dc=telperion,dc=org

ldapsearch can eventually be run from anywhere on the network with the appropriate configuration. Use slapcat to run locally. We can do the same for configuration. Set the base to cn=config to dump the entries for the configuration file. This search requires SASL authentication mechansim and to identify the localhost and protocol explicitly.

root@barad-dur:~# ldapsearch -Q -LLL -Y EXTERNAL -H ldapi:/// -b cn=config dn
dn: cn=config

dn: cn=module{0},cn=config

dn: cn=schema,cn=config

dn: cn={0}core,cn=schema,cn=config

dn: cn={1}cosine,cn=schema,cn=config

dn: cn={2}nis,cn=schema,cn=config

dn: cn={3}inetorgperson,cn=schema,cn=config

dn: olcBackend={0}mdb,cn=config

dn: olcDatabase={-1}frontend,cn=config

dn: olcDatabase={0}config,cn=config

dn: olcDatabase={1}mdb,cn=config

root@barad-dur:~# slapcat -n0 | grep ^dndn: cn=config
dn: cn=module{0},cn=config
dn: cn=schema,cn=config
dn: cn={0}core,cn=schema,cn=config
dn: cn={1}cosine,cn=schema,cn=config
dn: cn={2}nis,cn=schema,cn=config
dn: cn={3}inetorgperson,cn=schema,cn=config
dn: olcBackend={0}mdb,cn=config
dn: olcDatabase={-1}frontend,cn=config
dn: olcDatabase={0}config,cn=config
dn: olcDatabase={1}mdb,cn=config

By default, the inetorgperson and nis schema are included. This will help when we begin to populate the database as those schemas define the object classes we’ll use to build our directory.

wifi access point with hostapd (debian jessie)

When running a headless Debian MPD player with pulseaudio, RTP packets flood the wireless network with a lot of traffic.  I use a laptop as a remote multicast client to play music from the MPD server.  Unfortunately, my wife’s laptop (running Windows 7 Professional) barfs and loses its connection to the wireless network every time we play music.  The problem manifests when she uses her laptop as an MPD remote (Cantata).  On a relatively calm wifi connection (no audio playing) she can connect to MPD server and browse the music catalogue.  When she attempts to play a file the multicast flooding from pulseaudio kills her internet connection.

The solution: create a secondary wireless network bridge with hostapd and prevent multicast packets from killing the connection using ebtables.  This post is all about setting up the access point with hostapd.

This solution works because I’ve already got a DHCP server on the network and with a bridged network, the server would forward ethernet packets from the secondary wireless connection to the rest of the LAN. The DHCP server already on the LAN will provide any clients on this wireless network with an IP. It is possible (with dnsmasq and iptables) to use this setup just like your SOHO wireless router: https://seravo.fi/2014/create-wireless-access-point-hostapd

To create the wifi access point, I needed a cheap adapter.  With a little bit of research, I chose the relatively inexpensive TP-LINK TL-WN722N.  It uses an atheros AR9271 chipset which supports AP operation. On my Debian Jessie machine, I had to install the atheros firmware:
apt-get install firmware-atheros

To determine if your wifi adapter supports AP mode, run the following commands to install the iw package and get info on wireless hardware:
apt-get update
apt-get install iw
iw list

If AP is listed as one of the supported interface modes, you should be in good shape.

To bridge the wifi connection to the existing gigabit ethernet connection, we will need to add a bridge. First, we install network bridging utilities:
apt-get update
apt-get install bridge-utils

Second, update networking interface configuration to add a bridge on startup. Edit /etc/network/interfaces to make these changes:

# /etc/network/interfaces

# The primary network interface
allow-hotplug eth0
iface eth0 inet manual

# wifi access point
auto wlan0
iface wlan0 inet manual

# wifi to ethernet network bridge
# configured to get an IP from
# a DHCP server on the LAN
auto br0
iface br0 inet dhcp
bridge_ports eth0 wlan0

Restart networking to enable the changes with the systemctl command:
systemctl restart networking.service

Now we need to configure hostapd to act as an access point. To install hostapd, run the following commands:
apt-get update
apt-get install hostapd

I’ve configured my access point by creating a configuration file at /etc/hostapd/hostapd.conf. The config file doesn’t exist by default. You can inspect the sample config file (with decent documentation) by running this command:
catz /usr/share/doc/hostapd/examples/hostapd.conf.gz

Here’s a look at a working config file for a wireless-n access point.

# wifi interface to use
interface=wlan0
# your ssid
ssid=telperion

# use the nl80211 driver if you have centrino or atheros cards
driver=nl80211

# the ethernet bridge
bridge=br0

# even "n" modes use wireless g
hw_mode=g
country_code=US
ieee80211n=1
ieee80211d=1
channel=2 
# these options can be tuned based on what is in `iw list` 
ht_capab=[HT40+][SHORT-GI-40][DSSS_CCK-40][HT20][SHORT-GI-20] 
# cipher suite info for this connection.
# you should be using WPA2 
wpa=2
wpa_passphrase=MelkorSucks
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
auth_algs=1
macaddr_acl=0

After setting up the config file, we need to make sure that the systemd service script uses the config file. You need to point the DAEMON_CONF environment variable at /etc/hostapd.conf by adding this line to /etc/default/hostapd:

DAEMON_CONF="/etc/hostapd/hostapd.conf"

Once the config file, the service file, and the network bridge are appropriately configured, you can install the hostapd service so that it starts at boot with the following command:
systemctl enable hostapd.service