svnserve-sgidtunnel - secure wrapper for svnserve

Motivation: increased security when providing access to subversion repositories through svn+ssh://. Read up on subversion in case you have no idea what it is. The bottom line: it is a version control system that supports multiple access protocols:

Multiple users can access the server via ssh. Each of them has a named linux account, no "shared" accounts. Typically, there is no need or reason to provide them full shell access, it is ok if they can refresh their web pages and copy files around. For this purpose, scp/sftp access is well enough for them, so thez get an rssh shell. Developers essentially access the subversion repositories through ssh, so they do not need to manage multiple credentials or complex configurations.

What are the typical issues in such an environment?

When using svn+ssh, an svnserve process is spawned on the server side of the ssh tunnel after successful ssh authentication - on behalf of the authenticated user; the svn client running on the other end of the tunnel communicates through standard input/output with this temporal process. The process is destroyed when the connection is closed. The advantage in this approach is that there is not need to maintain an additional open port and no svnserve daemon is running at times when nobody accesses the repository. On the other hand this approach dictates that the svnserve process spawned with the users' permission needs to have read/write access to certain parts of the repository.

The lack of read/write permissions

When accessing the repo over file:// or svn+ssh:// protocols, the user needs to have read access to the whole repository and also be able to create files during commit: part of these files are temporal files, locks, while other files make up the revisions and store meta data of them. Practically, file permissions are to be set up the way that users can not alter repositry configuration files and hooks. It is considered a best practice to put the developers into a common group and make the repo owned by this group (chown -R :svn *). When following this practice it is enough to just set permissions for the group, isn't it?

Umask problems, group sticky bit

By default, any file created by a process will have the owner and the group assigned based on the user running the process and the primary group of that user; file permissions are set based on the umask of the user. This will potentially yield files in the repo that are not accessible to anybody else, not even to the svn technical user (e.g. 640 alice:alice).

Some time ago this issue had been worked around by using wrapper script which take care of setting the proper umask prior to invoking the real svn binaries. In practice, the umask problem has been fully eliminated in recent versions of subversion: during creation of new revisions, the FSFS 'filesystem' applies permissions of the previous revision file; no reason to worry about files creates with the umask of the committing user which could render the repository unusable for others.

The problem around permission bits is taken care of, however, it still has to be ensured that new files created in the db directory do not inherit the primary group of the user on behalf of whom the file is created, but instead, the files should be owned by that certain svn group all developers are member of. One common approach is to set the SGID bit on the db directory (which should be owned by svn:svn), this will do the trick (note: when running on *BSD Unices this step is not necessary). From this point on, all files created within this folder will inherit the group of the parent folder instead of the primary group of the user creating the file.

Malice

Repository permissions are set according to the recommendations of the book, files are owned by the technical user and group svn:svn, SGID bits are set where necessary. Developers can work without any issue from the point where they are put into the svn group since they get write permissions for the repo with the group membership, and will not screw up the permissions as the SGID bit is set, so newly created files will be owned by the svn group, and subversion - at least the not so dated versions - already ensures the proper umask is applied based on permissions of the surrounding files.

Subversion's internal access control mechanism governs, which developer can read or write which part of the repository. However, as all the developers are members of the svn group, they could read, modify or even delete any of the revision files. This situation is analogous to a stupid database admin well restricting access on SQL level, but forgetting about the fact, that anyone could just copy the raw database files (table spaces) and take them home unless proper filesystem permissions are applied... in the case of svn, any revision could be simply deleted by a malicious developer.

The solution

Let's remove those users for the svn group. The right question is not what permissions to grant the users to enable them to access the repo. With a Clark-Wilson mindset, the right question is, how we can enforce that they could access the repo via select subversion executables only, and not by other means. To narrow down the problem even further, for svn+ssh:// access it is just enough if users can access the repo throught the svnserve executable. Setting the SUID/SGID bit on the svnserve binary enables the invoking user to exercise more privileges in the scope of the spawned process than he posesses. And svnserve in fact will enforce svn internal access control by performing internal authorization checks.

As svnserve allows the user to specify an alternative configuration file on the command line, the diligent attacker could easily bypass internal access controls. Another command line switch enables the caller to specify an alternative identity to be used for the transaction. Therefore, I do not consider setting the SUID/SGID bit on svnserve a secure practice. Instead I rather prefer a small wrapper that checks the command was invoked in tunnel mode, without tricky parameters, an if this is not the case immediately drops the privileges and delgates the call to svnserve. Then, I provision this small wrapper with SGID and set the owning group of the executable to the svn group. Having investigated the command line switches available and the anatomy of svn+ssh://, my conclusion is that in tunnel mode, without fiddling around, only the -t (tunnel mode) argument is set by the svn client when spawing svnserve. Other arguments are either specific to daemon mode in which case there is no need for extra privileges, or activate functionalities in the presence of which I do not want to grant addition permissions.

In summary the wrapper is very simple: it check it was invoked with one and only parameter -t. If so, it passes control to svnserve, which inherits the privileges of the svn group in this case. If the executable was launched with other parameters, then it first drops all privileges, only then it invokes the original binary. Two small smart bits made their way into the wrapper: first and foremost the real svnserve process is executed with explicitely set, minimal environment variables to prevent mucking around (e.g. manipulating the PATH variable), further a virtual repository root is set which enables us to hide the exact location of the repository from nosey but less knowledgable eyes.

Because the SGID bit is not effective when set on scripts, I created the wrapper in C, which is also more preferable from security perspective. While the minimal set of permissions to be set on an executable script is r-x (as the interpreter will have to read the script to execute), for machine code --x is just fine. As security is the priority in this matter, to prevent further tinkering configuration is done via #defines and is burned into the binary.


#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define REAL_PATH "/usr/bin/svnserve.real"
#define ROOT "/var/svn"

char *secargv[] = { "", "--tunnel", "--root=" ROOT, NULL }, *newenv[] = { "PATH=/bin:/usr/bin", "SHELL=/bin/sh", NULL };
char **newargv = secargv;

int main(int argc, char **argv) {
	if (argc != 2 || strcmp(argv[1], "-t")) {
		if (setgid(getgid()) == -1) { //permanently drop privs
			perror("setgid()");
			return 1;
		}
		newargv = argv;
	}
	execve(REAL_PATH, newargv, newenv);
	perror("could not execute " REAL_PATH);
	return -1;
}

Build and deploy

There is no gcc on your server boxen (is there?!) so the build/deploy process is split into 2 phases.


#!/bin/sh

F=svnserve-sgidtunnel

gcc -Wall -O2 -o $F ${F}.c
strip $F

# # run on target machine:
# chmod 2111 $F
# sudo chown .svn $F
# sudo mv $F /usr/local/bin
# sudo dpkg-divert --add --rename --divert /usr/bin/svnserve.real /usr/bin/svnserve
# sudo ln -s /usr/local/bin/$F /usr/bin/svnserve
© 2003-2020 lithium.io7.org
Content on this site is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.