Setting up a CVS server on a Unix system usually means that you either run CVS in its insecure :pserver: mode or run it over SSH, requiring that the CVS users have a shell account and consequently, full access to your system.  The concept of a “chroot jail” is an essential part of isolating remote service users from remote system administrators.

Thanks to the magic of pam_chroot, you can host a full-featured, encrypted and secure CVS server on any server with SSH.  Users are able to connect to CVS via SSH but are denied access to any sort of command-line.

pam_chroot is part of the standard PAM modules.  It consults /etc/security/chroot.conf for a list of users to be chroot jailed:

# /etc/security/chroot.conf
# format:
# username_regex        chroot_dir
someuser                 /home/chroot_directory

This entry indicates that whenever the user “someuser” logs in, they are to be shuttled off into the virtual root /home/chroot_directory. They will only have access to commands and libraries available within that virtual root.

A default-configured Fedora Core 3 system won’t have chroot in place out-of-the-box. We need to tell the system that whenever the user logs in via SSH, they should first be authenticated and then dropped into the chroot jail if we’ve set them up in chroot.conf. This is done by editing the /etc/pam.d/sshd entry and adding the entry:

auth       required service=system-auth
auth       required
account    required service=system-auth
password   required service=system-auth
session    required
session    required service=system-auth
session    required
session    required
session    optional

Once the user has authenticated via the system authentication stack the chroot will take effect. Note that the remainder of PAM authentication is beyond the scope of this article. Consult the pam man-page and system documentation for more information.

The final step to enabling chroot on SSH is disabling privilege seperation in the SSH daemon. Privilege seperation is part of the SSH secure design that drops root access in certain parts of the application that don’t need it. Since the chroot system call requires root access, we need to disable this part of the system. This is done by editing the /etc/ssh/sshd_config file and adding the line:

UsePrivilegeSeparation no

You’ll need to restart SSH to let it pick up the configuration change.

servive sshd restart

Now that the chroot setup is complete, we need to build the chroot jail. You’ll need to determine exactly what your users will need within the jail and copy those files only. Make sure that you’ve got the cvs package installed before following these steps.

The following steps are inspired by the information provided by Julio Merino for a similar setup in NetBSD:

# Create the CVS chroot directory
mkdir -p /var/chroot/cvs
# Create the required root directories
cd /var/chroot/cvs
mkdir -p bin cvs dev etc home lib sbin tmp var
# Link the usr directory to the root
ln -s . usr
# Set the appropriate permissions on cvs and tmp
chmod 770 cvs
chmod 1777 tmp

We’ll now copy the required binaries to the chroot jail:

cp /bin/sh bin/sh
cp /bin/ls bin/ls
cp /usr/bin/cvs bin/cvs

Note that CVS will need /dev/null too, so we’ll copy it:

cp -a /dev/null dev/null

We can then run a single command to populate the required libraries for the binaries we installed. This command is handy to keep around (also from Julio Merino’s article). You’ll see some warnings about duplicate libraries and invalid files that appear as hex numbers - these are harmless garbage from the ldd output:

cp `ldd bin/* sbin/* | awk '{print $3}'` lib

At this point, the CVS chroot should be complete. You can test it using the chroot command as root:

chroot /var/chroot/cvs bin/ls -l
drwxr-xr-x    2 0        0            4096 Nov 15 04:09 bin
drwxrwx---    2 0        0            4096 Nov 15 04:07 cvs
drwxr-xr-x    2 0        0            4096 Nov 15 04:08 dev
drwxr-xr-x    2 0        0            4096 Nov 15 04:07 etc
drwxr-xr-x    2 0        0            4096 Nov 15 04:08 lib
drwxr-xr-x    2 0        0            4096 Nov 15 04:07 sbin
drwxrwxrwt    2 0        0            4096 Nov 15 04:07 tmp
lrwxrwxrwx    1 0        0               1 Nov 15 04:07 usr -> .
drwxr-xr-x    2 0        0            4096 Nov 15 04:07 var
chroot /var/chroot/cvs bin/cvs
Usage: cvs [cvs-options] command [command-options-and-arguments]
  where cvs-options are -q, -n, etc.
    (specify --help-options for a list of options)
  where command is add, admin, etc.
    (specify --help-commands for a list of commands
     or --help-synonyms for a list of command synonyms)
  where command-options-and-arguments depend on the specific command
    (specify -H followed by a command name for command-specific help)
  Specify --help to receive this message

The Concurrent Versions System (CVS) is a tool for version control.
For CVS updates and additional information, see
    the CVS home page at or
    Pascal Molli's CVS site at

If you received any errors about missing libraries or the like, copy them by hand to the lib directory in your chroot jail.

We’re now ready to create our CVS users. First thing we’ll do is create a group for them all to belong to:

groupadd cvsusers

Now add your CVS users, making their default group cvsusers. We’ve added a default shell that we’ll use later. The home directory for this user, /home/cvsuser1 by default, will still exist in the root filesystem. You can use this directory for setting SSH keys if you prefer to log in using private key authentication instead of passwords. For now, use the passwd command to set the appropriate passwords for each user you’re adding:

# Create the user with a default group of "cvsusers"
useradd -g cvsusers -s /sbin/nologin cvsuser1
# Create the alternate home directory in the chroot jail
mkdir /var/chroot/cvs/home/cvsuser1
# Set the user's password
passwd cvsuser1
# Add the user to the PAM chroot configuration
echo cvsuser1 /var/chroot/cvs >> /etc/security/chroot.conf

Once you’ve added the appropriate users for your CVS, copy your /etc/group and /etc/passwd files to the jail and remove all lines but those of your cvs users:

cp /etc/passwd etc/passwd
cp /etc/group etc/group

Set the group ownership of the cvs directory to your new cvsusers group:

chown .cvsusers cvs

Next, we’ll copy the files required for reverse lookups of uid/gid to username/groupname. Without this, CVS will end up with entries like uid750 in its history. It’s also the reason that zeroes appear instead of root for the username/groupname in the ls -l command we tested above. Note that reverse lookups are handled within glibc using the name switch server (NSS) and the default lookup source is the passwd/group files:

cp -a /lib/libnss*files* lib

Now, you should be able to see the reverse lookup for cvsusers in your chroot jail. If not, check to make sure that all of the required nss files were copied over.

chroot /var/chroot/cvs bin/ls -dl cvs
drwxrwx---    2 0        cvsusers     4096 Nov 15 04:07 cvs

We’re going to create the default shell nologin we assigned to each of the CVS users above. By running the only allowed command explicitly, we prevent the users from logging in or running any other command. Since the file is being created as nologin, we can ensure that they wouldn’t be able to log in to the root filesystem if the chroot failed in any way. Create this file as nologin in the chroot’s sbin directory:

/bin/cvs server

Now make it executable with chmod:

chmod +x /var/chroot/cvs/sbin/nologin

All of the required system files have been copied over, so we can now create our CVS repository:

chroot /var/chroot/cvs bin/cvs -d /cvs init
chown .cvsusers /var/chroot/cvs/cvs/CVSROOT

You should be able to log in to cvs via SSH and test your CVS server before we lock it down. Don’t forget to set passwords for your CVS users or SSH won’t let them in:

cd /tmp
CVS_RSH=ssh cvs -d:ext:cvsuser1@localhost:/cvs checkout CVSROOT
cvs checkout: Updating CVSROOT
U CVSROOT/checkoutlist
U CVSROOT/commitinfo
U CVSROOT/config
U CVSROOT/cvswrappers
U CVSROOT/editinfo
U CVSROOT/loginfo
U CVSROOT/modules
U CVSROOT/notify
U CVSROOT/rcsinfo
U CVSROOT/taginfo
U CVSROOT/verifymsg

If you can grab the files from the CVSROOT directory, you’ve successfully created a chroot-jailed CVS server. To test the security of this setup, try a few things to break it:

ssh localhost -l cvsuser1
cvsuser1@localhost's password: ****
Connection to localhost closed.

ssh localhost -l cvsuser1 bin/ls
cvsuser1@localhost's password:
< it hangs up here in cvs server mode, hit ctrl-c to kill this >
Killed by signal 2.

sftp cvsuser1@localhost
Connecting to localhost...
cvsuser1@localhost's password:
< it hangs up here in cvs server mode, hit ctrl-c to kill this >
Killed by signal 2.

Remember each of the steps above for adding new CVS users. Congratulations, your CVS server is now secure!

Read full post