Thursday, April 24, 2008

Securing Subversion via LDAP -- A Followup

After some discussion of this on the Subversion mailing list we discovered that checking out the parent of a protected child will pull that child's contents to the local workspace. That probably isn't what you want so...

Let's say we have the repository paths .../parent and .../child and we want only a certain group of people to be able to checkout or update the child. In my initial pass at this I did the following:

<Location /svn/myRepo/parent>
Require valid-user
</Location>

<Location /svn/myRepo/parent/child>
Require group ...
</Location>

This will protect against the following:

svn ls http://myserver/svn/myRepo/parent/child
svn co http://myserver/svn/myRepo/parent/child

and commits against the child.

It will *not* protect against:

svn co http://myserver/svn/myRepo/parent

which will check out the child as it processes parent.

When we look in the apache logs we see a PROPFIND on the parent and some internal things but no mention of the child.

While digging through the mod_dav_svn source I discovered that, as it traverses the directory tree, it does an internal GET on each path to determine if the path is accessible. However, these GETs use the internal subversion URIs, not the ones that we naturally think of. So, to protect our child path, we can add another <Location/> tag thusly:

<Location ~ /svn/myRepo/!svn/ver/[0-9]+/parent/child>
Require group ...
</Location>

Note that this needs to come after:

<Location /svn/myRepo/!svn>
Require valid-user
</Location>

I wasted a couple of hours because I had that nugget tucked away in an included conf file following the one I was tweaking.

So, in order to protect the child the way we wanted to in the first place, our configuration would be:

<Location /svn/myRepo/parent>
Require valid-user
</Location>

<Location /svn/myRepo/parent/child>
Require group ...
</Location>

<Location ~ /svn/myRepo/!svn/ver/[0-9]+/parent/child>
Require group ...
</Location>

What's really nice about this is that a checkout of the parent silently ignores the now-protected child and populates the local workspace with everything that is unprotected.

I haven't tested it (yet) but it is reasonable to assume that the same technique will work with the more complicated <Limit/> and <LimitExcept/> tags.

Friday, April 18, 2008

Securing Subversion via LDAP

This started out as an email reply to the Subversion users mailing list but I decided to post it here instead since it got a little wordy. I can't believe it's been a year since I posted anything, time flies when you're falling off of a mountain bike...


So, in our environment, we're using the LDAP interface to Active Directory. All of our users are known there and we have defined groups to represent various bits of the repository. I'm using mod_auth_ldap in front of SVN to do the integration.


$ grep svn.conf httpd.conf
include conf/svn.conf

$ cat svn.conf

<IfModule mod_dav_svn.c>
<IfModule util_ldap.c>
<IfModule mod_auth_ldap.c>

LDAPSharedCacheSize 200000
LDAPCacheEntries 1024
LDAPCacheTTL 600
LDAPOpCacheEntries 1024
LDAPOpCacheTTL 600

<Location /ldap-status>
SetHandler ldap-status
Order deny,allow
Deny from all
Allow from .myco.com 172.18.
</Location>

<Location /svn>
DAV svn
SVNParentPath /usr/local/svn/repositories
SVNListParentPath on
SVNAutoVersioning on
AuthName "Enterprise Shared SCM Repository"
AuthType Basic
Require valid-user
AuthLDAPUrl ldap://myLdapHost/DC=foo,DC=myco, \
DC=com?SAMAccountName?sub?(objectCategory=person)
AuthLDAPBindDN "CN=foobar,DC=foo,DC=myco,DC=com"
AuthLDAPBindPassword ...

</Location>

include conf/svn/Authorization.conf

</IfModule>
</IfModule>
</IfModule>

So that sets up the basic wiring and in Authorization.conf I break it down into Subversion specifics:

$ cat svn/Authorization.conf

include conf/svn/root.conf
include conf/svn/sandbox.conf
include conf/svn/admin.conf
include conf/svn/BusinessUnit1.conf
include conf/svn/BusinessUnit2.conf
...
include conf/svn/BusinessUnit3.conf

We've taken the route of one large repository shared by our multiple business units. There are pros and cons of this approach (as there are with the repository-per-project approach). For various reasons we felt that a single repository would be most appropriate.

Defining the per-whatever rules below was a bit of trial and error. I put together a set of typical use cases for various paths and who should and should not access them. By running through those cases and watching the logs I was able to sort out the Limit tags necessary to secure things the way I needed.

Since the repository is shared across business units, the first thing is to make sure that only the admin can create top-level directories. this is done in the root.conf file included above:

$ cat svn/root.conf

# Any attempt to modify the myRepo repository requires
# membership in the admin group.
<Location /svn/myRepo>
<Limit MERGE MKCOL POST PUT DELETE PATCH PROPPATCH>
Require group CN=ACL-SVN-ADMIN,OU=SVN,DC=foo,DC=myco,DC=com
</Limit>
</Location>

# On many operations, Subversion does some behind the
# scenes work at .../!svn
<Location /svn/myRepo/!svn>
Require valid-user
</Location>

# Any paths below the root that are not otherwise secured
# only require a valid user for read/write access
<LocationMatch /svn/myRepo/.*>
Require valid-user
</LocationMatch>

In order to encourage people to use Subversion, I have setup a special "sandbox" are of the repository that is more or less a free-for-all. Given the above configuration, this config isn't entirely necessary but if we change the default policy (via the LocationMatch tag) from all-access to no-access our sandbox would break.

$ cat svn/sandbox.conf

<Location /svn/myRepo/sandbox>
Require valid-user
</Location>

Now I define business unit specific access. For each path we want to secure, the business unit tells me if they want the path to be read/write for all members of the group or if they want to have some people with read-only and others with read-write.

For instance, I keep the actual Subversion configuration in the repository. Because some of that may be "sensitive" (e.g. -- the LDAP bind password) I need to protect it.

$ cat svn/admin.conf

<Location /svn/myRepo/Admin/SVN>
Require group CN=ACL-SVN-ADMIN,OU=SVN,DC=foo,DC=myco,DC=com
</Location>

Thus, any access (read/write/merge/...) to /svn/myRepo/Admin/SVN requires membership in the administration group.

In some cases, we may have a project where it is acceptable for anybody with a valid username/password to view the files but only members of a particular group can edit them:

$ cat svn/BusinessUnit1/someProject1.conf

<Location /svn/myRepo/BusinessUnit1/SharedSource>
<Limit MERGE MKCOL POST PUT DELETE PATCH PROPPATCH>
Require group CN=ACL-SVN-ADMIN,OU=SVN,DC=foo,DC=myco,DC=com
Require group CN=ACL-SVN-BusinessUnit1-SharedSource-RW, \
OU=ACLs,DC=foo,DC=myco,DC=com
</Limit>
</Location>

The group that protects this branch is "CN=ACL-SVN-BusinessUnit1-SharedSource-RW,...". I've included the administration group also, however, so that the repository admins can help out if people get in a bind. This may or may not be something you want to do or you may have a different group (e.g. -- a help desk or support org) that would do this kind of thing:

<Location /svn/myRepo/BusinessUnit1/SharedSource>
<Limit MERGE MKCOL POST PUT DELETE PATCH PROPPATCH>
Require group CN=ACL-SVN-Support,OU=SVN,DC=foo,DC=myco,DC=com
Require group CN=ACL-SVN-BusinessUnit1-SharedSource-RW, \
OU=ACLs,DC=foo,DC=myco,DC=com
</Limit>
</Location>

In any case, you can see how you can list multiple 'Require group' directives for the path. These are OR'd together BTW.

There may be other paths that I do not want to be world-readable. In the simple case this is identical to my /svn/myRepo/Admin/SVN example. In the more complex case, however, you may have one group of people who need read-only and another who need read-write access.

$ cat svn/BusinessUnit2/someProject1.conf

<Location /svn/myRepo/BusinessUnit2/someProject1>
<LimitExcept MERGE MKCOL POST PUT DELETE PATCH PROPPATCH>
Require group CN=ACL-SVN-ADMIN,OU=SVN,DC=foo,DC=myco,DC=com
Require group CN=ACL-SVN-BusinessUnit2-someProject1-RO, \
OU=ACLs,DC=foo,DC=myco,DC=com
Require group CN=ACL-SVN-BusinessUnit2-someProject1-RW, \
OU=ACLs,DC=foo,DC=myco,DC=com
</LimitExcept >
<Limit MERGE MKCOL POST PUT DELETE PATCH PROPPATCH>
Require group CN=ACL-SVN-ADMIN,OU=SVN,DC=foo,DC=myco,DC=com
Require group CN=ACL-SVN-BusinessUnit2-someProject1-RW, \
OU=ACLs,DC=foo,DC=myco,DC=com
</Limit >
</Location>

By pairing the Limit and LimitExcept tags I can control who gets to make changes separately from who gets to view files.

One last thing I'll cover is protecting sub-paths within a parent path. Assuming that below /svn/myRepo/BusinessUnit2/someProject1 we have the traditional Subversion directories of "trunk", "tags" and "branches", it is very likely that your enterprise will want to secure the branches area to a specific group. In our case, we want to be sure that our branched code is only modifiable by a few experienced people who are allowed to make changes to the production candidate.

So, building on the /svn/myRepo/BusinessUnit2/someProject1 example, we further restrict the branches to this smaller group of people:

<Location ~ /svn/myRepo/BusinessUnit2/someProject1/[^/]+/branches>
<Limit MERGE MKCOL POST PUT DELETE PATCH PROPPATCH>
Require group CN=ACL-SVN-ADMIN,OU=SVN,DC=foo,DC=myco,DC=com
Require group CN=ACL-SVN-BusinessUnit2-someProject1-branches-RW, \
OU=ACLs,DC=foo,DC=myco,DC=com
</Limit >
</Location>

That's about all of the highlights I can think of to share. The most critical thing is to setup a test LDAP (I used OpenLDAP) and repository that is similar to your production environment. Then drill through as many usecases as you can think of to ensure that you're getting the behavior you expect. Once you're done, leave the test environment in place so that when somebody thinks something is broken you can go there and prove that it is user error :-)

My future goal is to define a simple XML format such that business unit (or project) owners can define their authorization requirements and submit them to an agreed-upon repository path. I would then have code to transform that XML into the Apache rules and drop them into place. In other words, I really don't intend to create and maintain all of the Apache configs by hand as we roll this out to dozens of projects.