Running a Java server application in a chroot jail

Java has become a popular language for Internet-facing applications, and for good reasons. Java is, for example, largely immune to the buffer-overrun and stack-smashing attacks that plague servers written in C. However, because Java is intended to be platform-neutral, it lacks many of the low-level features available to C for sandboxing operations, and this is undeniably a problem for security.
The situation hasn’t been helped by the decision of the JDK maintainers to remove the ancient “security manager” API. Their reason for this is that the security manager isn’t used much, and there are better ways to sandbox an application using operating-system features.
They’re right – at least in an industrial context. We have containers like Podman and Docker, all kinds of virtual machines and hypervisors, and so on. Unfortunately, these don’t work so well in the “small web” world, where enthusiasts and researchers are running their online services from cheap virtual servers, or Raspberry Pi boards in the attic. We’d like to get at least some of the sandboxing features the heavyweight methods offer, but without their resource demands, costs, and energy consumption.
This article is about running a Java server application on Linux in a least-privileges way, without resorting to containers or virtual machines. In particular, we want to run the application:
- As an unprivileged user, and
- With access only to that part of the filesystem that contains the applications’ own data and code.
A “chroot jail” is a way to achieve both these goals. The first
argument to the chroot command is a new root directory –
the directory that will be see as / from within the
application. The second argument to chroot is the
executable to run. Any application run under chroot will be
prevented, at the kernel level, from seeing any file outside the
selected directory. chroot has to run as root,
but modern versions provide a way to change to an unprivileged user
before running the application.
Unfortunately, setting up a chroot jail for a Java application is fiddly. That’s because we need to replicate the whole JVM installation, and all its dependencies, inside the directory we’re using for the jail.
This article outlines the approach I use. I doubt it’s the only one, and I don’t even claim it’s the best – it’s just what works for me. A warning though: this process can’t easily be automated, and it involves a good deal of trial and error.
1. Create a directory for the jail
This can be any convenient directory, but bear in mind that it will end up containing the entire Java JVM and all its dependencies, along with the application and all its files and data.
Basically, though:
$ mkdir my_jail
$ cd my_jail
2. Copy or mount the Java JVM
You’ll need to locate the JVM and either copy it, or create a bind
mount, into the jail directory. I’ll only describe copying here. Using a
bind mount saves space, but it creates the additional complexity that
you’ll need to create the mount as root.
On my installation, the JVM is at
/usr/lib/jvm//usr/lib/jvm/java-17-openjdk-amd64/. So I’ll
copy the whole thing into the jail:
$ mkdir java
$ cp -ax /usr/lib/jvm//usr/lib/jvm/java-17-openjdk-amd64/* java/
You might think you could now run Java under chroot like
this:
$ sudo chroot . /java/bin/java
If you try, you’ll get a “command not found” error, even though the executable is most definitely there. The reason is that it doesn’t have access to the low-level system libraries it needs, because they’re all outside the jail.
3. Fix all the missing dependencies
To resolve this problem, we need to copy into the jail all the
missing libraries, maintaining the relevant directory structure. How do
we know which libraries we need? Well, we don’t. But ldd
will give us a starting point.
$ ldd /usr/bin/java
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f09ed2bb000)
libjli.so => not found
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f09ed2b6000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f09ed2b1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f09ed0cf000)
/lib64/ld-linux-x86-64.so.2 (0x00007f09ed2fe000)
So libz.so.1 is in /lib/x86_64-linux-gnu
(on my Linux installation). I’ll need to replicate that structure in the
jail:
$ mkdir lib/x86_64-linux-gnu/
$ cp /lib/x86_64-linux-gnu/libz.so.1 lib/x86_64-linux-gnu/
And repeat for all the other missing libraries.
What about libjli.so? Oddly, the absence of
libjli.so doesn’t seem to be a problem when running
java normally, but it is a problem in the chroot
jail. This library is part of the JVM, but the java command
doesn’t find it in its usual location. What works for me is the find
this library in the java/ directory, and copy it to
lib/x86_84-linux-gnu. As I said, though, this might not be
how your own system is set up, and you might have to experiment.
I found that, even with all the libraries identified by
ldd taken care of, java still won’t start,
because of other libraries that (I presume) the JVM loads by name at
runtime. These include, for example, libstdc++.so.6.
Happily, the error messages are clear, so it’s just a case of finding
these libraries outside the jail, and copying them inside, maintaining
the directory structure.
Eventually the JVM starts up, but not for long: it fails because it can’t find one of its own files:
Exception in thread "main" java.lang.InternalError: Error loading java.security file
at java.base/java.security.Security.initialize(Security.java:106)
at java.base/java.security.Security$1.run(Security.java:84)
...
Although it isn’t obvious, what it’s complaining about is files under
/etc/java/security, which will also need to be replicated
in the chroot jail. Since there aren’t that many, I find it easiest just
to copy the whole of /etc/java:
$ mkdir etc
$ cp -ax /etc/java etc/
4. Install the Java program
It should be obvious that, not only do we need a working Java JVM, we
also need the application we’re going to run. This might take the form a
Java JAR file, or collection of .class files, or whatever.
All need to be replicated.
5. Run the Java program
If the program takes the form of a JAR file, and it’s in the top level directory of the jail then, finally, we can run it like this:
$ sudo chroot --user nobody:nogroup . java -jar /myprogram.jar
And now we have (hopefully) the desired result: the Java server
running as user nobody, with access restricted to the
chroot Jail.
Of course, all the files needed by the Java server will also need to be in the jail directory and references to those files need to be relative to the new top-level directory. Getting this to work means either
- Replicating the structure of the program’s data within the chroot jail, or
- Configuring the application so that file locations can be configured at runtime.
I prefer the second approach and, because I use chroot
all the time, I’ve gotten used to writing my own code on the
understanding that I’ll be using it. This might mean no more than
providing a way for the program to read its file locations from a
configuration file, whose location can be specified on the command
line.
Closing remarks
I can’t promise that the method I’ve described here will work for every Java application. I’ve mostly used it for applications I’ve written myself, so I know the things that are likely to go wrong, and how to avoid problems. I’m not sure I’d want to use this technique with a really substantial Java application that I didn’t write, because I probably wouldn’t be able to interpret the error messages – if it even produces any.
It’s also worth keeping in mind that, although a “chroot jail” will sandbox an application at the filesystem level, it won’t protect you from unauthorized network access, if an intruder manages to subvert your application. For this you need strong firewall policies as a minimum.
Have you posted something in response to this page?
Feel free to send a webmention
to notify me, giving the URL of the blog or page that refers to
this one.


