Wednesday, December 29, 2010

Linux client authentication with LDAP, PAM, and NSS

As part of my OpenLDAP under Ubuntu Linux project, this post documents configuring Linux client authentication and authorization through LDAP, using Pluggable Authentication Modules (PAM) and Name Service Switch (NSS). As with my previous posts, this post was written against Ubuntu Linux's latest release, 10.10 ("Maverick Meerkat").

As Linux uses numeric IDs for users and groups (in separate domains), I highly recommend creating a numbering strategy to keep things organized. Per existing Debian policy:

  • 9 - 99: Reserved for local system / package accounts, and should be consistent across systems. (For example, 0 is "root".)
  • 100 - 999: Typically for dynamically allocated system accounts.
  • 1,000 - 59,999: Dynamically allocated user accounts.
    • (For the first user created on an Ubuntu system, the user ID and primary group ID are probably both 1,000.)
  • 65,000 - 65,533: Reserved for global allocation by the Debian project.
  • 65,534: The "nobody" user and "nogroup" group.

Before any attempts are considered to organize the available user IDs into blocks, according to the above list, this would leave only 59,000 valid IDs for actual users. This shouldn't be a problem for many organizations, but what if a larger organization needed all of their employees on a large network in the same domain? (There are quite a few companies with over 250,000 employees.) Apparently, the 16-bit limit is only a recommendation, with a current maximum limit of 32 bits. However, 32-bit systems may treat this as signed, allowing for a safe total of 31 bits, or just over 2 billion accounts. So I'm not going to worry about this any further for now, knowing that IDs 65,536 - 2,147,483,648 should be available for future use if necessary.

For now, I'm breaking down the above 1,000+ range as follows:

  • 1,000 - 1,999: Local accounts, not in LDAP.
  • 2,000 - 2,999: Administrative groups, in LDAP.
  • 3,000 - 3,999: Service accounts (users and groups), in LDAP.
  • 4,000 - 4,999: Reserved for future use.
  • 5,000 - 59,999: Actual users.

At a minimum, this offers one significant benefit. As LDAP is a network service, the unfortunate reality is that it will go down on occasion. If all the accounts were in LDAP, there would be no way to access the systems while LDAP is unavailable. Linux can be configured to look for accounts both locally, as well as in LDAP, which will be detailed below. There should be at least one account created locally in the 1,000 - 1,999 range for emergency use independent of LDAP.

Before making any configuration changes, review what is returned by the "getent passwd" and "getent group" commands. Nothing should be visible from LDAP yet, only locally-defined users and groups.

For simplicity, I'd like to install only the NSS support (provided by libnss-ldap) to start with, then review the outputs of getent before continuing with PAM. However, libnss-ldap includes libpam-ldap (for PAM) in its dependency hierarchy, so, I guess we'll just take everything together in one batch:

sudo apt-get install libpam-ldap libnss-ldap 

During configuration of ldap-auth-config, answer the following prompts:

  • LDAP server Uniform Resource Identifier: ldapi:/// (Default)
    • This assumes that the LDAP server is running on the same machine. Any valid LDAP URL can be used here.
    • For any serious usage, or any usage with real user accounts hosted on a seperate LDAP server, this needs to be properly configured to use ldaps:// (SSL/TLS) to protect user credentials between the client and the LDAP server.
    • See below for a current issue with using ldapi:///.
  • Distinguished name of the search base: dc=example,dc=com (Default uses "dc=net")
  • LDAP version to use: 3 (Default)
  • Make local root Database admin: No (Default is Yes)
  • Does the LDAP database require login? Yes (Default is No)
  • Unprivileged database user: cn=ldap.proxy,ou=serviceAccounts,dc=example,dc=com (Default is "cn=proxyuser,dc=example,dc=net")
  • Password for database login account:
  • Local crypt to use when changing passwords: exop (Default is md5)

This added /etc/ldap.conf, which did not exist prior to this configuration. (The closest base reference I can find is /usr/share/doc/libnss-ldap/examples/ldap.conf.gz). To re-run the prompted configuration (if necessary), run "dpkg-reconfigure ldap-auth-config".

Now NSS needs to be configured. Edit /etc/nsswitch.conf to use "files ldap" for the "passwd", "group", and "shadow" databases instead of the existing "compat." This can be done automatically by running "sudo auth-client-config -t nss -p lac_ldap". Re-run the getent commands, and combined results from both the local accounts as well as LDAP should now be visible.

It appears that simply by installing one of the above packages (probably either libpam-ldap or ldap-auth-config), PAM was also already configured with default LDAP extensions. Logins can be attempted by using "su <username>". Attempting to login as a user that does not exist either locally or in LDAP should return a "Unknown id: " error message. Otherwise, a "Password: " prompt should be displayed, and using a password that matches an account configured in LDAP should successfully change to that user. (Alternatively, use "sudo login" for a more comprehensive login and test.)

To see what was configured in PAM for LDAP, here is a diff comparing "/etc/pam.d" before and after this configuration, basically showing the additions using "pam_ldap.so":

diff -ur before/common-account after/common-account
--- before/common-account       2010-12-29 17:14:02.264282406 -0600
+++ after/common-account        2010-12-29 17:14:36.554657351 -0600
@@ -14,7 +14,8 @@
 #

 # here are the per-package modules (the "Primary" block)
-account        [success=1 new_authtok_reqd=done default=ignore]        pam_unix.so
+account        [success=2 new_authtok_reqd=done default=ignore]        pam_unix.so
+account        [success=1 default=ignore]      pam_ldap.so
 # here's the fallback if no module succeeds
 account        requisite                       pam_deny.so
 # prime the stack with a positive return value if there isn't one already;
diff -ur before/common-auth after/common-auth
--- before/common-auth  2010-12-29 17:14:02.264282406 -0600
+++ after/common-auth   2010-12-29 17:14:36.554657351 -0600
@@ -14,7 +14,8 @@
 # pam-auth-update(8) for details.

 # here are the per-package modules (the "Primary" block)
-auth   [success=1 default=ignore]      pam_unix.so nullok_secure
+auth   [success=2 default=ignore]      pam_unix.so nullok_secure
+auth   [success=1 default=ignore]      pam_ldap.so use_first_pass
 # here's the fallback if no module succeeds
 auth   requisite                       pam_deny.so
 # prime the stack with a positive return value if there isn't one already;
diff -ur before/common-password after/common-password
--- before/common-password      2010-12-29 17:14:02.264282406 -0600
+++ after/common-password       2010-12-29 17:14:36.554657351 -0600
@@ -22,7 +22,8 @@
 # pam-auth-update(8) for details.

 # here are the per-package modules (the "Primary" block)
-password       [success=1 default=ignore]      pam_unix.so obscure sha512
+password       [success=2 default=ignore]      pam_unix.so obscure sha512
+password       [success=1 user_unknown=ignore default=die]     pam_ldap.so use_authtok try_first_pass
 # here's the fallback if no module succeeds
 password       requisite                       pam_deny.so
 # prime the stack with a positive return value if there isn't one already;
diff -ur before/common-session after/common-session
--- before/common-session       2010-12-29 17:14:02.264282406 -0600
+++ after/common-session        2010-12-29 17:14:36.554657351 -0600
@@ -22,5 +22,6 @@
 session        required                        pam_permit.so
 # and here are more per-package modules (the "Additional" block)
 session        required        pam_unix.so
+session        optional                        pam_ldap.so
 session        optional                        pam_ck_connector.so nox11
 # end of pam-auth-update config
diff -ur before/common-session-noninteractive after/common-session-noninteractive
--- before/common-session-noninteractive        2010-12-29 17:14:02.264282406 -0600
+++ after/common-session-noninteractive 2010-12-29 17:14:36.554657351 -0600
@@ -22,4 +22,5 @@
 session        required                        pam_permit.so
 # and here are more per-package modules (the "Additional" block)
 session        required        pam_unix.so
+session        optional                        pam_ldap.so
 # end of pam-auth-update config

Here are some additional (manual) modifications, in addition to the above, and described below:

diff -ur after//common-account custom//common-account
--- after//common-account       2010-12-29 17:14:36.554657351 -0600
+++ custom//common-account      2010-12-29 17:17:22.626472292 -0600
@@ -14,8 +14,13 @@
 #

 # here are the per-package modules (the "Primary" block)
-account        [success=2 new_authtok_reqd=done default=ignore]        pam_unix.so
-account        [success=1 default=ignore]      pam_ldap.so
+#account       [success=2 new_authtok_reqd=done default=ignore]        pam_unix.so
+#account       [success=1 default=ignore]      pam_ldap.so
+
+account [success=1 default=ignore] pam_succeed_if.so uid < 2000 quiet
+account [success=2 user_unknown=ignore default=bad]     pam_ldap.so
+account [success=1 new_authtok_reqd=done default=ignore]  pam_unix.so
+
 # here's the fallback if no module succeeds
 account        requisite                       pam_deny.so
 # prime the stack with a positive return value if there isn't one already;
diff -ur after//common-password custom//common-password
--- after//common-password      2010-12-29 17:14:36.554657351 -0600
+++ custom//common-password     2010-12-29 17:17:22.626472292 -0600
@@ -22,8 +22,13 @@
 # pam-auth-update(8) for details.

 # here are the per-package modules (the "Primary" block)
-password       [success=2 default=ignore]      pam_unix.so obscure sha512
-password       [success=1 user_unknown=ignore default=die]     pam_ldap.so use_authtok try_first_pass
+#password      [success=2 default=ignore]      pam_unix.so obscure sha512
+#password      [success=1 user_unknown=ignore default=die]     pam_ldap.so use_authtok try_first_pass
+
+password [success=1 default=ignore] pam_succeed_if.so uid < 2000 quiet
+password [success=2 user_unknown=ignore default=die] pam_ldap.so try_first_pass
+password [success=1 default=ignore] pam_unix.so obscure sha512
+
 # here's the fallback if no module succeeds
 password       requisite                       pam_deny.so
 # prime the stack with a positive return value if there isn't one already;
diff -ur after//common-session custom//common-session
--- after//common-session       2010-12-29 17:14:36.554657351 -0600
+++ custom//common-session      2010-12-29 17:17:22.626472292 -0600
@@ -22,6 +22,7 @@
 session        required                        pam_permit.so
 # and here are more per-package modules (the "Additional" block)
 session        required        pam_unix.so
+session required       pam_mkhomedir.so
 session        optional                        pam_ldap.so
 session        optional                        pam_ck_connector.so nox11
 # end of pam-auth-update config

As mentioned above, and partially inspired by http://wiki.debian.org/LDAP/PAM, the edits in common-account and common-password configure LDAP to not be used for accounts with a UID below 2,000. However, the above approach seems a little cleaner, as pam_succeed_if is used first as the conditional, and then channels the request to either pam_ldap or pam_unix as appropriate, without having to ignore "user_known" return statuses from LDAP for local users. I've tested this and can confirms that it also successfully allows local logins if the LDAP server is unavailable. (I haven't tested the other configuration, but it looks like if LDAP can't return "user_unknown", it will default to a "bad" result, causing local logins to also fail.) The final edit in common-sesson simply adds pam_mkhomedir.so to create home directories at login as necessary.

Two other references that I found helpful were "LDAPClientAuthentication" in the Ubuntu Community Documentation, and Brian White's "LDAP Authentication" LinuxWiki.

Now that everything should be mostly working, there are a few further enhancements and fixes that can be made:

First, it may not be desirable for everyone in LDAP to have access to every LDAP-enabled client machine. /etc/ldap.conf provides several options for limiting which accounts can be used from the current machine. Options include "pam_filter", "pam_check_host_attr", "pam_groupdn", "pam_min_uid", and "pam_max_uid". I find "pam_groupdn" to be the most effective and useful, especially after setting "pam_member_attribute" to "memberUid" to allow reusing posixGroup's.

Next (as mentioned above), is an issue with using ldapi:///. This is really only applicable (possible) when running the LDAP server on the same machine as a client. Basically, "connection_read(): no connection!" warnings are logged when using ldapi:///, which are resolved simply by switching over to ldap:///. This actually appears to be an issue with the libldap client library provided by OpenLDAP itself (2.4.21), and not the slapd daemon. This was reported in full detail as OpenLDAP ITS # 6548. Even after being confirmed on the same ticket with a follow-up by Gerald Turner, the ticket was promptly closed without proper justification, as far as I can understand. This is now open as Ubuntu bug # 594840, where it is marked as confirmed and affecting at least 7 people as of this writing.

Finally, Ubuntu bug # 297408 may prove beneficial for some setups. From Edward Murrell's original bug description:

Binary package hint: libpam-modules

The pam_group module allows assigning groups based on group membership. This is handy, if for example you want to automagically enable various rights on a specific server dependent on a users membership in a given netgroup. The following line in /etc/security/group.conf makes a user part of the admin group on the machine if they are part of the net group developers.

*;*;@developers;Al0000-2400;admin

Unfortunately, this only works for NIS net groups. This isn't much help if your network groups are held in LDAP, which are recognized as normal NSS groups.

Patch is attached which fixes this. NSS groups are recognized by '%'. Ie;

*;*;%someldapgroup;Al0000-2400;admin

Reportedly, this functionality has been included in PAM 1.1.2. Thanks to Edward for his involvement in providing a solution for this! (As of this writing, Ubuntu Maverick is still using version 1.1.1.)

Tuesday, December 28, 2010

LDAP authentication for Apache HTTP Server

As part of my OpenLDAP under Ubuntu Linux project, this post documents configuring the Apache HTTPD Server to use LDAP for authentication and authorization. The Apache HTTPD Server will simply be referred to as "Apache" for the remainder of this post. As with my previous post on phpLDAPadmin, this post was written against Apache 2.2.16, and Ubuntu Linux's latest release, 10.10 ("Maverick Meerkat").

Before configuring Apache to accept credentials, ensure that sensitive pages and the prompts for credentials will only be accepted on SSL/TLS-protected sites, otherwise users' credentials will be passed over the network in clear-text where they may easily be compromised. (It doesn't make much sense to use SSL/TLS encryption between Apache and the LDAP server, if the communication between the web browser and the web server isn't protected.) Documentation for using SSL/TLS encryption for the Apache web server is readily available elsewhere (e.g. the "HTTPS Configuration" section under "HTTPD" in the Ubuntu Server Guide), and is beyond the scope of this post.

At least under Ubuntu Maverick, Apache is installed with the mod_authnz_ldap module, but it is not enabled by default. To enable, use "sudo a2enmod authnz_ldap", or manually create the symbolic links in "/etc/apache2/mods-enabled". (a2enmod is a Debian-specific script shared with Ubuntu.)

Next, add a <Location> directive into the Apache configuration that contains configuration directives as documented in mod_authnz_ldap. Assuming that only one LDAP directory will be used for the Apache instance, I just place most of the configuration under a root Location, like this:

<Location "/">
    AuthBasicProvider ldap
    AuthType Basic
    AuthName "example.com"
    AuthzLDAPAuthoritative off
    AuthLDAPURL "ldaps://ldap.example.com/dc=example,dc=com?uid?sub"
    AuthLDAPBindDN "cn=ldap.proxy,ou=serviceAccounts,dc=example,dc=com"
    AuthLDAPBindPassword "secret123"
</Location>

I need the bind distinguished name (DN) and password, as I have OpenLDAP configured to not allow anonymous binds, which is what Apache 2.2 defaults to if these are not specified. I don't particularly like the need for the proxy account, however, for security and maintenance reasons. I would think that ideally, Apache could just use the username and password of the authenticating user to perform the bind. If the user's bind fails, then access should be denied anyway. However, this would require Apache to know how to convert a username (e.g. "mark") into a DN (e.g. "cn=mark,ou=people,dc=example,dc=com") that is acceptable for the bind. As configured above, Apache currently determines this with a query performed using the specified bind credentials, searching for an LDAP entry where the uid (user identifier) attribute matches the user's entered username. Interestingly, it appears that "binding as the user" will be available in Apache 2.3 (currently in alpha), as seen in the 2.3 documents for AuthLDAPInitialBindAsUser and AuthLDAPInitialBindPattern. However, while sometimes beneficial, this method of translating the username is limited to string matching and substitution only (using regular expressions) as LDAP access is not yet available, so continuing to use a configured bind DN and password will still be more flexible for several scenarios.

Securing the LDAP queries with SSL/TLS is necessary to protect the credentials being passed over the network, as accomplished with the "ldaps://" AuthLDAPURL above. For testing or troubleshooting during initial setup, it can be temporarily replaced with simply "ldap://" to use unencrypted connections. Alternatively, if the LDAP server is operating on the same host as Apache, "ldapi:///" can be used (with the hostname ommitted) to use *NIX sockets, bypassing the IP layer entirely. Interestingly, while I can't find any documentation to support this (only Apache Bug # 44302 which reports that it wasn't working at least as of Apache 2.2.8 on 2008-01-27), "ldapi:///" is working for me as of this writing.

For SSL communications to be successful, the LDAP server's SSL certificate must be trusted by the calling server. This is currently accomplished by providing one or more trusted cerver certificates, and setting LDAPTrustedGlobalCert in the Apache configuration. If multiple certificates must be supported, they can simply be concatenated into a single file when using the Base64 / PEM (CA_BASE64) certificate format. Currently, this must be configured globally per Apache instance, not per directory or location. Alternatively, this can be set globally per server through the TLS_CACERT option in /etc/ldap/ldap.conf. Setting "TLS_REQCERT never" should also work, but this is not recommended for security reasons, nor does it appear to work currently.

Now, just add require directives as appropriate. Available options for LDAP include ldap-user, ldap-group, ldap-dn, ldap-attribute, and ldap-filter. ldap-user should be avoided for reasons of maintainability and security best practices. One of these directives may be placed at the same root <Location> block as above, or in child locations. (Per the Apache Configuration Sections documentation, <Directory> directives should be used over <Location> directives for access control of filesystem-based resources, as improper configurations may otherwise easily result in circumventions.) Either Location or Directory directives will inherit the above-configured Auth* directives from a parent.

For example, to protect phpLDAPadmin as previously configured:

  <Location "/phpldapadmin">
    Options +ExecCGI
    AddHandler fcgid-script .php
    FCGIWrapper /usr/lib/cgi-bin/php5 .php

    require ldap-group cn=ldap-admins,ou=groups,dc=example,dc=com
  </Location>

Finally, reload the Apache configuration ("sudo /etc/init.d/apache2 reload"), and test away!

Monday, December 27, 2010

LDAP web administration with phpLDAPadmin

As part of my OpenLDAP under Ubuntu Linux project, I wanted to find a good web administration tool for the directory. The best option I found was phpLDAPadmin (Wikipedia), a.k.a. PLA.

I meant to complete several other LDAP-related posts since I started this project in April, but other priorities took precedence. However, a side benefit is that my previous configurations have now been successfully repeated and tested under Ubuntu Linux's latest release, 10.10 ("Maverick Meerkat").

Apache Configuration

As phpLDAPadmin is a web application, it depends on a web server that can serve PHPs. My choice is the Apache HTTPD Server (Wikipedia) version 2.2, which will simply be referred to as "Apache" for the remainder of this post.

phpLDAPadmin is available in the Ubuntu package repositories. However, the default configuration (as packaged for Ubuntu) could use some improvement, so I'd recommend against installing it from the repository. I already have an Apache instance running for some other uses, which uses Apache's default worker MPM. However, Ubuntu's packaged version depends on libapache2-mod-php5, which depends on apache2-mpm-prefork, which reconfigures Apache to use the prefork MPM. The differences between these Multi-Processing Modules is documented by Apache in Apache Performance Tuning, and a few other references that I stumbled upon:

Now granted, my uses of Apache and/or LDAP aren't currently anywhere near demanding enough for a significant difference in performance between the MPMs. However, this is a good exercise, and I can be confident that this setup will be able to scale with fewer issues in the future.

My existing Apache installation consisted of the "apache2" Ubuntu (meta)package. To support PLA, the "php5-cgi" and "php5-ldap" packages also need to be installed. (Do not install the base "php5" package, as this also has the same dependencies listed above, and will reconfigure Apache to use the prefork MPM.)

The primary reason for libapache2-mod-php5 depending on the prefork MPM is due to thread-safety issues. As such, a suitable way is necessary to run PHP under Apache in a thread-safe fashion. Standard CGI would be one option, but is terribly inefficient. FastCGI is an option that provides for isolation between PHP (and/or other modules and languages) and Apache, while avoiding the inefficiencies of CGI. Support for FastCGI in Apache was originally available through mod_fastcgi, but is discouraged due to various stability and license compatibility issues. mod_fcgid is the preferred alternative, provides binary compatibility, is reportedly faster, and is available as the libapache2-mod-fcgid package in Ubuntu.

To summarize the above paragraph, also install libapache2-mod-fcgid. The module should be automatically enabled. If not, use "sudo a2enmod fcgid", or manually create the symbolic links in "/etc/apache2/mods-enabled". (a2enmod is a Debian-specific script shared with Ubuntu.)

Download the latest "phpldapadmin-php5" package from http://sourceforge.net/projects/phpldapadmin/files/phpldapadmin-php5/. As of this post, I am using version 1.2.0.5. I extracted the download into "/var/www/phpldapadmin" (the default DocumentRoot). Then add a <Location> directive into the Apache configuration, utilizing fcgid:

  <Location "/phpldapadmin">
    Options +ExecCGI
    AddHandler fcgid-script .php
    FCGIWrapper /usr/lib/cgi-bin/php5 .php
  </Location>

As phpLDAPadmin deals with credentials (including passwords for both administrative login, as well as user accounts), proper measures should be taken to secure this site. It should only be accessible over SSL/TLS (HTTPS). I've also configured Apache to require authorization to access PLA at all - including its login page (detailed later).

phpLDAPadmin Configuration

phpLDAPadmin provides an example configuration in config/config.php.example. Copy it to config/config.php, then customize it from there. If you are running the LDAP server (e.g. OpenLDAP) on the same host, no changes should be required, but setting the server "friendly" name is probably desired at a minimum. Details are available at Installation, Config, Config.php, and LDAP server definitions in the PLA Wiki.

At this point, phpLDAPadmin should be mostly usable. However, as of version 1.2.0.5 of PLA, there are a few quirks that I found and addressed:

  1. One unloadable DN causes others to not expand in the tree, due to a buggy early return from the draw function in lib/HTMLTree.php that skips over draw_javascript() from being called, and ajax_tree.js and layersmenu-browser_detection.js not being loaded into the browser. I reported this in bug # 2997938, and also provided a patch (attached to the bug report). As of 2010-12-27, there has been no acknowledgement or other activity taken by the PLA developers on this bug, so anyone also affected will need to manually apply the patch.

  2. When using phpLDAPadmin over HTTPS (as recommended above), all page resources are properly loaded over HTTPS except the SourceForge logo in the page footer. This causes security warnings in the browser due to "partial encryption". I reported this in bug # 2997703, along with a work-around of using the "remoteurls" option that is mentioned but otherwise undocumented at http://phpldapadmin.sourceforge.net/wiki/index.php/Config.php, as well as a code fix to properly use proper relative URLs. As of 2010-11-06, a fix has been committed for the next (but not yet available) PLA release.
  3. With every attempt to edit an entry, there is a nuisance "Select a template to edit the entry" page, that prompts for a template to use (either "Default", "Generic: Address Book Entry", or "Generic: Posix Group"). This prompt is repeated for repeat edits, with no visible option to save a default template. After reading this forum post on gentoo.org, and extending upon some of the suggestions offered for other issues in the PLA FAQ, I renamed each of the 2 .xml files under "templates/modification" to add a non-.xml suffix, essentially disabling the modification templates. (Simply renaming or removing the directory seemed like it should be a better option, but causes other errors.)
  4. Similar to the above, and more closely covered by the FAQ, even after removing the "modification" templates, there are still "Automatically removed objectClass from template" warnings displayed when first editing an entry in a PLA session. Renaming "mozillaOrgPerson.xml", "courierMailAccount.xml", and "courierMailAlias.xml" under the "templates/creation" folder seems to have removed all of these warnings, at least for my configured LDAP schema. (I'm not sure why the "creation" templates are even being referenced during modification.)

Alternatives

I had looked through a few alternatives before choosing phpLDAPadmin. At least while limiting the search to free and open-source solutions, I really didn't find anything that I think can compare to PLA. However, one cool, but non-web-based alternative I did find is shelldap, which allows for browsing and manipulating an LDAP directory using familiar shell commands like cat, cd, cp, ls, touch, mkdir, move, and vi.