It’s not enough to authenticate a user. You need to also check their authorization to see if that person should be allowed access. With Apache at work I use mod_auth_kerb for authentication and it works well. Both IE and Firefox will send the user’s domain credentials via GSSAPI once configured correctly.* The next step is to setup mod_authnz_ldap so we can check user account information. Probably the most common authorization check people might use is a group membership check. You probably have content which you only want your administrators to have access to. The combination of mod_auth_kerb and mod_authnz_ldap does not work directly out of the box. Additionally, Active Directory throws another wrench into the problem.
Setup for mod_auth_kerb is simple enough. With the system joined to AD already you can easily use Samba to create your HTTP SPN using the following command:
# net ads keytab add HTTP
Additionally you should use ktutil to extract only the keys for the HTTP/machine.example.com principle to a separate keytab readable by the apache process. Once that is done the following in your Apache configuration will have kerberos authentication working:
<Location /private>
AuthType Kerberos
AuthName "EXAMPLE Domain Login"
KrbMethodNegotiate On
KrbMethodK5Passwd On
KrbAuthRealms EXAMPLE.COM
Krb5KeyTab /etc/httpd/conf/keytab
require valid-user
</Location>
This location isn’t really private since every authenticated user has access to this content. I want to restrict this content to our Domain Admins group in AD. This is where mod_authnz_ldap comes in. Once the user is authenticated I want to check their group membership. Now the config block has been expanded with an authorization check to check the user’s group membership.
<Location /private>
AuthType Kerberos
AuthName "EXAMPLE Domain Login"
KrbMethodNegotiate On
KrbMethodK5Passwd On
KrbAuthRealms EXAMPLE.COM
Krb5KeyTab /etc/httpd/conf/keytab
AuthLDAPURL "ldap://dc1.example.com dc2.example.com/dc=example,dc=com?sAMAccountName"
AuthLDAPBindDN cn=nss_ldap,ou=services,dc=example,dc=com
AuthLDAPBindPassword ********
Require ldap-group cn=Domain Admins,ou=Groups,dc=example,dc=com
</Location>
What I’ve done above is in AuthLDAPURL first given two domain controllers to search for user information in case one is down. (Remember the quotes if you specify multiple DCs.) I’ve then specified that mod_authzn_ldap should perform searches from the domain root. And finally, I want it to search for the entity with the sAMAccountName equal to the username provided by kerberos. With AD you need to use sAMAccountName and not uid since uid is only available if you’ve extended the AD schema for POSIX info and entered it on the account. sAMAccountName is your guaranteed unique username. The AuthLDAPBindDN and AuthLDAPBindPassword lines are the DN and password of a user in AD with read only access to certain parts of the directory tree to get user information. In my case it’s the same user I use for nss_ldap. Finally I specify the DN of the group that the user is required to be a member of. This will still not work though and you’ll see the following in your error log:
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1432): [client 172.30.20.2] kerb_authenticate_user entered with user (NULL) and auth_type Kerberos
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1432): [client 172.30.20.2] kerb_authenticate_user entered with user (NULL) and auth_type Kerberos
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1147): [client 172.30.20.2] Acquiring creds for [email protected]
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1266): [client 172.30.20.2] Verifying client data using KRB5 GSS-API
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1282): [client 172.30.20.2] Verification returned code 0
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1300): [client 172.30.20.2] GSS-API token of length 163 bytes will be sent back
[Thu Jul 15 09:33:49 2010] [debug] src/mod_auth_kerb.c(1348): [client 172.30.20.2] set cached name [email protected] for connection
[Thu Jul 15 09:33:49 2010] [debug] mod_authnz_ldap.c(683): [client 172.30.20.2] ldap authorize: Creating LDAP req structure
[Thu Jul 15 09:33:52 2010] [debug] mod_authnz_ldap.c(695): [client 172.30.20.2] auth_ldap authorise: User DN not found, ldap_search_ext_s() for user failed
This is because the kerberos supplied principle is in the form of <username>@EXAMPLE.COM while their sAMAccountName is simply <username>. There is no attribute created by default in AD with the user’s full kerberos principle name. There are two ways of mangling the kerberos principle to work in an LDAP search. One is a patch to mod_auth_kerb which adds the “KrbStripDomain” directive to remove the user’s realm and pass only the username to mod_authnz_ldap. The other is mod_map_name which lives in the mod_auth_kerb CVS here:
http://modauthkerb.cvs.sourceforge.net/viewvc/modauthkerb/mod_map_user/
I don’t like the idea of patching distro packages so I chose the latter option. It’s not available as a tar ball from the project’s site so you need to retrieve it from CVS, autoconf it, build, and install it. (I’ve emailed the author asking if he would create an official release. Waiting to hear back.) With that module now installed and loaded a simple line to mangle the user’s kerberos principle into the user’s sAMAccountName is added.
<Location /private>
AuthType Kerberos
AuthName "EXAMPLE Domain Login"
KrbMethodNegotiate On
KrbMethodK5Passwd On
KrbAuthRealms EXAMPLE.COM
Krb5KeyTab /etc/httpd/conf/keytab
# Strip the kerberos realm from the principle.
MapUsernameRule (.*)@(.*) "$1"
AuthLDAPURL "ldap://dc1.example.com dc2.example.com/dc=example,dc=com?sAMAccountName"
AuthLDAPBindDN cn=nss_ldap,ou=services,dc=example,dc=com
AuthLDAPBindPassword ********
Require ldap-group cn=Domain Admins,ou=Groups,dc=example,dc=com
</Location>
The mod_map_user documentation gives other creative examples of how to use it which can be combined with the AuthLDAPURL but I found this to be the simplest and fit my needs. You could probably tune MapUsernameRule and AuthLDAPURL to place less load on your AD controller if you wanted/needed to.
AD however appears to throw a wrench into mod_authnz_ldap while trying to search for an entity with a sAMAccountName value of the transformed username. The error log indicates the kerberos realm was stripped but mod_authnz_ldap still had problems finding a match.
[Thu Jul 15 11:48:49 2010] [info] [client 172.30.19.45] Applying pattern '^(.*)@(.*)$' to user '[email protected]', mech:'Any'
[Thu Jul 15 11:48:49 2010] [info] [client 172.30.19.45] Pattern matched
[Thu Jul 15 11:48:49 2010] [notice] [client 172.30.19.45] User name '[email protected]' rewritten to 'TMCLAUGHLIN'
[Thu Jul 15 11:48:49 2010] [debug] mod_authnz_ldap.c(683): [client 172.30.19.45] ldap authorize: Creating LDAP req structure
[Thu Jul 15 11:48:52 2010] [debug] mod_authnz_ldap.c(695): [client 172.30.19.45] auth_ldap authorise: User DN not found, ldap_search_ext_s() for user failed
After scratching my head for a bit I resorted to a packet trace between the web server and DC. What I found was a search for “(&(objectClass=*)(sAMAccountName=TMCLAUGHLIN))” which yielded a result. But, since the result was performed at the root of the directory the DC also returned referrals which mod_authnz_ldap attempted to search and fail in doing so. I’d assume this should work but I had to workaround it. The solution was to make the search path where all our users are. This could be a problem depending on your tree layout however.
<Location /private>
AuthType Kerberos
AuthName "EXAMPLE Domain Login"
KrbMethodNegotiate On
KrbMethodK5Passwd On
KrbAuthRealms EXAMPLE.COM
Krb5KeyTab /etc/httpd/conf/keytab
# Strip the kerberos realm from the principle.
MapUsernameRule (.*)@(.*) "$1"
AuthLDAPURL "ldap://dc1.example.com dc2.example.com/cn=users,dc=example,dc=com?sAMAccountName"
AuthLDAPBindDN cn=nss_ldap,ou=services,dc=example,dc=com
AuthLDAPBindPassword ********
Require ldap-group cn=Domain Admins,ou=Groups,dc=example,dc=com
</Location>
With all this in place I’m now able to successfully authenticate access to content and authorize them based on the AD group membership. One import note though. If it’s really that important to restrict access to content then SSLRequireSSL should be added. I simply left it out for debugging and setup purposes.
* For Firefox in about:config you can add “*.example.com” to “network.negotiate-auth.trusted-uris”. For IE the default settings for the Intranet Zone is to send credentials automatically. However, if you use an FQDN it assumes the host is part of the Internet Zone and you need to add “http://*.example.com” to the list of sites in the Intranet Zone.