Systems exposed to the internet are heavily challenged to keep the bad
guys out, and keeping up with the latest security patches is not always
easy. So, the wise admin will attempt to institute systemic steps
to limit the damage should a compromise occur, and one excellent
method is the use of a chroot() jail.
A chroot jail presents a dramatically restricted view of the filesystem to an
application, and usually far fewer system privileges, and this all intends
to limit the damage should the application go awry or be subverted by the
bad guy.
This document touches on how chroot works and discusses some best
practices that developers and administrators can use to make their
installations more secure.
Background on chroot
The chroot system call changes the root directory
of the current and all child processes to the given path, and this is
nearly always some restricted subdirectory below the real root of the
filesystem. This new path is seen entirely as "/" by the process,
and we refer to this restricted environment as the "jail". It's not
possible to escape this jail except in very limited circumstances.
The chroot system call is found in all versions of UNIX that
we know of, and it serves to create a temporary root directory for
a running process, and it's a way of taking a limited hierarchy
of a filesystem (say, /chroot/named) and making this the
top of the directory tree as seen by the application.
How to break out of jail
There are well-known techniques used to escape from jail, but the most
common one requires root privileges inside the jail. The idea is for the
program to do a chroot to a subdirectory, leaving the current
directory outside the jail.
We'll add more notes on ways to break out of a jail - which is meant
more to show what must be protected against than it is as a how-to
for jailbreakers -- but we've found a good article on chroot in general
here.
- Use mknod to create a raw disk device, thereby doing
pretty much anything you like to the system.
- Use mknod to create /dev/mem and modify kernel memory
- Find a carelessly-left hard link that leads outside the
jail (though symbolic links don't escape jail, hard
links do).
- Use ptrace to trace a process living outside the jail.
We may be able to modify this program to do our bad stuff on our behalf.
Almost all jail breaking requires root privileges.
General chroot principles
We have presented these in no particular order, and no one site will
use them all. In particular, some tips apply to developers at the source
code level, while others apply to administrators trying to jail an
existing system.
Many of these points may end up being overly petty in practice, in
that there are only so many layers of defense that a workable system
can use, but we'll present all we can think of and let you pick and
choose. An overriding principle is "What if the bad guy somehow does
X? How can we limit our exposure".
Our general concern is mostly about remote buffer overflows, and
this can give the bad guy complete control over our CPU: all our
steps are designed to limit the damage should this unfortunate
circumstance arise.
- Run in the jail as a non-root user
-
A chroot jail is not
impervious to escape, but it not easy and requires root permission in
the jail itself, so we must take steps to limit this possibility. By
running the jail as a non-root user, it's as secure as we know how to
make it. It may be necessary for the daemon to launch as root in order
to do a few tasks that require these permissions (say, binding to a
low-numbered port), but the program must "give up" its root permissions
after doing so.
-
We believe that this single factor is the most important one
in setting up a jail properly.
- "Give up" permissions correctly
-
We've seen situations where
on some operating systems, a program can jump back and forth between
a non-root user and root by use of a "saved" uid, and this has been
exploited by the bad guy who get root.
-
The details of how to do this correctly are much more tricky when
OS differences are taken into account: the variants are setresuid()
seteuid(), setreuid(), and setuid() — it's likely
that this does not exhaust the options. The right one depends on the OS
you're running.
-
The best resource by far we've found on this is the outstanding
Usenix 2002 paper Setuid Demystified,
by Hao Chen, David Wagner, and Drew Dean: it is precisely on point, and we'll
direct the reader to section 5.2 "Comparison among Uid-setting System Calls".
- Explicitly chdir into the jail
- The chroot call itself does not change the working directory, so if
the new root is below the current directory, the application can
still have access outside resources.
-
The application should explicitly change to a directory within the
jail before running chroot:
-
...
chdir(dir);
chroot(dir);
setXXuid(nonroot); // give up root permissions correctly.
...
-
This closes a trivial escape route from the jail (but we'll note that you
must use the proper setuid-esqe calls as noted in the previous item).
-
An alternate order of chdir and chroot:
...
chroot(dir);
chdir("/");
...
appears to be equivalent.
- Keep as little in the jail as possible
- This limits what
can be compromised should a vulnerability be discovered. Often this
requires development support to do some "preloading" of non-jailed
files before the chroot operation itself is performed (we'll touch
on this a bit more later). But we're quite ruthless in removing things
from the jail when possible.
- Limit non-jail running of jailed binaries
- For systems that
do not have a command-line option for running chroot, the only alternative
is to create a wrapper program. This wrapper will perform the key chroot
operation, give up root permission, and then execute the jailed binary.
-
The wrapper must be run as root (only chroot can perform this
operation), but the wrapper itself must not be found in the jail.
Otherwise an intruder could quietly compromise the wrapper, and
the next time the system is launched, the intruder's program would
be run as root in a non-jailed environment. This is complete
compromise.
- Have root own as many jailed files as possible
- This limits the
ability of the intruder to make changes should a compromise occur.
Our feeling is that the most likely cause of penetration will be
the buffer overflow exploit in which the intruder executes arbitrary
code in environment, and for files that the jailed system need not
ever write to, making them readonly and owned by root means that
the penetration can't chmod the file before writing to it. This
rule applies to directories as well.
- Drastically limit all permissions of files and directories
- Our
feeling is that if a permission bit is not required, it should not
be set. For instance, the jailed "/dev/" directory should be of
mode d--x--x--x with owner = root. Even though the only thing
in the directory is /dev/null, forbidding searching of a directory
strikes us as prudent practice across the board when it's known to work.
- Create a permissions-setting script
- When first setting
up the jail, many of the permission-related knobs are tweaked by hand
as we gradually tighten things up, looking for things to break (at
which point the knob is eased back a bit). This research is intricate,
and the knowledge gained really ought to be represented in source code
somewhere.
-
We typically create a small shell script — living outside the jail
— that sets the owner, group, and permissions mode on every file in the
jailed environment. It always starts with a few recursive change-everything
options to hardcode everything to very tight permissions, then relaxes
the settings on the files that can tolerate this. It's important to include
documentation in the script on why particular permissions are relaxed,
as well as describing why certain files are found in the jail in the first
place.
-
Once this script is created, we typically make all of our
permissions-related changed here and then re-run the script to
make them take effect. This is the only way that we can be sure
that our script matches the running environment.
A great side benefit of the permission script is that it serves as
documentation to the next person setting up a similar environment.
-
A sample permission script that we use for one of our projects
(running BIND in a chroot jail). The specific details aren't
really important, but this gives an idea
-
cd /chroot/named
# by default, root owns /everything/ and only root can write
# but directories have to be executable too.
chown -R root.named .
find . -print | xargs chmod u=rw,og=r # *all* files
find . -type d -print | xargs chmod u=rwx,og=rx # directories
# the "secondaries" directory is where we park files from
# master nameservers, and named needs to be able to update
# these files and create new ones.
find conf/secondaries -type f -print | xargs chown named.named
find conf/secondaries -type f -print | xargs chmod ug=r,o=
chown root.named conf/secondaries
chmod ug=rwx,o= conf/secondaries
# the var/run business is for the PID file
chown root.root var
chmod u=rwx,og=x var
chown root.named var/run
chmod ug=rwx,o=rx var/run
- Try to do the chroot operation inside the daemon itself
- ... rather than rely on the explicit chroot command (this requires
source code modifications). A daemon that has its own internal chroot
can often park the executable located outside the jail: this is a big
win because an intruder is not able to ever infect the binary directly.
-
But the more immediate benefit is that shared libraries and other startup
files can be automatically loaded from the full system and need not be
located inside the jail. This not only makes the system safer — less
exposure to the outside — but also makes it easier to set up.
-
In many cases, even configuration files can be loaded from outside the
jail, though this won't usually work if the daemon includes any kind of
"reread config files" option.
- Preload dynamically loaded objects
-
For developers adding chroot
support to programs, consider operations that require access to full-system
resources and perform them before closing the jail door. These steps are
often not entirely obvious at first and require some trial and error,
but we've found several that qualify.
-
Many systems load nameservice resolver clients dynamically at runtime,
and they are not included in the shared objects bound to the executables.
We have found that simply calling gethostbyname one time before the
jail door is closed will load all the appropriate libraries required, so
that later nameservice requests are handled properly:
-
(void) gethostbyname("localhost");
-
We believe that syslogging operations fall in this category too, as many
systems uses UNIX domain sockets for this and require access to the socket
that syslogd is listening on. We've not done the modifications required
for syslog support and cannot offer any specific suggestions. We believe
that Solaris -- with its use of "doors" -- is an added complication.
-
For daemons that permit cmdline parameters to select the runtime users
and group (after giving up root), the mapping of name to UID and GID must
be done before the chroot operation so that the system-wide /etc/passwd
and related files are used, not the one inside the jail. See the
next section for the rationale.
-
This bit of C code shows the idea of how the user lookup should be performed
separately from the user ID changing:
-
if ( geteuid() == 0 )
{
struct passwd *userent = 0;
if ( (run_as_user != 0) && (userent = getpwnam(run_as_user)) == 0 )
{
/* ERROR */
}
chroot( working_dir );
if ( userent )
setXXuid(userent); // use the proper call!
...
- Avoid using the jailed /etc/passwd file
- ...particularly for
name to UID mapping used to determine the runtime user ID of the daemon.
The mapping involves scanning the passwd file for the given name
(say, named) and finding the user ID associated with it. If the
bad guy somehow manages to compromise the jailed passwd file, it's
possible that the UID for the runtime user could be changed to zero,
which is root. This will take effect the next time the daemon restarts.
-
The bad guy shouldn't be able to compromise this file in the first place,
because it should not be writable by the running user, but it's not out
of the question that the daemon could somehow retain a writable file
descriptor that the buffer overflow could use to modify the file: we
believe we have seen this happen before. As is so common, a bug in one
area of the system can have surprising impacts on security.
- Close file descriptors aggressively before chrooting
- We don't
wish to leave handles open to non-jailed resources because these can all
be exploited by those living inside the jail. Some file descriptors are
required (say, to the syslog daemon), but developers should make a point
to close anything that is not strictly required.
-
Update 7/2013: a great resource for this can be found here.
(tip o' the hat to Tim Kuijsten for this)
- Link config files from the outside
-
Some systems (such as BIND)
share the configuration file between the jailed daemon and other utilities
that are run from user mode. In this case, the config file simply must
live inside the jail so that the daemon can access it, but the other utils
from user mode still need to access this file. Rather than rebuild these
utilities to use the special path (say, /chroot/named/etc/named.conf),
instead go to the "regular" place for this file and create a symbolic
link from the outside to the inside of the jail:
-
# ln -s /chroot/named/etc/named.conf /etc/named.conf
-
This allows most of the tools to operate "normally", though one has
to be a little more careful that users editing /etc/named.conf
realize that they're affecting a jailed system.
-
This doesn't go the "other" direction, though it's not always obvious
at first. Symbolic links from inside the jail to the outside will
work for the administrator but will not work for the system running
inside the jail.
- Update environment variables to reflect the new root
-
Once in the jail, some environment variables might need to be updated to reflect
the new jail-ness of the application. In particular, many shells maintain a $PWD
variable reflecting the current working directory, and the getcwd(3) library
function consults it as an optimization step.
-
Calling putenv("PWD=/") after the successful chrooting synchronizes
this variable with the new directory.
-
Other environment variables might need to be altered (or removed) depending
on how much it matters that environment leakage from the non-jailed environment
might impact the jailed one.
Other Resources