changing the root directory without fooling yourself
chroot is one of the oldest isolation tools on Unix and, at the same time, one of the most misunderstood. Anyone building a jail environment to seal off a process from the rest of the filesystem must understand exactly what chroot actually secures and where that protection ends, since many servers have already been compromised through a chroot environment mistakenly believed to be secure.
Table of Contents
- 1. What process isolation means and why chroot exists
- 2. chroot fundamentals: changing the root directory
- 3. Building a chroot jail step by step
- 4. Binaries, libraries and devices inside the jail
- 5. The limits of chroot: not a security feature in the true sense
- 6. Practical example: chroot for SFTP and rescue systems
- 7. Common pitfalls when operating a jail
- 8. chroot versus namespaces versus containers: the distinction
- 9. Debugging and comparison table
- 10. Summary
- 11. FAQ
1. What process isolation means and why chroot exists
Process isolation describes the idea of deliberately exposing a running process to only a limited slice of system resources instead of granting it full access to the entire system. chroot, short for change root, is historically the first and simplest building block of this idea on Unix: the system call changes the root directory for a process and all its child processes, so that this process perceives a specific subdirectory as / from then on and simply no longer sees anything outside of it.
The original motivation for chroot was less about security than testability: in the 1970s, developers wanted to test new software versions inside an isolated directory structure without endangering the production system. Only later did the notion establish itself that chroot could also serve as a security boundary against compromised services, a misunderstanding that still leads to insecure configurations today. Anyone setting up a jail, meaning an environment sealed off with chroot, should therefore first understand what the tool was actually designed for.
2. chroot fundamentals: changing the root directory
The command chroot /path/to/jail /bin/bash starts a new Bash instance whose root directory is set to /path/to/jail. Inside this shell, /etc/passwd no longer refers to the system wide file but to /path/to/jail/etc/passwd, provided that file even exists. This is exactly where the first practical hurdle appears: chroot only changes the view of the filesystem, it does not copy any files and sets up no environment. If a required file is missing inside the jail, the process simply reports that it does not exist, even though it would certainly be present outside the jail.
It is also important that chroot exclusively requires root privileges, because without this restriction any user could perform arbitrary path changes and thereby undermine the system's security assumptions. The kernel itself enforces this restriction through the CAP_SYS_CHROOT capability. After the switch, the process stays permanently trapped in the new root view, unless it still holds root privileges and uses a known escape route, described in the section on the limits of chroot.
# Create a minimal jail directory structure
mkdir -p /srv/jail/{bin,lib,lib64,usr,etc,dev,proc}
# Change root and start a shell inside — requires root privileges
sudo chroot /srv/jail /bin/bash
# Inside the chroot, / now refers to /srv/jail on the host
ls / # shows only what exists under /srv/jail
cat /etc/passwd # fails unless /srv/jail/etc/passwd was created
exit # returns to the original shell, outside the jail
3. Building a chroot jail step by step
Building a working jail starts with the directory structure a program expects for normal operation: at minimum /bin, /lib, often /lib64 on 64 bit systems, and /etc for configuration files the program actually needs. Next, the desired binaries, such as bash or ls, get copied into the corresponding directory of the jail. The decisive step after that is often the most overlooked one: every copied binary brings its own dependencies on dynamic libraries that must also be provided inside the jail, otherwise the start fails with an error like "No such file or directory" even though the file is visibly present.
The tool ldd lists exactly these dependencies and is therefore the central helper when building a jail. For every line from the ldd output, the corresponding library must exist at the same relative path position inside the jail as it is referenced outside. With more complex programs carrying many dependencies, this effort adds up quickly, which is why production jails today are usually built with ready made tools like debootstrap, which automatically install a minimal, consistent base system into a target directory.
#!/usr/bin/env bash
# Populate a chroot jail with a binary and its shared library dependencies
set -euo pipefail
JAIL="/srv/jail"
BINARY="/bin/ls"
# Copy the binary itself into the jail, preserving its relative path
mkdir -p "$JAIL$(dirname "$BINARY")"
cp "$BINARY" "$JAIL$BINARY"
# Resolve dynamic library dependencies and copy each one
ldd "$BINARY" | awk '{print $3}' | grep '^/' | while read -r lib; do
mkdir -p "$JAIL$(dirname "$lib")"
cp -n "$lib" "$JAIL$lib"
done
# The dynamic linker itself must also be present
LINKER=$(ldd "$BINARY" | grep -o '/lib.*ld-linux[^ ]*')
mkdir -p "$JAIL$(dirname "$LINKER")"
cp -n "$LINKER" "$JAIL$LINKER"
4. Binaries, libraries and devices inside the jail
Beyond binaries and libraries, many programs need access to virtual filesystems like /proc and device files like /dev/null or /dev/urandom. These do not exist automatically inside the jail and must be explicitly bound in via mount --bind or created as standalone device files with mknod. If /dev/null is missing, for example, many standard tools already fail when trying to redirect output there, with a hard to trace error message.
The bind mount is by far the more robust choice compared to device files created manually with mknod, because it automatically inherits the permissions and properties of the original file and stays in sync with changes on the host. A common pattern in production jails is therefore to bind mount /dev, /proc and partly /sys from the host into the jail, rather than maintaining separate, isolated copies of these virtual filesystems.
# Bind-mount host device and proc filesystems into the jail
sudo mount --bind /dev /srv/jail/dev
sudo mount --bind /dev/pts /srv/jail/dev/pts
sudo mount -t proc proc /srv/jail/proc
# Verify from inside the jail
sudo chroot /srv/jail /bin/bash -c 'ls /dev; cat /proc/version'
# Always unmount in reverse order before deleting the jail directory
sudo umount /srv/jail/proc
sudo umount /srv/jail/dev/pts
sudo umount /srv/jail/dev
5. The limits of chroot: not a security feature in the true sense
The most important point about chroot is that it was never intended as a standalone security boundary and is explicitly not recommended as such in security literature today. A process with root privileges inside a chroot jail can, under certain conditions, escape it, classically through the so called "chroot breakout" using a second chroot() call combined with an open file descriptor outside the jail that still dates from before the switch. This escape route has been documented for decades and still works today whenever the process inside the jail holds root privileges.
In addition, chroot isolates neither process IDs, network sockets, nor user IDs. A process inside the jail still sees the same processes via /proc, provided it is mounted, can still access open network ports of the host, and fully shares the same kernel with the rest of the system. Real security isolation additionally requires Linux namespaces, cgroups, and ideally forgoing root privileges inside the jail, which are exactly the building blocks modern container runtimes are constructed from.
6. Practical example: chroot for SFTP and rescue systems
A legitimate and still widely used purpose of chroot is restricting SFTP users to their own directory via ChrootDirectory in the OpenSSH configuration. Since these users hold no root privileges inside the jail and OpenSSH itself strictly controls the jail environment, the chroot breakout problem largely disappears here, as long as no setuid binaries are provided inside the jail. This configuration is considerably more robust than a general purpose chroot call for arbitrary services.
A second classic use case is a server's rescue system: booting from a live system to repair a broken operating system, one chroots into the actual system partition to run tools like grub-install or dpkg with the correct root context, instead of applying them to the live system itself. In this scenario, chroot is not a security tool but a pure context switching tool, exactly matching its original purpose.
# /etc/ssh/sshd_config — restrict a user to their home directory via chroot
Match User sftpuser
ChrootDirectory /srv/sftp/%u
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
7. Common pitfalls when operating a jail
The most common mistake is setting up a jail while forgetting that timezone data, DNS resolution via /etc/resolv.conf, and locale files must also be present inside the jail if the jailed process performs network access or produces localized output. Without these files, DNS resolution seemingly fails for no reason, even though the network itself works fine, because the process inside the jail simply cannot find a valid resolv.conf.
A second common mistake is overlooking forgotten bind mounts when deleting a jail directory structure. Deleting the jail directory with rm -rf while /dev or /proc are still mounted accidentally deletes content of the host filesystem, because rm -rf operates transparently through the bind mount. Before deleting any jail, all bind mounts must therefore be reliably removed with umount in reverse order.
8. chroot versus namespaces versus containers: the distinction
Modern container runtimes like Docker use chroot, or rather its more secure successor pivot_root, only as one of several isolation layers. In addition, Linux namespaces are used, each independently isolating process IDs, network stacks, hostnames and user IDs, along with cgroups that enforce resource limits for CPU and memory. A container is therefore technically a combination of chroot like filesystem isolation plus namespaces plus cgroups, not a replacement for chroot but a substantial extension of it.
Anyone needing a real security boundary today should therefore reach directly for namespaces and container based tools instead of treating chroot alone as a supposed isolation layer. chroot nevertheless remains relevant as a lightweight tool for cases where no real security boundary is needed, but merely a controlled filesystem view, such as SFTP jails or packaging build environments.
9. Debugging and comparison table
To debug a jail environment that fails to start, strace -f chroot /srv/jail /bin/program reveals the exact point at which a missing file or library prevents the start. ldd run inside the jail itself shows whether all dependencies of a binary can actually be resolved. For a running jail, lsof on the host helps identify open file descriptors of the jailed process that could potentially represent an escape route.
| Mechanism | Isolates Filesystem | Isolates Process/Network | Security Boundary |
|---|---|---|---|
| chroot | Yes | No | Weak, bypassable with root |
| chroot + non root | Yes | No | Sufficient for simple cases (SFTP) |
| Linux namespaces | Yes (with pivot_root) | Yes | Considerably stronger |
| Docker container | Yes | Yes | Namespaces + cgroups + seccomp |
This table clearly shows that chroot alone sits at the lower end of the isolation scale. Production security requirements always require at minimum the combination with namespaces and forgoing root privileges inside the jail.
Mironsoft
Linux server hardening and process isolation for PHP hosting
Isolation that actually delivers what it promises?
We evaluate existing chroot jails for their actual security effect, migrate critical services to namespaces and containers, and set up SFTP or build jails cleanly and maintainably.
Isolation Audit
Review of existing jails for breakout risks and missing non root configuration
SFTP Jails
Clean ChrootDirectory configuration for customer and deployment access
Container Migration
Moving critical services from chroot to namespaces and cgroups
10. Summary
chroot changes a process's view of the root directory and was originally intended for test environments, not for security isolation. A working jail needs its own copies of all required binaries, their library dependencies, and bind mounts for /dev and /proc. A process with root privileges can escape a chroot jail through known techniques, which is why chroot should never serve as a standalone security boundary.
Sensible use cases remain SFTP restrictions without root privileges, rescue systems, and build environments. Real security isolation additionally requires Linux namespaces and cgroups, exactly the building blocks modern containers consist of. Anyone who knows this distinction uses chroot where it makes sense and relies on the stronger, more modern tools for critical isolation requirements.
chroot and Jails: The Key Facts at a Glance
How it works
Only changes the root directory of a process, copies or installs no files automatically.
Building a jail
Binaries, ldd dependencies and bind mounts for /dev and /proc are necessary, otherwise the start fails.
Limits
No protection against root processes, no isolation of process IDs, network, or user IDs.
Recommendation
For real security, additionally use namespaces and cgroups, use chroot alone only for non root cases like SFTP.