I have been recommending chroot jails as part of every hardening guide I write. Last week, having spent two days reading the actual implementation in the kernel source, I have concluded that I have been recommending them slightly incorrectly. The thing chroot does, and the thing chroot does not do, are different from what I had thought.
I want to write down both — for my own future reference, and because the misunderstanding is widespread enough to deserve clearing up.
What chroot actually does
chroot is a system call. When called with a path argument, it changes the calling process's idea of the root directory of the filesystem.
Normally, processes see the root directory as /. If they ask to open /etc/passwd, the kernel resolves the path starting at the actual root.
After chroot("/jail"), the process's view of / is now /jail in the real filesystem. If the process then asks to open /etc/passwd, the kernel resolves this against the new root, producing the actual filename /jail/etc/passwd. Anything outside /jail is, from the process's point of view, simply not there.
This is a visibility restriction. The process cannot reach files outside the jail by ordinary path operations. If the jail is constructed to contain only what the process needs, the process is — apparently — confined to a small portion of the filesystem.
Used correctly, this is a useful defence: a network daemon that gets compromised in a chroot jail is limited in what it can read, write, or execute compared to the same compromise without a chroot.
What chroot does not do
This is the part I want to be careful about.
chroot is not a security boundary against root. A process running with CAP_SYS_CHROOT (or, simply, running as root in pre-capabilities kernels) can call chroot again. It can also do chroot("..") from inside the jail to escape it, by way of a well-known technique involving a previously opened file descriptor.
The escape works like this. The process opens a directory file descriptor before chrooting. After chroot(), it does fchdir(fd) to change directory to that descriptor. The descriptor refers to a directory in the original filesystem, outside the jail. From there, repeated chdir("..") operations climb up to the real root. Then chroot(".") resets the jail to the real root. The process is free.
This attack has been documented for years and the technique is well known. It is closed only by ensuring that no root-capable process is ever allowed to chroot() itself (since the second chroot is what breaks the jail).
The practical implication: chroot is only a meaningful boundary against an attacker who is not root inside the jail. If your daemon runs as root inside a chroot, the chroot does not protect against the daemon being compromised.
chroot does not affect process visibility. A chrooted process can still call kill() on processes outside the jail, can still observe them with ps, can still send signals to them. The PID space is not jailed.
chroot does not affect network access. A chrooted process can still open sockets, connect to anything reachable on the network, and listen on any port it has permission for. The network namespace is not jailed.
chroot does not affect IPC. Shared memory, semaphores, message queues — anything in the System V IPC namespace — is shared across the entire host, regardless of chroot.
chroot does not protect against device file access. If a device file like /dev/kmem happens to be inside the jail, the process can use it. The jail constrains what paths the process can reach, not what devices are accessible.
What it follows that chroot is good for
Chroot is a reasonable defence in a specific, narrowly-scoped use case:
- The process inside the jail is not running as root.
- The jail contains the minimum files the process needs (a small
/lib, a stripped/etc, the binary itself, and any data files). - The jail contains no device files.
- The process is on a separate user ID with no significant privileges in the host system.
- The jail is the second of several defences, not the only defence.
Used in this configuration, chroot is a meaningful additional layer. A compromise of the daemon does not give the attacker access to the rest of the filesystem. The attacker has a small, bounded environment with no useful privileges. They can do harm only insofar as the privileges of the jail's user ID permit, which should be minimal.
How to actually deploy this
For a daemon that supports chroot natively (BIND, named, sendmail in some configurations), the procedure is:
- Create a jail directory:
/var/jail/<service>. - Populate it with: a stripped-down
/libcontaining only the libraries the daemon needs (ldd <binary>tells you which); the binary itself; any config files; any data files. - Create a dedicated user for the service:
useradd -d /var/jail/<service> -s /bin/false <service>. - Configure the daemon to start, do its privileged setup, then chroot and drop to the dedicated user.
- Test by deliberately compromising the daemon (or a copy of it) in a controlled environment, and confirming that the compromised process cannot reach files outside the jail.
For a daemon that does not support chroot natively, you have two choices: wrap it in chrootuid or similar, or modify the source. The former is easier; the latter is more reliable.
A small mistake I made
My own mail server, until last week, ran sendmail in a chroot jail but as root inside the jail. After re-reading the chroot semantics, this is no defence at all — a sendmail compromise still gives the attacker root inside the jail, who can immediately escape using the technique above.
The fix was to reconfigure sendmail to drop to a non-root user after the privileged setup, while still inside the chroot. This required updating the sendmail.mc configuration to specify the post-privilege user, and adjusting file permissions inside the jail accordingly.
The lesson: chroot without unprivileged user is decorative.
The bigger picture
Chroot is a 1979 mechanism. It was the first containment primitive in Unix. It has been showing its age for a while.
More recent containment mechanisms — FreeBSD's jails, Solaris's zones, Linux's user-mode Linux — go further. They jail the network namespace, the PID namespace, IPC, device access. A process in a FreeBSD jail cannot see processes outside its jail, cannot send packets to or from interfaces outside its jail, cannot access shared memory outside its jail.
Linux is going to acquire similar mechanisms over time. The 2.4 development kernel has hooks in this direction. By 2002 or 2003 I expect Linux to have something close to FreeBSD's jail equivalent.
In the meantime, chroot is what we have. Used correctly — unprivileged user inside, minimal jail contents, layered with other defences — it is a meaningful tool. Used incorrectly, it is a placebo. The difference is structural, and worth getting right.