Configuring a CVS repository is not an easy task. Creating anonymous accounts for read-only access to projects while maintaining write security—and perhaps leaving some projects unreadable—presents a formidable obstacle. This problem is sometimes solved through the use of pserver, though pserver can present new issues of maintainability and security.
ssh is a popular choice for providing CVS access, since it offers security for both authentication and data transfer, and access decisions are performed by the operating system, requiring no extra server configuration. However, ssh raises issues of its own, since selectively turning on read permissions for certain groups is not possible when the group permission fields are already used to grant developer write access, and it can be difficult to maintain coherency among accounts if users on a system are given a separate account for CVS. This document attempts to solve these problems and create a secure, but accessible, CVS server using a chrooted environment, ssh, and filesystem access control lists.
To avoid the overhead of a restricted shell program such as smrsh or rbash, I wrote a small program, chrootsh, to handle chrooting into a CVS environment and calling the cvs program. Other shell programs are certainly possible, but this document is written with chrootsh in mind, and chrootsh requires no extra configuration. In addition, I wrote a program to handle the maintenance of ssh keys between a user's normal account and cvs accounts, called keycopy. Both of these programs are available from http://gophernet.org/projects/cvsstuff.tar.gz. Lastly, a PAM module to maintain coherency among a user's passwords is available at http://gophernet.org/projects/pam_userlink.tar.gz. Instructions for anonymous CVS access are available at http://gophernet.org/anoncvs.html.
This document assumes that you know how to configure your system to use ACLs and that PAM is used for authentication. PAM isn't necessary for the repository itself—keycopy and pam_userlink are simply convenient tools that you might want to use—but ACLs are needed in order to control read access for specific projects.
An easy way to prevent CVS users from accessing anything else on the host system is to confine them to a cvs environment containing only the repository and necessary support tools. This environment will need to contain copies off all necessary programs and system configuration files, as well as take some special considerations due to the lack of shared libraries.
Henceforth, I'll refer to the chroot environment as "/cvs". The path doesn't matter; you can put it wherever you want, I just don't want to type “environment” any more.
Obviously, a copy of the cvs binary is necessary. To avoid the need for libraries in the chroot environment, you'll probably want a copy of cvs that has been statically linked. No peculiar issues arise in creating this; simply download cvs from http://www.cvshome.org/, include --enable-server among your configure options, and include LDFLAGS=-static in your environment (using gcc. other compilers may have different flags to use for static linkage, such as -B static).
On a system that uses glibc, such as most Linux systems, the statically linked cvs binary is somewhat deceptive. Although it is not dynamically linked to any shared libraries, it still requires shared libraries in order for the “name service switch” functions, such as getpwent, to work properly. The decision of which method to use to use for password and group lookups is determined by the contents of /etc/nsswitch.conf, and the “switch” is performed by opening one of the /lib/libnss_* shared libraries. It is possible to statically link with the appropriate NSS library, but since static NSS libraries are not built by default, most people would need to reconfigure glibc. It is probably far less difficult to simply copy the necessary libraries and the dynamic linker into /cvs/lib. If this route is taken, building the statically linked copy of the cvs binary may not be worthwhile.
On some systems, such as Solaris, creating a statically linked cvs binary may not be possible, so shared libraries will be necessary regardless of what you want.
Besides cvs itself, nothing else is necessary in /cvs/bin. I like to keep chrootsh in /cvs/bin, but since it is called by sshd before any chroot call is made, it can be installed anywhere. Similarly, it does not need to be statically linked. In order to perform the chroot, though, it does need to be installed setuid root, and it will drop its privileges before running cvs.
In order to know which user has committed what, user information is needed, and group information is needed as well if the restricted admin features of cvs are being used.
For most systems, simply creating the files /cvs/etc/passwd and /cvs/etc/group is sufficient. Copying /etc/passwd and /etc/group is probably acceptable for most people. The copies in /cvs will need to be updated any time a change is made to the cvs accounts, and this is, for the most part, unavoidable. Hard links will only work if /cvs is on the same filesystem as /etc, and symlinks offer no aid for reading files outside of the chroot. It may be possible to create a proxy daemon to respond to name service requests and use a NSS module that makes requests through the network, but this would create a lot of complexity for little benefit. Another possible solution, and one that requires no new software, is to synchronize the two files in a maintenance script, run from cron or some similar service. Or, you could simply remember to keep the copies of files in /cvs up to date every time you change /etc/passwd and /etc/group for CVS users. The only security problems raised by out-of-sync files are those of the cvs admin command, since the cvs server will read the data in /cvs/etc/group to determine whether the user has sufficient access. The worst that can be caused by an out-of-sync passwd file is that commits could be attributed to the wrong developer, or the developer's name appears as something like “uid8004”.
Many systems require an /etc/nsswitch.conf file to determine what method should be used to access the passwd and group databases. This is so that several methods of lookups, such as NIS, or database queries, can be used transparently in place of a traditional /etc/passwd system. Most people just use /etc/passwd and /etc/group, though, so the “files” method of name service lookups is the most commonly used. In general, the following is sufficient for programs to use the passwd and group files in /cvs/etc:
passwd: files
group: files
Note that some systems also require shared libraries for name service functions to work correctly.
Libraries aren't normally required when statically linked binaries are in use, but name service switch functions often create an exception. While some systems, such as FreeBSD, compile the commonly used NSS functions into libc, allowing for statically linked binaries that usually behave as one might expect, other systems, such as Solaris and glibc systems, require that NSS libraries be opened at runtime.
For systems that require shared libraries for NSS, copies of these libraries will be necessary in /cvs/lib. The actual path of this directory may differ on some systems. For example, Solaris looks for the dynamic linker and libraries in /usr/lib. The glibc dynamic linker will use either /lib or /usr/lib in the absence of both LD_LIBRARY_PATH and a library cache, but the linker itself must appear in /lib, as well as the libnss files to be opened by the NSS functions.
On Solaris, the NSS libraries are found in /usr/lib/nss_method.so.1, and on glibc-2 systems they are found in /lib/libnss_method.so.2. Use ldd to determine what libraries these libraries require, and copy everything over until you run out of dependencies. Don't forget to include the linker itself, since this may or may not show up in ldd's output.
cvs requires that /dev/null exist. Create /cvs/dev/null using the command mknod /cvs/dev/null c major minor. The major and minor numbers are operating system dependent, but can be found simply by running ls -l /dev/null to see what your system uses. For some examples, Linux uses 1, 3; Solaris uses 13, 2; IRIX uses 1, 2; and FreeBSD uses 2, 2.
cvs also requires /tmp. /cvs/tmp is just a normal tmp directory; the mode should be 1777.
So far you should enough of an environment setup that you can chroot into it and run cvs. cvs won't be able to do much yet, but it can run. The output of ls -l (using a Linux system for library and device examples) might look something like the following:
/cvs# ls -AlR /cvs /cvs: total 20 drwxr-xr-x 2 root root 4096 2004-01-09 00:06 bin drwxr-xr-x 2 root root 4096 2004-01-09 00:01 dev drwxr-xr-x 2 root root 4096 2004-01-09 00:06 etc drwxr-xr-x 2 root root 4096 2004-01-09 00:03 lib drwxrwxrwt 2 root root 4096 2004-01-09 00:06 tmp /cvs/bin: total 588 -rwsr-xr-x 1 root root 4444 2004-01-09 00:01 chrootsh -rwxr-xr-x 1 root root 586256 2004-01-09 00:02 cvs /cvs/dev: total 0 crw-r--r-- 1 root root 1, 3 2004-01-09 00:01 null /cvs/etc: total 4 -rw-r--r-- 1 root root 0 2004-01-09 00:06 group -rw-r--r-- 1 root root 27 2004-01-09 00:06 nsswitch.conf -rw-r--r-- 1 root root 0 2004-01-09 00:06 passwd /cvs/lib: total 1756 -rwxr-xr-x 1 root root 97900 2004-01-09 00:02 ld-linux.so.2 -rwxr-xr-x 1 root root 1517893 2004-01-09 00:03 libc.so.6 -rwxr-xr-x 1 root root 24659 2004-01-09 00:02 libcrypt.so.1 -rwxr-xr-x 1 root root 90346 2004-01-09 00:03 libnsl.so.1 -rwxr-xr-x 1 root root 44858 2004-01-09 00:02 libnss_files.so.2 /cvs/tmp: total 0
Creating separate users for CVS access instead of using the normal accounts for users on the system allows for a finer degree of control concerning who can read or write which files, as well making the chroot environment possible. However, this often creates problems with passwords, since users are unable to login and change the password for their CVS accounts. Many systems now use PAM, Pluggable Authentication Modules, to handle authentication and password management, so the solution can actually be as simple as adding another module to the password phase of programs. pam_userlink simply takes the new password that was given and runs it through the real password chaging module, often pam_unix, as a different user. If passwords are only modified through PAM, this can effectively link a single password to multiple accounts. On systems that do not use PAM, this sort of method may not be possible, and some other tecnique for managing CVS passwords will need to be devised.
Another common annoyance when using ssh instead of pserver is that the .cvspass file is not used, so a password must be typed each time cvs is run. This problem can be circumvented through the use of ssh keys, and the keycopy program can be used to allow users to manage their own keys. If file permissions are set carefully, then even an anonymous account can be given an ssh key, with the private key distributed to users along with, or instead of, the password.
I mentioned earlier that restricting read operations is often an obstacle in the absence of pserver since traditional UNIX filesystem permissions don't offer enough flexibility. This problem is encountered not only in CVS; many applications that need to create finely-tuned sets of permissions are often forced to create their own systems of privileges. As a solution to these problems, filesystem access control lists were created, and though POSIX.1e filesystem ACLs were never formalized as a standard, they are implemented in many operating systems, including Linux, Solaris, FreeBSD, and IRIX.
Access control lists aren't as difficult as they may sound. In addition to the standard user/group/other definitions, which form a base definition for access to a file, read/write/execute permissions can be added for additional, arbitrary users and groups. Taking advantage of access control lists for a CVS repository requires no more than granting read permission for a group to a project directory.
For each project in the repository, create two groups: a group to read from the project, and a group to write to the project. The write group will be given ownership of the directory, as is usual for CVS projects, and the read group will be given read access through the directory's access control list. More on specific permissions and considerations is in the next section, on the repository.
As an example for a naming convention, I name my groups as project-r and project-w. This is certainly not the only way to do things, but a consistent naming scheme is helpful in order to remember which group does what when you need to change something.
In addition to groups for the projects, two additional groups should be added for the repository: a generic “users” group for all cvs users (cvs-users is a possible name), which will be used to manage the lock directory, and an “admin” group (cvs-adm, perhaps) to be used to control access to the CVSROOT directory. You may need an additional “cvsadmin” group, which may be the same as the other administrative group, if you want to restrict the use of the cvs admin command.
For each user to whom you want to give CVS access, you should create an additional account. The shell for CVS users should be your restricted, chrooting shell, and they should be given given home directories if you want the user to be able to keep ssh keys. In addition, home directories can be used to prevent ssh from trying to run xauth.
When a user requests X11 forwarding, which is common with ssh connections, sshd will attempt to run xauth, which will create a .Xauthority file in the user's home directory. Although this is generally harmless, paranoid administrators will probably want to prevent random files from being written to the CVS home directories. In some cases, the user may not even be able to write to their home directories, and xauth will simply hang, attempting to lock the authority file, until it decides to give up. xauth can be avoided entirely, however, since sshd first checks for the presence of two files before deciding on xauth: /etc/ssh/sshrc for a system-wide xauth script, and ~/.ssh/rc for per-user xauth scripts. Creating a system-wide xauth wrapper is probably too much trouble, but adding an empty rc file when creating a user's home directory is fairly simply. This file doesn't need to do anything, it just has to be executable.
The users don't actually need write permission to any part of their home directory besides .ssh, which needs to be writable in order to create the authorized_keys file. It's necessary that they own their own home directory, otherwise ssh will have a fit, and they need to be able to enter it to read the key file, but that's about it. The home directory for a typical ssh user may look like the following:
# ls -AlR /cvs/home/das-cvs /cvs/home/das-cvs: total 4 drwx------ 2 das-cvs cvs-users 4096 2004-01-10 01:24 .ssh /cvs/home/das-cvs/.ssh: total 4 -rw------- 1 das-cvs users 222 2004-01-08 17:55 authorized_keys -rwxr-xr-x 1 root root 0 2004-01-10 01:40 rc
The authorized_keys file was created by the keycopy command; you shouldn't have to create it. Whether to allow CVS users write permission to their own directories probably isn't worth worrying about, since they can't actually login or run anything besides cvs, but it can prevent programs like xauth from writing random files to the filesystem if accidentally run, as well as try to minimize damage when security exploits are found in cvs or sshd.
Anonymous users can use a setup similar to this, with the only differences being that the authorized_keys file, if present, is added by you, the administrator, and write permission should not be available on .ssh. The reason for this last requirement is to prevent users with access to the system from replacing the authorized_keys file, since the password for the anonymous account is probably well known. The keycopy command uses the password of the CVS user to authenticate, so a lack of write permission ensures that authorized_keys can't be changed by other users. An anonymous user's directory may look something like the following:
# ls -AlR /cvs/home/anoncvs /cvs/home/anoncvs: total 4 dr-x------ 2 anoncvs cvs-users 4096 2004-01-10 01:54 .ssh /cvs/home/anoncvs/.ssh: total 4 -rw-r--r-- 1 root root 220 2004-01-10 01:54 authorized_keys -rwxr-xr-x 1 root root 0 2004-01-10 01:40 rc
For those of you just skipping to the last subsections to see what you need to do, you need the following in /etc/passwd:
anoncvs:x:7999:8000::/cvs/home/anoncvs:/cvs/bin/chrootsh
das-cvs:x:8000:8000::/cvs/home/das-cvs:/cvs/bin/chrootsh
qwr-cvs:x:8001:8000::/cvs/home/qwr-cvs:/cvs/bin/chrootsh
The last two entries are just example users. Group 8000 is the cvs-users group, like in the following:
cvs-users:x:8000:
cvs-adm:x:8001:das-cvs
dbiff-r:x:8002:anoncvs
dbiff-w:x:8003:das-cvs,qwr-cvs
You need a read group and a write group (the -r and -w) for each project in your repository. Look back a few lines to the other example sections for the home directory layout.
By now you should have all of the support files in place, and it's almost time to make the actual repository. But first, another directory must be created to handle lock files.
By default, CVS simply keeps all of its lock files inside the repository. However, since locks must be made in order to read files as well as write to them, this makes it impossible to provide read-only access. Fortunately, it's possible to tell CVS to place lock files somewhere else; you just need to create the directory. This directory will be used by everyone, and so needs to be writable by everyone with CVS access. Lockfiles for subdirectories within projects are placed in subdirectories of their own, so you need to ensure that group ownership is inherited by these new directories. On a BSD system, this is done automatically, but systems that use SysV group inheritance, such as Linux, require the setgid bit to be set.
mkdir /cvs/locks
chgrp cvs-users /cvs/locks
chmod 2770 /cvs/locks
Now, let's make a repository! Start it the usual way, with cvs init.
cvs -d /cvs/repository init
I named my repository “repository”, but this actually turned out to be quite a bit to type. You may want to use something shorter, like “cvs”, before you tell everyone to use the longer name and end up stuck with it.
First, fix the permissions on CVSROOT. It should be world readable, but only writable by the group you chose for administrative functions (cvs-adm in the groups example). The mode should have been set correctly by cvs, so you should only need to change the group ownership.
chgrp cvs-adm /cvs/repository/CVSROOT
Now to configure the lock directory. This requires a checkout and commit of CVSROOT, so everything needs to be in place before this point: users, groups, restricted shell, cvs binary, sshd, and assorted support files. Set the CVS_RSH environment variable to “ssh” and checkout CVSROOT using a user that has been created with cvs-adm membership.
cvs -d ':ext:das-cvs@fiona.gophernet.org:/repository' co CVSROOT
Open up the file “config” in a text editor and set the “LockDir” variable to the directory you chose for locks. Don't forget to strip out the chroot portion. For example, if you are using /cvs for your chroot environment and chose /cvs/locks to hold lock files, you would use “LockDir=/locks”. Write the file, and run cvs commit from within the CVSROOT directory to save the new configuration.
cd CVSROOT
edit config
cvs commit
To create a project, start with just a directory. The group owner will be the project-write group, and the directory should always be globally unreadable, whether read-only access is desired or not. Read access is controlled through the access control list. Don't forget to include the setgid bit if your system needs it.
mkdir /cvs/repository/project
chgrp project-w /cvs/repository/project
chmod 2770 /cvs/repository/project
For the read permissions, add an ACL entry granting the project-read group read and execute access on the directory. How exactly to do this differs depending on the operating system. Using the getfacl/setfacl interface (BSD, Solaris), use something like this:
setfacl -m 'group:project-r:r-x' /cvs/repository/project
which adds read and execute permissions (r-x) for the group project-r. The chacl interface (IRIX) is similar, but requires that the default entries be specified as well instead of taking them from the file permission bits. Something like
chacl 'user::rwx,group::rwx,other::---,mask::rwx,group:project-r:r-x' /cvs/repository/project
will do the same as the setfacl command. Linux offers both interfaces.
And that's it. Entry to the topmost directory is the only place where access must be enforced, and CVS will create appropriate permissions below that. ACLs don't need to be propagated to subdirectories, only the group owner. Any time users' CVS permissions need to changed, simply add them to or remove them from the appropriate groups.
Since running ls is just so much fun, here's one last listing as a quick reminder for how the repository should look:
# ls -Al /cvs/repository total 12 drwxrwxr-x 3 root cvs-adm 4096 2003-12-06 18:26 CVSROOT drwxrws---+ 2 root project-w 4096 2004-01-10 12:35 project # getfacl /cvs/repository/project # file: cvs/repository/project # owner: root # group: project-w user::rwx group::rwx group:project-r:r-w mask::rwx other::---