Yvan Rodrigues, Graphics, December 21,
2005
One of the benefits of administering both IIS and Apache web servers is getting to understand how each does things well.
Apache HTTP Server is very flexible. Its modular design empowers its administrator to start with a lean server and build it to do almost anything.
Microsoft Internet Information Services (IIS) is built into Windows Server and sets up a working website in minutes. Complexities like virtual hosting become a breeze to configure in the Windows GUI.
In a current project, my team is migrating a web server from an overburdened Windows 2000 server to a purpose-built LAMP server. One of the bumps along the road is converting a web site that relies on the HTTP authentication mechanism that is built into IIS. This slick mechanism automatically prompts an
HTTP basic authentication request when the ACL permissions on the underlying file are insufficient to meet a GET request for the anonymous (IUSR_SERVER) user.
This means that if a webmaster wants to restrict access to a page to a group of users, s/he merely needs to make the users members of a security group (typically in the Active Directory of which the server is a member) and assign read permissions for that security group to the file in question. This is an easy way of limiting access to a page or webspace to an existing group of users. For example, we have a security group in AD that has all the staff in our department as members. In this case we wish to limit access to the internal departmental website to our staff.
Not surprisingly, with a little more work and research, we are able to accomplish the same thing in Apache, without any changes to Active Directory or the established security groups.
In this how-to article I will demonstrate how to configure Apache to authenticate a user against
Active Directory and how to authorize them to view pages based on their membership in security groups.
Some parts of this article demonstrate how to apply the strategies demonstrated at the University of Waterloo. I will demarcate these in orange.
Pre-requisites
Application
Access control consists of two parts,
authentication and
authorization. In the authentication stage we ask, "Is this person really who they say they are?" Once that is answered, authorization answers the question, "What are they allowed to access?"
Specifically for this project we wish to accomplish the following steps:
- Request that the user's browser prompt for a userid and password
- Establish a connection to the Active Directory (LDAP) server
- Search the server for the userid that has been provided and authenticate the user using the password provided by the user
- Ask the server if the user is a member of a specified security group
- Allow or deny access to a web page based on the results of the above.
We accomplish all of this by using
Apache directives. Apache directives are configuration commands that can be used either in a configuration file (e.g. httpd.conf) or in content directories in the form of .htaccess files.
In this example I will be using an .htaccess file. In order for this to work, the
AllowOverride AuthConfig (or AllowOverride All) must be set in httpd.conf. If this is not set, the .htaccess file and the directives it contains will be ignored. The .htaccess file should be in the upper-most directory in the webspace for which you wish it to be effective.
My .htaccess file looks like this, and an explanation of its content follows. I will be referring to this file throughout the article:
| Code: |
# Authentication realm and method:
AuthType Basic
AuthName "Graphics Staff Intranet"
# DN of Active Directory server
AuthLDAPUrl ldap://ads.uwaterloo.ca/DC=ads,DC=uwaterloo,DC=ca?sAMAccountName?sub?(objectClass=*)
# An account in the AD that has enough permissions to perform an LDAP search
AuthLDAPBindDN "CN=Technical Support Graphics (graphics),OU=Users,OU=Graphics,OU=Academic Support,DC=ads,DC=uwaterloo,DC=ca"
AuthLDAPBindPassword abc123pass
# The following would also be valid, although not truly LDAP compliant
#AuthLDAPBindDN ADS\\yrodrigu
#AuthLDAPBindDN yrodrigu@ads.uwaterloo.ca
# When checking for group membership, use the DN of the user, not the HTTP entry
AuthLDAPGroupAttributeIsDN on
# Require groups, specifying the DN of the security group
require group CN=gfx-Graphics,OU=Security Groups,OU=Graphics,OU=Academic Support,DC=ads,DC=uwaterloo,DC=ca
|
In this example my goal is to restrict the viewing of the pages of the Graphics department to the staff of my department. Each of these staff has a user account in the ADS Active Directory and is a member of the gfx-Graphics security group.
The AuthType directive specifies that HTTP basic authentication shall take place. I have linked to an article that explains what this is, but essentially most HTTP 1.0 and later browsers can be asked to prompt the user for userid and password, both of which will be sent back to the httpd server in plaintext. Important: Yes, I said plaintext. Particularly when sensitive passwords such as UWDir credentials are being requested, this must only be performed on SSL (https) connections. I'll repeat that. Do not use HTTP basic authentication on un-encrypted connections.
When the httpd server requests HTTP basic authentication, it provides a realm which lets the user know to what, or how they are authenticating. This realm is specified by the AuthName directive.
The AuthLDAPUrl specifies against which server we are authenticating the user's input as well as to which LDAP attribute we are comparing their userid.
That's a lot to digest, and we're getting ahead of ourselves.
First, Active Directory is nothing more than Microsoft's implementation of an LDAP server. Once we accept that, we demystify most of what is happening here. That being said, if you have never seen or used the LDAP protocol before, it can look weird and overwhelming. You can address this in two ways.
1. Read the Wikipedia article on LDAP to get an understanding of its conventions. Pay particular attention to the term Distinguished Names (DNs). I won't spend time here explaining DNs, other than to say they are a way of absolutely referencing an element in a hierarchal structure, the basis of the LDAP protocol.
2. Get an easy-to-use GUI LDAP browser (client) such as Microsoft's Active Directory Users and Computers MMC snap-in or Softerra's freeware LDAP Browser. Don't forget to drop them an email to thank them for making such a useful tool available to us for free. Being able to navigate and browse the Active Directory in LDAP terms is the key to success in making the methods discussed here work. The base DN when connecting to UW's ADS server, ads.uwaterloo.ca is DC=ads,DC=uwaterloo,DC=ca.
Figure 1: Viewing the hierarchal nature of Active Directory using Softerra's LDAP Browser
The syntax for the AuthLDAPUrl is ldap://host:port/basedn?attribute?scope?filter and each element is described in detail in the module's documentation.
Taking a look at my implementation: The LDAP server is ads.uwaterloo.ca. The base DN (the uppermost node in the hierarchy) is DC=ads,DC=uwaterloo,DC=ca.
The attribute element of the directive is very important because this is for what the LDAP server will be searching against the userid that the client specifies in the browser. In this case I have specified sAMAccountName. This is important because by default the module would use uid for which Active Directory returns the Name attribute when queried for uid. If you use your LDAP browser to note the attributes for a user, and their corresponding values you will notice that the Name attribute for a user is in the format Yvan Rodrigues (yrodrigu). Therefore, in order for the end-user to authenticate with their AD credentials they would need to literally type their name and userid in this format in order for the LDAP search to be successful. Again, browsing the attributes and corresponding values for the same user you can note that the only attribute that returns the value in the format for which we are looking is sAMAccountName (see figure). Unfortunately this creates an inconvenience later...
The remaining part of the AuthLDAPURL directive specifies that we want to perform a multi-level/deep search without any further filtering. In short, find this person.
In order to perform searches in an LDAP server one must first connect to it. This may or may not require authentication in itself. In the case of UW, anonymous/guest connections are not permitted. This is reasonable since a guest could download contact information for the entire population of the university and use it for spamming or other bad stuff. Therefore we need to authenticate with the server before being able to perform the search that is the whole point of this exercise.
The designers of the mod_auth_ldap module thought of this and designed it to automatically attempt to authenticate with the server using the userid and password provided by the HTTP basic authentication request. This is great but it presents a problem. When it attempts authentication with the server, it presents the userid verbatim for the LDAP server to search and find a matching uid. As we noted earlier, Active Directory stores the uid in the format FirstName Initial LastName (UserId). This is a problem for us, because although the user could authenticate with the LDAP server by typing in their name in that format, it is unrealistic to expect them to do so, especially with the de facto standard of eight characters or less at UW.
For this reason we simply cannot use the built-in mechanism to authenticate in establishing the connection to the LDAP server. That is a pain, because it means we need to statically provide credentials for connection establishment, i.e. we need a userid and password for an account with sufficient privileges to establish a connection, but insufficient enough that if it were compromised (by someone looking at httpd.conf or .htaccess) they could do any meaningful harm. Do not use your personal UWDir credentials here. IST does not have a general service account for this purpose. You may wish to request a UWDir userid for this specific purpose.
We provide these credentials using the AuthLDAPBindDN and AuthLDAPBindPassword directives. As you will notice, the userid needs to be in the form of a DN. Forgot how this works? Use your GUI tool to find the object and retrieve its DN. It can be put in double quotes which is necessary if it contains any spaces or illegal characters.
At this point we have taken user input from the browser, established a connection with the LDAP server and (a) found the record for the target userid (b) not found a matching record. Since userids must be unique across an entire Active Directory forest, it should be impossible to find more than one record.
Side note: if we just wanted to check that the user is a valid member of the AD, we would simply add a require valid-user directive at this point and we would be done.
Now we get to why we are here: checking for membership in a security group. We will be using the require group directive for that but first we use the AuthLDAPGroupAttributeIsDN on. This specifically addresses the fact that when checking for group membership we are comparing the DN of the search result, not the raw input from the user.
At this stage, controlling access is as simple as using the require group directive with the DN of the security group as the parameter. Note that this must not be in double quotes, even if it contains spaces. Don't ask me why. The require group directive implicitly checks the security group's member attributes for a value that contains the DN of our user.
By using your LDAP browser to examine the attributes and values of objects in Active Directory you can come up with even more creative ways to control access. For example using other directives that are part of mod_auth_ldap you could limit access based on department name, email address, or anything else.
Conclusion
With mod_auth_ldap installed, Apache has all the functionality necessary to control access to resources on the httpd server based on criteria in the Active Directory server such as security group membership.
I was unable to find a resource on the internet to explain how to accomplish this. Hopefully this article proves useful to you.
Yvan Rodrigues, Graphics, December 21,
2005
Addendum
A few things have changed in Apache 2.2 that affect this article.
- The mod_auth_ldap module has been renamed to mod_authnz_ldap
- Your config file or .htaccess needs to add the lines AuthBasicProvider ldap after the AuthType Basic line
- If mod_authn_file is enabled you may need to add AuthUserFile /dev/null
- require group and require user directives must be changed to require ldap-group and require ldap-user respectively.
Therefore, the configuration discussed above might look like:
# Authentication realm and method:
AuthType Basic
AuthBasicProvider ldap
AuthName "Graphics Staff Intranet"
# DN of Active Directory server
AuthLDAPUrl ldap://ads.uwaterloo.ca/DC=ads,DC=uwaterloo,DC=ca?sAMAccountName?sub?(objectClass=*)
# An account in the AD that has enough permissions to perform an LDAP search
AuthLDAPBindDN "CN=Technical Support Graphics (graphics),OU=Users,OU=Graphics,OU=Academic Support,DC=ads,DC=uwaterloo,DC=ca"
AuthLDAPBindPassword abc123pass
# The following would also be valid, although not truly LDAP compliant
#AuthLDAPBindDN ADS\\yrodrigu
#AuthLDAPBindDN yrodrigu@ads.uwaterloo.ca
# When checking for group membership, use the DN of the user, not the HTTP entry
AuthLDAPGroupAttributeIsDN on
# Require groups, specifying the DN of the security group
require ldap-group CN=gfx-Graphics,OU=Security Groups,OU=Graphics,OU=Academic Support,DC=ads,DC=uwaterloo,DC=ca
Yvan Rodrigues http://www.yvanrodrigues.com
March 7, 2007