Setting up a Chroot Jail for CVS



I've recently had to set up a fairly secure CVS server that grants access to several semi-trusted people. Because I didn't want to set up a pserver based CVS repository, I decided to use the UNIX / Linux chroot functionality to offer CVS over a SSH connection. This article relates my experiences and the oddities I encountered along the way.

Even though I'm going to use my so called chroot jail for a CVS server, you'll find a lot of commands and tricks that are equally applicable to any other kind of server. It's just that I needed to offer a CVS service instead of an Apache daemon or FTP server, for that matter.


You might wonder what you need to set up a chroot jail. Well, first of all you'd need some kind of UNIX based operating system, like Linux, Solaris, FreeBSD or any other choice. Of course you will need the chroot binary which provides the main functionality. If you're on Linux you'd also probably want a compiler, including the autotools, and wget because you will most likely need to compile some sources.

And, of course, you need root access to the system you're going to use chroot on.

Getting Started

Define a plan, create a group

Let's start with the basics: which users are going to use which services? In my case, I've got six people wanting to share source code using CVS. This is a kind of simple example scenario which I will use for this article. CVS is easy regarding it's dependencies, so if you are for instance going to offer Apache you might want to study first which files it'll need for succesfull execution.

After deciding what I'm going to offer to whom, I figured it would be easy to put all user accounts in the same user group. Since they were never going to do something other than using CVS, I created a sole user group and added the accounts only to this one. Use the groupadd command to create your user group:

groupadd cvsjail

So, in this article I'll be using the group cvsjail for all the accounts. Once the user group was in place, it was time to create the root directory for the CVS users, a simple combination of mkdir, chown and chmod did the trick, notice that I already create the new root's directory skeleton:

mkdir -p /cvsjail/{bin,cvs,etc,lib,usr,var}
ln -s /cvsjail/bin /cvsjail/usr/bin
chown -R nobody.cvsjail /cvsjail
chmod -R 070 /cvsjail

The /usr/bin symlink is there so that you don't have to put binaries in two distinct places. There's also a /cvs directory in there which will act as the CVS repository root.

Setting up Users

It takes sudo to chroot

A problem arises when you simply try to use a shell script which calls chroot when you log in as a ordinary user. You need to be the superuser to use chroot. To work around this problem, you can use the tool called sudo to elevate privileges for a normal user, or a group of users. In this example, we'll need to grant the cvsjail group the permission to execute chroot as superuser without entering a password.

You should use the command visudo as root to edit the /etc/sudoers file. Add a line that looks like this:

%cvsjail ALL= NOPASSWD: /bin/chroot

This line will prevent a password prompt appearing when chroot is executed and will also prevent the users in the cvsjail group to execute other commands as superuser.

Login argument passing

You will need to create a script that you can use as the shell for all the jailed user accounts. This fake shell calls sudo and chroot so the user can never get a normal login prompt. A tricky detail you've got to watch for is that this script passes all login arguments to the chroot shell, if you forget to do this, CVS won't work as it passes commands alongside the login action.

I'm using the following script, save it in the CVS jail, for example like /cvsjail/bin/enterchroot:

if [ "$1" = "-c" ]; then
for param in $*; do
if [ $i -gt 0 ]; then
PARAMS="$PARAMS $param";
let i++;

sudo /usr/bin/chroot /cvsjail /bin/su - $USER -c "$PARAMS"
sudo /usr/bin/chroot /cvsjail /bin/su - $USER

Make the script executable:

chmod +x /cvsjail/bin/enterchroot

You're now set to create the user accounts in the normal environment.

Creating user accounts in the normal environment

Using the command useradd, you can add the user accounts which will use the jailed environment. These user accounts should have a normal home directory so you can use SSH public/private keys to ease the login procedure. The shell should be set to the chroot login script displayed above. I created my user accounts like this:

useradd -m -s /cvsjail/bin/enterchroot -g cvsjail user1
useradd -m -s /cvsjail/bin/enterchroot -g cvsjail user2
useradd -m -s /cvsjail/bin/enterchroot -g cvsjail user3
useradd -m -s /cvsjail/bin/enterchroot -g cvsjail user4
useradd -m -s /cvsjail/bin/enterchroot -g cvsjail user5
useradd -m -s /cvsjail/bin/enterchroot -g cvsjail user6

The '-m' parameter indicates that the default home directory location should be used, the '-s' defines the login shell for each user, the '-g' defines the primary user group for each accounts and lastly every user is given a (fictive) username called 'userX', where X denotes a single digit.

You should use the passwd command to give each user a password (kind of important).

You're now ready to create user accounts inside the chroot jail.

Creating user accounts in the jailed environment

So, you've got the normal users set up, now they need to have a login entry inside the jailed chroot environment. If you're like me, you'll want to have the users seem the same from inside and outside the jail, this can be accomplished using matching user IDs (UID) and group IDs (GID). The first step is to find the user group entry in your normal /etc/group file. Mine's like this:


So, the GID for the cvsjail group is 408. The trick is to create a new group-file inside the jail which will be stripped, but almost identical to the real group file. To do this, fire up a text editor and create a file called /cvsjail/etc/group. Then enter the following lines:


Replace the number 408 with the GID in your own setup. Also, replace the usernames with the ones you will be using since my names aren't usable at all. After you've done this, we must recreate a new passwd file which describes the user accounts. Look for the users in the normal environment and note their UIDs:

#cat /etc/passwd

In my situation, the UIDs range from 1014 to 1019 because I created the user accounts right after each other. Well, you should check your own passwd file and write down the UIDs and usernames of the users. Then, create the passwd inside the chroot directory, name it /cvsjail/etc/passwd and enter your users like this (don't forget root):


As you can see, I've got all home directories pointing to the root (this is the root inside the jail), because my CVS users don't need home directories, only the shared /cvs directory in which the repository resides. Also take care that you enter the GID in the passwd file, otherwise you might encounter permission problems later on.

You might've seen that I gave each user a bash shell inside the jail. It isn't there yet, so we're now going to take a look at moving binaries to the chroot jail.

Binaries in a Chroot Environment

First things first

When you enter a chroot jail, you can't access any of your files anymore, since the root of the filesystem is moved. If you do want to run programs inside a jail, and to be honest - why wouldn't you, you need to provide the binaries and the required shared libraries inside the jail.

You can use the ldd and strace tools to find out which libraries are required for a given binary.

Installing bash

Installing bash, the shell for the jailed users, is kind of easy. First, copy the binary to the chroot jail and then find out which libraries it uses using ldd:

#cp /bin/bash /cvsjail/bin/bash
#ldd /bin/bash => (0xffffe000) => /lib/ (0x4001a000) => /lib/ (0x4001d000)
/lib/ => /lib/ (0x40000000)

Don't feel ashamed if you can't find anywhere on your system, it's not a library file. You can safely ignore it for now. Copy over the other libraries using commands like these (the actual libraries may differ for your system):

cp /lib/ /lib/ /lib/ /cvsjail/lib

That's it for bash. The next binary we'll setup is the cvs binary itself.

Installing cvs

Installing the cvs binary can be done in the same way we copied bash. First, copy the binary and determine the required libraries:

#cp /usr/bin/cvs /cvsjail/bin/cvs
#ldd /usr/bin/cvs => (0xffffe000) => /lib/ (0x4001a000) => /lib/ (0x40047000) => /lib/ (0x4005c000)
/lib/ => /lib/ (0x40000000)

It's now kind of easy to copy all these libraries using cp. I won't hold your hand on this one, again you can safely ignore the library.

The last binary we'll absolutely need is su, which is used in the enterchroot shell script we created in the beginning.

Installing su

Installing the switch-user binary is a bit more difficult than bash or cvs. Especially if you're on Linux you will have to compile a new binary which does not use pam. Sounds difficult? Try the following commands and you'll be all set without any trouble:

wget --passive
tar xvfz coreutils-5.0.tar.gz
cd coreutils-5.0
nice -n19 ./configure
nice -n19 make
cp src/su /cvsjail/bin/su

These commands download the source for su and related binaries and after configuring and compiling the sources, we copy the binary to the jail. At this point you'll again need to use ldd on this binary to find out which libraries to copy:

#ldd /cvsjail/bin/su => (0xffffe000) => /lib/ (0x4001a000) => /lib/ (0x40047000)
/lib/ => /lib/ (0x40000000)

Copy the required libraries (you probably have a lot of them already) and also copy the following two two nss-libraries. If you don't you'll get "I have no name!" prompts when logging in:

cp /lib/ /lib/ /cvsjail/lib

If you've done everything correctly, you're now set to go!

The Aftermath

Setting up CVS

First of all, try logging in using one of your new accounts using a 'su - username' command. You should see a simple bash prompt greeting you inside the jail:

frank@tightrope:~$ ssh user1@server
user1@server's password:
Last login: Sun Dec 25 17:52:43 2005 from

That's about it. You don't have any command at your disposal, but it works great for CVS. I initialized the repository using a simple CVS command:

cd /cvs
cvs -d /cvs init

You're good to go. Configure your favourite CVS client to connect via SSH and you can use the repository without worrying about the users snooping around your filesystem.

A nice aftertouch would be to store SSH public keys in the real user's home directories, but it isn't mandatory to do this.

About this article

This article was written on the 30th of December 2005.

Back to top