SVNserver

From SELinux Wiki

Jump to: navigation, search

How I built a SELinux based server that holds the SVN repos of all our projects. Same thought patterns can be applied to securing any other sharing technology, not just SVN. The benefits are that we have a solid separation of project data that is enforced by the kernel; breach in any system except the SSH server or the kernel will not yield unauthorized data access (even with UID 0).

Contents

[edit] Requirements

  1. SVN should be confined to its own domain
  2. Access to SVN should be provided via SSH using public key authentication
  3. SVN data should be labeled by own type with only SVN having access to them
  4. Administration of the repos should be restricted only to the system administrator (sysadm_r)
  5. Various SVN repos should be restricted only to certain people (ie. the project members)
  6. Within this restriction, some people should be granted read-only access
  7. Regular backups!

[edit] The policy module

I based the server on Debian 5.0, therefore I was dealing with quite an old release of refpolicy 2:0.0.20080702-16 (even for the launch time of the distro). Much water has passed since then so some things might need adjusting for newer refpolicies (I'll indicate those I know about).

The SVN module is pretty straightforward once you match the requirements to known macros. I'll start with the interface because it makes a line to follow.

interface(`svn_domtrans',`                     
       gen_require(`                          
               type svn_t, svn_exec_t, $1;    
               role $2;                       
       ')                                     
       domtrans_pattern($1,svn_exec_t,svn_t)
       role $2 types svn_t;                 
')                                           
interface(`svnadmin_domtrans',`                     
       gen_require(`                               
               type svnadmin_t, svnadmin_exec_t, $1;
               role $2;                             
       ')                                           
       domtrans_pattern($1,svnadmin_exec_t,svnadmin_t)
       role $2 types svnadmin_t;
')

The first two macros are the classical 'allow-$1 to transition to another type and add that type to their $2 role'.

interface(`svn_manage_data',`
       gen_require(`
               type svndata_t, $1;
               class file { manage_file_perms };
               class dir { manage_dir_perms };
       ')
       allow $1 svndata_t : file { manage_file_perms };
       allow $1 svndata_t : dir { manage_dir_perms };
')

This macro will grant the $1 type access to manipulate our precious SVN repos.

The .fc file will also be relatively straightforward:

/usr/bin/svnserve               gen_context(system_u:object_r:svn_exec_t,s0)
/usr/bin/svnadmin               gen_context(system_u:object_r:svnadmin_exec_t,s0)
/data/svn(/.*)?                 gen_context(system_u:object_r:svndata_t,s0)

Finally, the actual policy looks like this:

policy_module(svn,1.0.0)      
require {
       type sshd_t;
       type initrc_t;
       type home_root_t;
       class fifo_file { read write };
       class process { sigchld };     
       class dir { search_dir_perms };
}                                      
type svn_t;
type svn_exec_t;
type svnadmin_t;
type svnadmin_exec_t;
type svndata_t;
files_type(svndata_t);

We start with requirements and declarations of the types we already have seen. Some say that a require block is an atrocity, but not always is everything available through macros.

type svnuser_home_dir_t;
type svnuser_home_ssh_t;
type svnuser_home_t;    
files_type(svnuser_home_dir_t);
files_type(svnuser_home_ssh_t);
files_type(svnuser_home_t);    

Following that are the types to support the public key authentication for SSH. That is /home/user (home_dir_t) and /home/user/.ssh (home_ssh_t). The home_t is supposed to be for other files inside the /home/user but that option is currently unused.

userdom_base_user_template(svnuser)
corecmd_search_bin(svnuser_t)      
kernel_dontaudit_read_system_state(svnuser_t)
allow svnuser_t svnuser_home_dir_t : dir { search_dir_perms };
allow svnuser_t home_root_t : dir { search_dir_perms };

This block creates the svnuser_u, svnuser_r and svnuser_t with minimal privileges to act as an user that can execute his shell. Remember that SSH will drop you to a shell from which you can (or are forced to) run the svnserve program. (Making svnserve the shell is a no-go because it takes the '-t' parameter ;) The two allow rules are also to support this scheme, because SSH will try to chdir() the user before running his shell.

Note that under red hat based policy the userdom_base_user_template() macro will add a pesky permission allowing the user to execute any program in /bin, /usr/bin, etc. which makes the setup somewhat less secure. If need be you can always roll out your own user template.

svnadmin_domtrans(sysadm_t, sysadm_r)
svn_domtrans(svnuser_t, svnuser_r)   

Next we call the two macros from our .if file to allow the respective users run svn and svnadmin under our domains. Running svnserve without transitioning to svn_t won't help the svn user much because it won't have access to the repo files.

application_domain(svn_t, svn_exec_t)
libs_use_ld_so(svn_t)                
libs_use_shared_libs(svn_t)          
term_use_all_terms(svn_t)            
miscfiles_read_localization(svn_t)   
files_read_etc_files(svn_t)          
domain_use_interactive_fds(svn_t)    
nscd_socket_use(svn_t)               
ssh_sigchld(svn_t) 
allow svn_t sshd_t : fifo_file { read write getattr };

type svn_tmp_t;
files_tmp_file(svn_tmp_t)
manage_dirs_pattern(svn_t, svn_tmp_t, svn_tmp_t)
manage_files_pattern(svn_t, svn_tmp_t, svn_tmp_t)
files_tmp_filetrans(svn_t, svn_tmp_t, { file dir })

svn_manage_data(svn_t)

Now it's finally time to say what svn_t can do. The first three macros are there to make the program even run. On newer refpolicies it might be that they already are integrated into application_domain(). Then I allow the program to use nscd (as I use it), to read non-sensitive stuff from /etc, to read the .po files from localization. The domain_use_interactive_fds and term_use_all_terms support operations where SSH allocates a tty for the user, while the allow fifo_file supports the opposite, ie. non-interactive invocation. The ssh_sigchld makes the SSH daemon know when our session has finished.

The tmp_t block allows svnserve to create and use some own stuff in /tmp with its own type svn_tmp_t.

And the call to our svn_manage_data macro is the 'gross' of the policy, ie. we want svnserve to access the repos ;)

application_domain(svnadmin_t, svnadmin_exec_t)
libs_use_ld_so(svnadmin_t)                     
libs_use_shared_libs(svnadmin_t)
term_use_all_terms(svnadmin_t)
miscfiles_read_localization(svnadmin_t)
files_read_etc_files(svnadmin_t)
domain_use_interactive_fds(svnadmin_t)
  
svn_manage_data(svnadmin_t)

Finally, I give some rights to the svnadmin command. The system administrator (sysadm_r) will have rights to mess the repository files directly, but will require a relabel of the messed up files afterwards. Plus this can lay foundation for having a dedicated svn administrator role.

Having this, it's time to create the repos, relabel files, and test!

[edit] Separation of users and repos

In theory this step should be pretty straightforward using MCS. I gave labels to various repos and users, such as

/data/svn/squeek(/.+)?                             all files          system_u:object_r:svndata_t:s0:c0

... and the same label to people who should access them.

The pitfall is with new files labels. Suppose the user has access to two repos, and thus his high level is ie. s0:c0,c4. His 'svn commit' to a c0 repo will result in wrongly leveled file, depending on his low level. If the user has the label s0-s0:c0,c4 his 'svn commit' will create all the new files with s0 (too low), if he has the label s0:c0,c4-s0:c0,c4 he will create files with s0:c0,c4 (too high). If his level is s0:c0-s0:c0,c4 he will create correct s0:c0 files, but also in the c4 repo.

This can be solved in three ways:

  1. Add support to svnserve so that it creates new files with the label of the directory they're contained in
  2. Add support to selinux to do the same
  3. Relabel the files periodically

Of course the least painful way is the #3, so that's the route I've taken. A 5-minute cron job that runs "restorecon -R /data/svn" does the trick.

Note that this problem is relevant only to creating files, not reading them. (But if a new file has incorrect label, the repository will either leak or break.)

The alternate way to do this is to ditch MCS and do it all using only the types. So you will create a monster apparatus to roll out and manage repo1_data_t, repo2_data_t, ... , user1_t, user2_t, ... user1_svn_t, user2_svn_t, ... and the appropriate transition rules and allow matrices. But this is ugly, right?

[edit] Read-only rights

Implementation of this requirement can be again placed in various parts of the system:

  1. SVN has some kind of access management, but this is fragile and requires duplication of the user list. SVN can of course be recoded to support selinux, etc, but poosh.
  2. Create svn_read_only_u, _r, _t. This requires people to have 2 accounts - for RO repos and RW repos.
  3. If we went the TE-only way in the previous step, we can enlarge our apparatus to somehow support this scheme.
  4. If we went the MCS way, we can do it but it requires some fiddling with MCS.

The route I took was to fiddle with MCS. Files can generally hold a 'low' and 'high' level, for example s0:c0-s0:c0,c1. Normally only the 'high' level matters, but who says it has to be that way. I used the low level for read only access and the high level for read/write access. This requires changing the constrains in the 'mcs' file of the policy source and recompiling the policy. Usually that step isn't that hard, as distributions provide the source package of their policy. Just unzip it, change the files, rebuild and replace the base.pp module.

I provided a patch for debian 5.0's policy. It probably won't apply to a newer refpolicy, but hey, it's just 5 lines, you should be able to figure it out.

Under this scheme, the label for repo files will be s0:RO-s0:RO,RW where RO and RW are the designated categories for RO and RW access. People with RO access will be given just the RO category, people with RW will be given both RO and RW categories.

[edit] Backups

Backups of this stuff are relatively straightforward. You can either 'pull' or 'push' the backups. For the 'pull' way you need an user that is able to read all the repos and do so from the backup machine. If the user's key leaks, things can get nasty. Therefore, 'push' is the preferred way with backups.

I have a simple cron job under the root (sysadm_r) account. (Possibly you can create your own backup user and give him the rights.) The core of the job is

exec 2>"$TOP/log/$TIME"

set -o pipefail  

tar cvvRpsf - --one-file-system /data/svn /etc /var /opt /home /root |\
       gzip -3c |\
       gpg --home "$TOP/gpg" --encrypt -r Backup \
               --compress-algo none |\
       ssh -i "$TOP/ssh/backupkey" "$BACKUPDEST"

That is, grab all the files, tar 'em, zip 'em, encrypt 'em (so a compromise of the backup host gives them nothing) and send 'em. (On the other side, there's a "cat > backupfile" waiting.)

[edit] Perfecting up

[edit] Named categories

Having fiddled with the policy base module, it's also possible to get rid of the numbered categories (like c0, c1,...) and use real names like squeekRO, squeekRW, and all that without msctrans daemon (which is a bit flaky).

The change is again in the "mcs" file of the refpolicy. The c0-c1023 can be either removed altogether, or the named categories can be sneaked in between them. I changed the gen_cats(mcs_num_cats) call to

gen_cats(decr(mcs_num_cats))
category squeekRO;
category squeekRW;
category c`'decr(mcs_num_cats);

This defines the named categories just before the last numbered one (eg. c1023). The benefit is that "c0.c1023" still means "all categories", although a scheme like

category first
... category ...
category last

would also make sense, in which case one would use "first.last" to mean "all categories".

[edit] Using seusers

In the original schema I used just the "svnuser_u" seuser with access to all categories and semanaged logins to give each person access to his project. The alternative is to use one seuser for each person. There are two advantages in this:

  1. All files created (ie. svn revisions) will bear the label with the user part of the person who did the change. This can be cross-referenced with actual svn history to discover wicked stuff (the seuser part is more authoritative).
  2. The person's seuser will be assigned just the categories he needs access to, versus a general-purpose seuser and individual logins trimmed to the categories. The difference is that the seuser category assignment is enforced by SELinux, while the other is "enforced" by SSH. If you remove a category from a seuser while he's still downloading the repository, his process will be instantly killed, whereas if you do this at the semanage login level, his process will continue, and the changes will be reflected only for each new login.

The downside is that my "restorecon -R" cron job would reset the file contexts to system_u. A more tender approach is needed here (as to just restore the MCS categories), for example like this:

for dir in /data/svn/*; do
       level="`ls -Zd $dir | sed 's/[^ ]*:\(s0[^ ]*\).*/\1/'`"
       chcon -R -l "$level" "$dir"
done
Personal tools