# 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. [→ Capsule home](index.gmi)