Tom's FreeBSD blog

July 21, 2010

Finding a user’s primary group in AD

Filed under: Active Directory — tmclaugh @ 1:30 pm

Active Directory is at its heart LDAP and Kerberos on steroids.  For what I’m concerned with in regard to *nix hosts that’s it.  On the host it’s nss_ldap for user info and MIT or Heimdal kerberos for authentication like it would be in a pure *nix environment.  The only need for Samaba is for the simplicity of adding hosts to AD and managing the kerberos keytab file.  The net command fills the role of kadmin.  All that could even be done using a Windows hosts and transferring a keytab file to a *nix host to eliminate Samba if you really wanted to do the extra work.  While working with Apache’s mod_authnz_ldap and and mod_authz_svn I’ve run into an idiosyncrasy with user primary groups which is a result of how AD stores that information differently from a traditional OpenLDAP setup.

AD stores user group info with both the group and the user.  On the group, user DNs are stored in the member attribute.  Using Python:

>>> import ldap, pprint
>>> l = ldap.initialize('ldap://example.com')
>>> l.simple_bind_s('[email protected]', '********')
>>> pprint.pprint(l.search_s('ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'cn=Domain Admins', ['member']))

[('CN=Domain Admins,OU=Groups,DC=example,DC=com',
 {'member': ['CN=TMCLAUGHLIN,CN=Users,DC=example,DC=com',...]})]

On the user, supplemental group DNs are stored in the memberOf attribute while their primary group in the primaryGroupID attribute with the group’s Windows RID value.

>>> pprint.pprint(l.search_s('cn=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'sAMAccountName=tmclaughlin', ['memberOf', 'primaryGroupID']))
[('CN=TMCLAUGHLIN,CN=Users,DC=example,DC=com',
 {'memberOf': ['CN=Radius Admins,OU=Groups,DC=example,DC=com',
   'CN=Domain Server Admin,OU=Groups,DC=example,DC=com',
   'CN=Employee,OU=Groups,DC=example,DC=com',
   'CN=Schema Admins,OU=Groups,DC=example,DC=com',
   'CN=Domain Admins,OU=Groups,DC=example,DC=com',
   'CN=Enterprise Admins,OU=Groups,DC=example,DC=com'],
  'primaryGroupID': ['513']}
)]

It’s easy enough to search and find a user’s supplemental groups but their primary group is a little harder.  The RID is the last component of the group’s SID.  (See Windows SID Structure for further explanation of SIDs and their components.)  The RID is not stored as a separate attribute of the group but is contained in the group’s objectSid attribute which stores the group’s SID in a binary encoded form.

>>> pprint.pprint(l.search_s('ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'cn=Domain Admins', ['objectSid']))

[('CN=Domain Admins,OU=Groups,DC=example,DC=com',
 {'objectSid': ['\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x007~;e\xa0\x07\xe4I\x18IF\x17\x00\x02\x00\x00']})]

In order to find the user’s primary group you need to first determine the domain SID, then convert it to a string, and finally search for the <domain SID>-<group RID> value.  I wasn’t sure how to convert the SID value stored in AD to a string but with some help from web2ldap I was able to see how to do it:

def sid2str(self,sid):
 srl = ord(sid[0])
 number_sub_id = ord(sid[1])
 iav = struct.unpack('!Q','\x00\x00'+sid[2:8])[0]
 sub_ids = [
 struct.unpack('<I',sid[8+4*i:12+4*i])[0]
 for i in range(number_sub_id)
 ]
 return 'S-%d-%d-%s' % (
 srl,
 iav,
 '-'.join([str(s) for s in sub_ids]),
 )

The result is the following steps:

# Get RID of primary group
>>> pri_grp_rid = l.search_s('cn=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'sAMAccountName=tmclaughlin', ['primaryGroupID'])[0][1]['primaryGroupID'][0]
# Get domain SID
>>> domain_sid = l.search_s('dc=example,dc=com', ldap.SCOPE_BASE)[0][1]['objectSid'][0]
# Convert domain SID to string form
>>> domain_sid_s = sid2str(domain_sid)
# Search for group with <domain SID>-<group RID> objectSid value
>>> pprint.pprint(l.search_s('ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'objectSid=%s-%s' % (domain_sid_s, pri_grp_rid), ['cn']))

[('CN=Domain Users,OU=Groups,DC=example,DC=com',
 {'cn': ['Domain Users']})]

July 20, 2010

mod_authz_svn and AD group synchronization

Filed under: Active Directory,Apache — tmclaugh @ 2:29 pm

I have a semi-private SVN repository served up over Apache at my job where I keep all my work.  It’s semi-private in that I want my group at to have read and write access.  Employees to have read access in case they find anything interesting.  And finally non-Employee domain accounts should have no access.  Down the road I may want some more flexibility in this setup.  Possibly restricting all access to parts of it to just Domain Admins while giving write access to certain other people or groups in the company to certain projects.  Rather than creating separate repos with their own separate Apache configs I decided on path based authorization.

There is a downside to this approach and it is how to use our AD groups to control access.  By creating separate repos I could use mod_authnz_ldap to authorize access based on our AD groups.  I’ve found mention of patches to mod_authz_svn to add LDAP functionality but no patches to try out.  With path based authorization I needed a way to keep the groups in the authz file in sync with our AD groups.  I ended up settling on a script I found here and run it as a cron job.

Using LDAP Groups With Subversion’s Authz File

[root@corptech ~]# ./sync_ldap_groups_to_svn_authz.py -d cn=nss_ldap,ou=services,dc=example,dc=com \
-p <password> -l ldap://example.com -b ou=groups,dc=example,dc=com -i sAMAccountName \
-z /srv/svn/authz.conf

The script works well but has a few issues.  I’ve merged some of the patches in the comments section of that post to his script.  These include the fix for the “Invalid cross-device link” error and the patch to handle paged searches.  I left out some others since they weren’t useful to me and the time to merge unformated patches (the comments section strips white spaces) wasn’t worth it.  I’ve also added support for handling ranged attributes.  A Windows 2k3 DC will only return 1500 values of a multi-valued attribute at a single time. This is not to be confused with the page size limit which in Windows 2k was 1000 values and the same as the default page size limit.  This is a hard limit and cannot be changed.

Python 2.6.2 (r262:71605, Apr 14 2009, 22:40:02) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import ldap, pprint
>>> l = ldap.initialize('ldap://example.com')
>>> l.simple_bind_s('[email protected]', '*********')
(97, [])
>>> r = l.search_s('ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'cn=Employee')
>>> pprint.pprint(r)
[('CN=Employee,OU=Groups,DC=example,DC=com',
 {'cn': ['Employee'],
 'dSCorePropagationData': ['20100412182218.0Z',
 '20100412181952.0Z',
 '20091218151750.0Z',
 '20091216205638.0Z',
 '16010714223649.0Z'],
 'description': ['Example Company's Employees'],
 'distinguishedName': ['CN=Employee,OU=Groups,DC=example,DC=com'],
 'groupType': ['-2147483646'],
 'instanceType': ['4'],
 'member': [],
 'member;range=0-1499': ['CN=TMCLAUGHLIN,CN=Users,DC=example,DC=com',
....

We have over 3000 members of our Employee group so this became a problem.  The patch linked at the end of this post includes the two previously mentioned patches along with the ability to handle large groups.  One thing the patch does not handle is the way AD handles primary group membership.  A user is not listed in the member attribute of their primary group and the group is not listed in the memberOf attribute of the user.

>>> r = l.search_s('cn=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, 'cn=tmclaughlin', ['memberOf', 'primaryGroupID'])
>>> pprint.pprint(r)
[('CN=TMCLAUGHLIN,CN=Users,DC=example,DC=com',
 {'memberOf': ['CN=Employee,OU=Groups,DC=example,DC=com',
 'CN=Radius Admins,OU=Groups,DC=example,DC=com',
 'CN=Domain Server Admin,OU=Groups,DC=example,DC=com',
 'CN=Schema Admins,OU=Groups,DC=example,DC=com',
 'CN=Domain Admins,OU=Groups,DC=example,DC=com',
 'CN=Enterprise Admins,OU=Groups,DC=example,DC=com'],
 'primaryGroupID': ['513']})]

The primaryGroupID attribute refers to the last part of the primary group’s SID.  In this case it’s Domain Users.  Unfortunately I’m not sure how to properly convert the objectSid attribute of a group from its binary form to a string in order to attempt a match.  For now I have no way to handle syncing primary groups.  I’m open to suggestions though.

sync_ldap_groups_to_svn_authz.py.diff

July 15, 2010

mod_auth_kerb + AD and LDAP authorization

Filed under: Active Directory,Apache — tmclaugh @ 12:34 pm

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.

July 14, 2010

mod_auth_kerb + Windows AD and “Server not found in Kerberos database”

Filed under: Active Directory — tmclaugh @ 3:13 pm

At work we’re a predominantly Windows shop but I have a machine which had CentOS 4 loaded on it which I used for various *nix related experimentation.  I’m our AD admin so I’ve used it a bit for kerberos and LDAP interoperability testing.  AD is pretty much krb5 and LDAP on steroids and it’s not hard to get a *nix machine to authenticate against and pull user info from it.  I set this box up a few years ago and recently decided to do a fresh install and move it to CentOS 5.  This fresh install however had some kerberos issues due to some things I forgot I did years back.

When I was first toying with kerberos on this box and authenticating against AD I used some older instructions which avoided Samba.

http://grolmsnet.de/kerbtut/

This worked just fine but step 6 caused me headaches after I installed CentOS 5 and decided to use Samba to join my machine to AD and use it to manage the kerberos keytab on it.  The instructions call for creating a dummy account and using ktpass.exe on a Windows box to generate a keytab file and a corresponding HTTP SPN.  I created an AD user called http_corptech which had the SPN HTTP/[email protected] attached to it.  Once I had the keytab in place and mod_auth_kerb setup this worked fairly well.

When I installed CentOS 5 on the machine I decided to use Samba for managing domain membership since it had advanced over the years and now had the ability to manage the kerberos keytab.  After /etc/krb5.conf was setup the following two commads took care of my domain membership:

# net ads join -U tmclaughlin
# net ads keytab create

A few days ago I went to setup mod_auth_kerb again.  I used the same config I’ve always used and currently have working on other machines and used the following command to create my HTTP SPN

# net ads keytab add HTTP

However, I received 401 errors when trying to access protected content.  I decided to crank Apache’s LogLevel up to “debug” and watched error_log.  The following was what I saw:

[Tue Jul 13 23:54:27 2010] [debug] src/mod_auth_kerb.c(1432): [client 172.30.19.45] kerb_authenticate_user entered with user (NULL) and auth_type Kerberos
[Tue Jul 13 23:54:27 2010] [debug] src/mod_auth_kerb.c(1432): [client 172.30.19.45] kerb_authenticate_user entered with user (NULL) and auth_type Kerberos
[Tue Jul 13 23:54:27 2010] [debug] src/mod_auth_kerb.c(1147): [client 172.30.19.45] Acquiring creds for [email protected]
[Tue Jul 13 23:54:27 2010] [debug] src/mod_auth_kerb.c(1266): [client 172.30.19.45] Verifying client data using KRB5 GSS-API
[Tue Jul 13 23:54:27 2010] [debug] src/mod_auth_kerb.c(1282): [client 172.30.19.45] Verification returned code 589824
[Tue Jul 13 23:54:27 2010] [debug] src/mod_auth_kerb.c(1309): [client 172.30.19.45] Warning: received token seems to be NTLM, which isn't supported by the Kerberos module. Check your IE configuration.
[Tue Jul 13 23:54:27 2010] [error] [client 172.30.19.45] gss_accept_sec_context() failed: Invalid token was supplied (No error)

I saw for some reason with tcpdump that both Firefox and IE were attempting NTLM negotiation instead of GSSAPI when attempting to access this content.  I spun my wheels with this error for a bit.  Eventually I enabled KrbMethodK5Passwd for mod_auth_kerb which I had disabled previously to make sure tickets were working correctly.  If my ticket was being rejected there was already something wrong and dealing with a password prompt was just a waste of time.  When I did this I found the following in error_log.

[Tue Jul 13 23:55:21 2010] [debug] src/mod_auth_kerb.c(1432): [client 172.30.19.45] kerb_authenticate_user entered with user (NULL) and auth_type Kerberos
[Tue Jul 13 23:55:21 2010] [debug] src/mod_auth_kerb.c(915): [client 172.30.19.45] Using HTTP/[email protected] as server principal for password verification
[Tue Jul 13 23:55:21 2010] [debug] src/mod_auth_kerb.c(655): [client 172.30.19.45] Trying to get TGT for user [email protected]
[Tue Jul 13 23:55:21 2010] [debug] src/mod_auth_kerb.c(569): [client 172.30.19.45] Trying to verify authenticity of KDC using principal HTTP/[email protected]
[Tue Jul 13 23:55:21 2010] [debug] src/mod_auth_kerb.c(584): [client 172.30.19.45] krb5_get_credentials() failed when verifying KDC
[Tue Jul 13 23:55:21 2010] [error] [client 172.30.19.45] failed to verify krb5 credentials: Server not found in Kerberos database
[Tue Jul 13 23:55:21 2010] [debug] src/mod_auth_kerb.c(994): [client 172.30.19.45] kerb_authenticate_user_krb5pwd ret=401 user=(NULL) authtype=(NULL)

I could see now that there was a problem with the kerberos setup somewhere.  I had already created a new keytab file and even removed and rejoined the machine to AD.  Finally it struck me.  The “Server not found in Kerberos database” error message was not entirely accurate.  It’s not that the SPN wasn’t found in AD…  It was that multiple objects in AD had that SPN.  Both the computer account I created with Samba and the old dummy user I had created had a servicePrincipalName attribute with the SPN HTTP/corptech.example.com.  Once I deleted the dummy account from AD and it propagated kerberos authentication worked just fine.  Anyone who has used the older instructions I used before and now plans on using Samba to manage the kerberos keytab will probably find this info helpful.  The kerberos error about the server not being found will probably lead you off in the wrong direction.

p.s. I had a similar issue with ksu before on the machine when I first installed CentOS 5.  I had created a host_corptech user so I could get tickets originally and was receiving the same error.

Powered by WordPress