runj is a proof-of-concept and the implementation has not been evaluated for its security. Do not use runj on a production system. Do not run workloads inside runj that rely on a secure configuration.
With that said, this document attempts to describe the security-related choices that have been made in runj.
runj makes use of a state directory located at /var/lib/runj. Directories for
individual jails exist underneath this one (in /var/lib/runj/jails/<id>) and
contain runj's own state file (state.json), a copy of the OCI configuration
provided in the bundle (config.json, plus runj.ext.json if present), and the
exec.fifo used to synchronize create and start.
Jails are identified by a name and an ID (JID). runj uses the user-supplied ID parameter as the jail's name and receives an automatically-assigned JID.
Jails are created with the "persist" parameter set, which runj passes directly
to the jail_set(2) syscall. This allows jails to exist without any running
processes.
runj mounts exactly the filesystems described in the OCI configuration's
mounts; it does not inject any mounts of its own. The example specification
generated by runj demo spec includes a devfs mount with the ruleset=4
ruleset (equivalent to devfsrules_jail), which allows basic devices like
null, random, and STDIO to be available inside the jail. (Some tools like
ps have a dependency on /dev/null to function.)
runj manages jails by invoking the FreeBSD jail syscalls directly
(jail_set(2), jail_get(2), jail_attach(2), and jail_remove(2)). It does,
however, rely on a few host userland utilities:
mount(8)to mount and unmount filesystems (the Go runtime does not implement mounting on FreeBSD).ifconfig(8)to move VNet interfaces into and out of a jail.ps(1)(run outside the jail) to enumerate processes and determine whether a jail is still running.jexec(8)(along withkill(1)inside the jail) to implementrunj kill; see "Inside the jail" below.
The default behaviors of these utilities are used in runj.
runj kill makes use of the kill(1) command inside the jail's rootfs; if this
command does not exist (or is not functional), runj kill will not work. If
the kill command has been replaced by a malicious binary, invoking runj kill
will cause that binary to run instead of the normal kill command.