app_jail - DtxdF/AppJail GitHub Wiki

Running an application from a FreeBSD jail like a host application

When we install an application on our FreeBSD system, it probably needs dependencies, configuration, and we expect it to coexist well with other applications without causing conflicts. We also expect that this or that other application will not misbehave and will not damage our system or introduce security problems. However, we do not live in an ideal world and these things are likely to happen.

Fortunately FreeBSD provides lightweight virtualization systems also known as jails. Jails solve the problems described above by isolating the applications we need to use. However, not all people feel comfortable creating jails using vanilla jails as it wastes a lot of time, especially configuring other things we may need, such as firewalls, resource limits, network management, and so on, and we will probably need to reproduce the jails on other systems, perhaps with a different configuration. Luckily AppJail exists and provides the automation we need.

In this howto we use a feature of AppJail named Makejail, a simple text file to automate the building of a jail, but the idea is to run an application installed on a jail like an application installed on the host. We will use the Badwolf browser as it is simple and lightweight.

Requirements

Privileges

AppJail needs root access to do its job, but throughout this howto we will try to limit root access to only run the application.

Firewall

Setting up the firewall only takes a moment. Read the AppJail documentation for more details.

Virtual network

Although we can use other networking options that AppJail supports, for simplicity we will use a virtual network. If you don't have a virtual network, don't worry:

# appjail network add -d "Virtual network for testing purposes" testing 10.42.0.0/24
# appjail network list
NAME        NETWORK    CIDR  BROADCAST       GATEWAY    MINADDR    MAXADDR         ADDRESSES  DESCRIPTION
testing     10.42.0.0  24    10.42.0.255     10.42.0.1  10.42.0.1  10.42.0.254     254        Virtual network for testing purposes

testing is the name of the virtual network, 10.42.0.0/24 its address and Virtual network for testing purposes is just a description or a comment, useful for when we are on vacation and do not want a call from a co-worker who needs to know what is the purpose of this virtual network... Feel free to provide anything you need, these values are for simplicity.

Creating the jail

Instead of creating the jail with Badwolf (or another application, remember that Badwolf is just an example to give an idea) from scratch we use a Makejail from the Centralized repository: AppJail-makejails/badwolf. However we need, at least in FreeBSD, to set clear_tmp_X to NO to avoid the removal of x11 sockets in a custom rc.conf(5) file.

mkdir -p /tmp/files/etc
sysrc -f /tmp/files/etc/rc.conf clear_tmp_X=NO

/tmp/files is a directory that mimics the root directory that AppJail will use to copy one or more files. In this case, we create only the /etc directory and put on it our rc.conf(5) file. When we are ready we can create the jail with just one command, but if you don't have the components to create jails yet, don't worry:

appjail fetch

The above will download the components, so you can make a coffee to wait. Once the components have been retrieved and extracted, proceed with the command itself:

appjail makejail -f gh+AppJail-makejails/badwolf -j badwolf \
    -o virtualnet="testing:badwolf default" \
    -o nat \
    -o copydir=/tmp/files \
    -o file=/etc/rc.conf \
    -o x11

With the -f gh+AppJail-makejails/badwolf we indicate to AppJail that we want the Makejail for Badwolf in the Centralized repository. With -j badwolf we indicate the name of the jail. With -o virtualnet="testing:badwolf" default we indicate that we want to use testing as the virtual network for this jail, badwolf (it is just a convention, you can use whatever you want) as the interface name and we make this virtual network the default router using the default option. With the -o nat option we indicate that we want to apply NAT for this jail. With the -o copydir=/tmp/files option we indicate that the /tmp/files directory will be used as copy directory which should mimic the root directory of the file system. With -o file=/etc/rc.conf we indicate that we want to copy /etc/rc.conf which is in the /tmp/files directory to the jail as is. And finally, with -o x11 we indicate that we want to mount the x11 sockets directory in our jail since Badwolf is an x11 application.

# appjail jail list
STATUS  NAME     TYPE  VERSION       PORTS  NETWORK_IP4
DOWN    badwolf  thin  13.1-RELEASE  -      10.42.0.3

The Makejail for Badwolf stops the jail so we need to start it if we want to run Badwolf.

# appjail start badwolf
[00:00:00] [ info  ] [badwolf] Starting badwolf...
access control disabled, clients can connect from any host
ea_badwolf
eb_badwolf
badwolf: created
add net default: gateway 10.42.0.1
defaultrouter: 10.42.0.1 -> 10.42.0.1
# appjail jail list
STATUS  NAME     TYPE  VERSION       PORTS  NETWORK_IP4
UP      badwolf  thin  13.1-RELEASE  -      10.42.0.3

To run Badwolf we can use the badwolf_open custom stage and provide the url argument to open a specific web site.

appjail run -s badwolf_open -p url=http://example.org badwolf

Makejails vs. Command-line

Makejail options can be set either from the command-line or using another Makejail file. A Makejail has advantages over the command-line: it is much easier to transport, it can be included in other Makejails or other Makejails can be included. We can also use Makejail instructions to make our work much easier.

So, for example, let's take the command used in the previous sections:

appjail makejail -f gh+AppJail-makejails/badwolf -j badwolf \
    -o virtualnet="testing:badwolf default" \
    -o nat \
    -o copydir=/tmp/files \
    -o file=/etc/rc.conf \
    -o x11

And convert it to a Makejail:

Makejail:

INCLUDE gh+AppJail-makejails/badwolf

OPTION virtualnet=testing:badwolf default
OPTION nat
OPTION copydir=/tmp/files
OPTION file=/etc/rc.conf
OPTION x11

It may be more useful to separate the networking options into a separate Makejail.

Makejail (v2):

INCLUDE gh+AppJail-makejails/badwolf
INCLUDE options/network.makejail

OPTION copydir=/tmp/files
OPTION file=/etc/rc.conf
OPTION x11

options/network.makejail:

OPTION virtualnet=testing:badwolf default
OPTION nat

To provide another level of modularization, we can use, in other Makejail, miscellaneous options.

Makejail (v3):

INCLUDE gh+AppJail-makejails/badwolf
INCLUDE options/network.makejail
INCLUDE options/misc.makejail

options/misc.makejail:

OPTION copydir=/tmp/files
OPTION file=/etc/rc.conf
OPTION x11

To execute this Makejail we will use the same command but without options:

appjail makejail -j badwolf

Sharing our profile

Simply running an application is not useful when we do not adapt it to our needs. AppJail provides simple ways to configure the application in the jail. We have two options for this application, Badwolf: first, we can copy all the data into a specific directory or completely emulate the behavior of the application and copy the directories using the same path that the application uses. The second, we can mount the directories on the jail. Both offer advantages and disadvantages. If we only copy the directories the application needs, any changes to the files are not reflected on the host, but this method is much simpler. The other method, mounting the directories, provides tracking of which directories are used and provides the other advantage that any changes to the files are reflected both on the jail and on the host.

copy

The Makejail for Badwolf automatically creates a user named badwolf whose home directory is /home/badwolf. We need to create /home/badwolf/.config/badwolf and /home/badwolf/.local/share/badwolf and copy the files from the host to the jail. We also need to change the owner and group of those files after the copy process to match the badwolf user.

INCLUDE gh+AppJail-makejails/badwolf
INCLUDE options/network.makejail
INCLUDE options/misc.makejail

COPY home
CMD --local-jaildir chown -fR 1001:1001 home/badwolf

As you can see, only two instructions have been added to the Makejail used in previous sections. COPY will copy a file or directory from the host to the jail using the exact path tree (see below). CMD with the --local-jaildir parameter will execute a command but on the host inside the jail directory. As mentioned, we need to change the owner and group to match the badwolf user, but remember that UIDs/GIDs in a jail are not the same as in the host, so we must explicitly set them using 1001 as uid and 1001 as gid, all of which correspond to badwolf and badwolf. The tree in the home directory is as follows:

tree -apug home
[drwxr-xr-x root     wheel   ]  home
└── [drwxr-xr-x root     wheel   ]  badwolf
    ├── [drwxr-x--- root     wheel   ]  .config
    │   └── [drwxr-xr-x root     wheel   ]  badwolf
    │       └── [-rw-r----- root     wheel   ]  content-filters.json
    └── [drwxr-xr-x root     wheel   ]  .local
        └── [drwxr-xr-x root     wheel   ]  share
            └── [drwxr-x--- root     wheel   ]  badwolf
                ├── [-rw-r----- root     wheel   ]  bookmarks-test.xbel
                ├── [-rw-r----- root     wheel   ]  bookmarks.xbel
                ├── [-rw-r----- root     wheel   ]  interface.css
                └── [drwxr-x--- root     wheel   ]  webkit-web-extension

8 directories, 4 files

There is no difference between the old command used to build, start and run the jail, just use the same commands:

appjail makejail -j badwolf
appjail start badwolf
appjail run -s badwolf_open -p url=http://example.org badwolf

As you can see, the files load correctly just as they do when the application is running on the host.

mount

Although this option is a bit more complex, when you understand it, you feel comfortable with it.

INCLUDE gh+AppJail-makejails/badwolf
INCLUDE options/network.makejail
INCLUDE options/misc.makejail

CMD --local-jaildir mkdir -p home/badwolf/.config/badwolf
CMD --local-jaildir mkdir -p home/badwolf/.local/share/badwolf
# Unnecessary except when we need the correct permissions when the directories we
# are unmounted.
CMD --local-jaildir chown -f 1001:1001 home/badwolf/.config
CMD --local-jaildir chown -f 1001:1001 home/badwolf/.local/share/badwolf

MOUNT --nomount /var/appjails/badwolf/config /home/badwolf/.config/badwolf
MOUNT --nomount /var/appjails/badwolf/datadir /home/badwolf/.local/share/badwolf
# The current owner and group when the directories are mounted are not the same
# as when they are unmounted, we need to run chown(8) again.
CMD --local chown -fR 1001:1001 /var/appjails/badwolf/config
CMD --local chown -fR 1001:1001 /var/appjails/badwolf/datadir

First, we create the structure used by Badwolf in the home directory of the badwolf user. Although we change the owner and group of the directories before mounting them, it is not necessary in most cases except when we need to use those directories without mounting other directories on them. MOUNT is used to create an entry in the badwolf jail's fstab. With the --nomount we indicate to the MOUNT instruction that we do not want to compile the entries and mount the devices, this is necessary since the badwolf jail has not been started. When the jail starts, those directories will be mounted automatically. Finally, we need to change the owner and group of the /var/appjails/badwolf/config and /var/appjails/badwolf/datadir directories to match the badwolf user and group. The tree structure of the /var/appjails/badwolf directory is as follows:

tree -pug /var/appjails/badwolf
[drwxr-xr-x root     wheel   ]  /var/appjails/badwolf
├── [drwxr-xr-x root     wheel   ]  config
│   └── [-rw-r----- root     wheel   ]  content-filters.json
└── [drwxr-x--- root     wheel   ]  datadir
    ├── [-rw-r----- root     wheel   ]  bookmarks-test.xbel
    ├── [-rw-r----- root     wheel   ]  bookmarks.xbel
    ├── [-rw-r----- root     wheel   ]  interface.css
    └── [drwxr-x--- root     wheel   ]  webkit-web-extension

4 directories, 4 files

Repeat the process again: build, start and run:

appjail makejail -j badwolf
appjail start badwolf
appjail run -s badwolf_open -p url=http://example.org badwolf

No privileges

Up to this point we only run the application with root access but for some users or some environments it will probably be necessary to reduce privileges. We don't want to complicate things, so we need four things: first, security/doas, second, /usr/local/bin/badwolf.appjail a script to run, third, /usr/local/bin/scripts/badwolf-jail.sh and fourth a rule for doas.conf(5). security/doas is used to execute commands as another user. /usr/local/bin/badwolf.appjail has the sole responsability to execute /usr/local/bin/scripts/badwolf-jail.sh as root. /usr/local/bin/scripts/badwolf-jail.sh is used to execute the custom badwolf_open stage of the badwolf jail. Our doas.conf(5) rule allows /usr/local/bin/scripts/badwolf-jail.sh to run as root.

/usr/local/bin/badwolf.appjail:

#!/bin/sh

doas /usr/local/bin/scripts/badwolf-jail.sh "$1"

/usr/local/bin/scripts/badwolf-jail.sh:

#!/bin/sh

appjail run -s badwolf_open \
        -p display="${DISPLAY}" \
        -p url="$1" \
        badwolf

/usr/local/etc/doas.conf:

permit nopass dtxdf-fbsd cmd /usr/local/bin/scripts/badwolf-jail.sh

Now we can run badwolf.appjail as a non-root user and there are no problems:

badwolf.appjail

Multi-user environment

Throughout this howto we have used a single user to run the application. For some users this is sufficient, but for others, especially in a multi-user environment, it is probably not. To make things more complicated we can mimic exactly the behavior of the application, copying or mounting exactly the path the application needs just for the user running the application. We also create the user in the jail if it does not exist with the exact name as in the host. In our doas.conf(5) rules, instead of specifying a single user, we can use a group named appjail.

copy

Makejail:

INCLUDE gh+AppJail-makejails/badwolf
INCLUDE options/network.makejail
INCLUDE options/misc.makejail

CMD --local echo "======> Installing dependencies (host) <======"
PKG --local doas

CMD --local echo "======> Installing scripts <======"

CMD --local mkdir -p /usr/local/bin
CMD --local cp scripts/badwolf.appjail /usr/local/bin
CMD --local mkdir -p /usr/local/bin/scripts
CMD --local cp scripts/rjail.sh /usr/local/bin/scripts
CMD --local cp scripts/ajcopy.sh /usr/local/bin/scripts

CMD --local echo "======> Creating group 'appjail' <======"
CMD --local pw groupadd -n appjail 2> /dev/null || :
CMD --local chown -R root:appjail /usr/local/bin/badwolf.appjail

CMD --local echo "======> Done <======"

CMD --local echo
CMD --local echo "===> Note #1: Add the following rules to your doas.conf(5): <==="
CMD --local echo
CMD --local echo "# badwolf.appjail"
CMD --local echo "permit nopass :appjail cmd /usr/local/bin/scripts/rjail.sh"
CMD --local echo "permit nopass :appjail cmd /usr/local/bin/scripts/ajcopy.sh"

CMD --local echo

CMD --local echo "===> Note #2: Add your user to the 'appjail' group <==="
CMD --local echo

scripts/ajcopy.sh:

#!/bin/sh

# see sysexits(3)
EX_USAGE=64

main()
{
	local _o
	local dst jail src user

	while getopts ":d:j:s:u:" _o; do
		case "${_o}" in
			d)
				dst="${OPTARG}"
				;;
			j)
				jail="${OPTARG}"
				;;
			s)
				src="${OPTARG}"
				;;
			u)
				user="${OPTARG}"
				;;
			*)
				usage
				;;
		esac
	done

	if [ -z "${dst}" -o -z "${jail}" -o -z "${src}" -o -z "${user}" ]; then
		usage
	fi

	# copy
	appjail cmd local "${jail}" \
		cp -a "${src}" "${dst}"
	# owner and group
	appjail cmd jexec "${jail}" \
		chown -R "${user}:${user}" "/${dst}"
}

usage()
{
	echo "usage: ajcopy.sh -d dst -j jail -s src -u user" >&2
	exit ${EX_USAGE}
}

main "$@"

scripts/badwolf.appjail:

#!/bin/sh

# scripts
RJAIL="/usr/local/bin/scripts/rjail.sh"
AJCOPY="/usr/local/bin/scripts/ajcopy.sh"

# config
JAIL="badwolf"
RUNAS="${RUNAS:-doas}"

case "${RUNAS}" in
	sudo|doas|qsudo) ;;
	*) echo "Only \"sudo, doas or qsudo\" are allowed." >&2; exit 1
esac

if ! which -s "${RUNAS}"; then
	echo "Command \"${RUNAS}\" not found." >&2
	exit 1
fi

# user information
USER=`id -un`
REAL_HOMEDIR=`getent passwd "${USER}" 2> /dev/null | cut -d: -f6`
CONFIGDIR="${REAL_HOMEDIR}/.config/badwolf"
DATADIR="${REAL_HOMEDIR}/.local/share/badwolf"

if [ -d "${CONFIGDIR}" ]; then
	${RUNAS} "${RJAIL}" -j "${JAIL}" -u "${USER}" \
		mkdir -p "/home/${USER}/.local/share/badwolf"
	${RUNAS} "${AJCOPY}" -j "${JAIL}" -u "${USER}" \
		-s "${DATADIR}/" -d "home/${USER}/.local/share/badwolf"
fi

if [ -d "${DATADIR}" ]; then
	${RUNAS} "${RJAIL}" -j "${JAIL}" -u "${USER}" \
		mkdir -p "/home/${USER}/.config/badwolf"
	${RUNAS} "${AJCOPY}" -j "${JAIL}" -u "${USER}" \
		-s "${CONFIGDIR}/" -d "home/${USER}/.config/badwolf"
fi

${RUNAS} "${RJAIL}" -j "${JAIL}" -u "${USER}" \
	env DISPLAY="${DISPLAY}" badwolf "$@"

scripts/rjail.sh:

#!/bin/sh

# see sysexits(3)
EX_USAGE=64
EX_NOUSER=67

main()
{
	local _o
	local jail= user=

	while getopts ":j:u:" _o; do
		case "${_o}" in
			j)
				jail="${OPTARG}"
				;;
			u)
				user="${OPTARG}"
				;;
			*)
				usage
				;;
		esac
	done
	shift $((OPTIND-1))

	if [ -z "${jail}" -o -z "${user}" -o $# -eq 0 ]; then
		usage
	fi

	local errcode

	check_user "${jail}" "${user}"

	errcode=$?

	if [ ${errcode} -eq 1 ]; then
		create_user "${jail}" "${user}"
	elif [ ${errcode} -eq 2 ]; then
		echo "An unknown error has been occurred: ${ERRNO}"
		return ${ERRNO}
	fi

	run_cmd "${jail}" "${user}" "$@"
}

check_user()
{
	local jail="$1" user="$2"

	if [ -z "${jail}" -o -z "${user}" ]; then
		echo "usage: check_user jail user" >&2
		return ${EX_USAGE}
	fi

	local errcode

	appjail cmd jexec "${jail}" \
		pw usershow -n "${user}" > /dev/null 2>&1

	errcode=$?

	if [ ${errcode} -eq 0 ]; then
		return 0
	elif [ ${errcode} -eq ${EX_NOUSER} ]; then
		return 1
	else
		ERRNO=${errcode}
		return 2
	fi
}

create_user()
{
	local jail="$1" user="$2"

	if [ -z "${jail}" -o -z "${user}" ]; then
		echo "usage: create_user jail user" >&2
		return ${EX_USAGE}
	fi

	appjail cmd jexec "${jail}" \
		pw useradd -n "${user}" -md "/home/${user}" -s /usr/sbin/nologin || return $?

	return 0
}

run_cmd()
{
	local jail="$1" user="$2" cmd="$3"

	if [ -z "${jail}" -o -z "${user}" -o -z "${cmd}" ]; then
		echo "usage: run_cmd jail user cmd args..." >&2
		return ${EX_USAGE}
	fi

	shift 3 # > jail > user > cmd

	appjail cmd jexec "${jail}" -U "${user}" -- \
		"${cmd}" "$@"
}

usage()
{
	echo "usage: rjail.sh -j jail -u user cmd args..." >&2
	exit ${EX_USAGE}
}

main "$@"

Repeat the process again: build, start but instead of using AppJail explicitly, we can run badwolf.appjail with a non-root user.

mount

Makejail:

INCLUDE gh+AppJail-makejails/badwolf
INCLUDE options/network.makejail
INCLUDE options/misc.makejail

CMD --local echo "======> Installing dependencies (host) <======"
PKG --local doas

CMD --local echo "======> Installing scripts <======"

CMD --local mkdir -p /usr/local/bin
CMD --local cp scripts/badwolf.appjail /usr/local/bin
CMD --local mkdir -p /usr/local/bin/scripts
CMD --local cp scripts/rjail.sh /usr/local/bin/scripts
CMD --local cp scripts/ajnullfs_badwolf.sh /usr/local/bin/scripts

CMD --local echo "======> Creating group 'appjail' <======"
CMD --local pw groupadd -n appjail 2> /dev/null || :
CMD --local chown -R root:appjail /usr/local/bin/badwolf.appjail

CMD --local echo "======> Done <======"

CMD --local echo
CMD --local echo "===> Note #1: Add the following rules to your doas.conf(5): <==="
CMD --local echo
CMD --local echo "# badwolf.appjail"
CMD --local echo "permit nopass :appjail cmd /usr/local/bin/scripts/rjail.sh"
CMD --local echo "permit nopass :appjail cmd /usr/local/bin/scripts/ajnullfs_badwolf.sh"

CMD --local echo

CMD --local echo "===> Note #2: Add your user to the 'appjail' group <==="
CMD --local echo

scripts/badwolf.appjail:

#!/bin/sh

# scripts
RJAIL="/usr/local/bin/scripts/rjail.sh"; export RJAIL
AJNULLFS="/usr/local/bin/scripts/ajnullfs_badwolf.sh"

# config
JAIL="badwolf"
RUNAS="${RUNAS:-doas}"

case "${RUNAS}" in
	sudo|doas|qsudo) ;;
	*) echo "Only \"sudo, doas or qsudo\" are allowed." >&2; exit 1
esac

if ! which -s "${RUNAS}"; then
	echo "Command \"${RUNAS}\" not found." >&2
	exit 1
fi

# user information
USER=`id -un`

${RUNAS} "${AJNULLFS}" -j "${JAIL}" -u "${USER}"

${RUNAS} "${RJAIL}" -j "${JAIL}" -u "${USER}" \
	env DISPLAY="${DISPLAY}" badwolf "$@"

scripts/ajnullfs_badwolf.sh:

#!/bin/sh

# see sysexits(3)
EX_USAGE=64

# scripts
RJAIL="${RJAIL:-/usr/local/bin/scripts/rjail.sh}"

main()
{
	local _o
	local jail user

	while getopts ":j:u:" _o; do
		case "${_o}" in
			j)
				jail="${OPTARG}"
				;;
			u)
				user="${OPTARG}"
				;;
			*)
				usage
				;;
		esac
	done

	if [ -z "${jail}" -o -z "${user}" ]; then
		usage
	fi

	local nro nrofile="/home/${user}/.nro"

	# nro
	if appjail cmd jexec "${jail}" test -f "${nrofile}"; then
		nro=`appjail cmd jexec "${jail}" head -1 "${nrofile}"`
	else
		nro=`jot -r 1 1000 1000000000`
		"${RJAIL}" -j "${jail}" -u "${user}" \
			sh -c "echo ${nro} > ${nrofile}"
	fi

	# user information
	local real_homedir=`getent passwd "${user}" 2> /dev/null | cut -d: -f6`
	local configdir="${real_homedir}/.config/badwolf"
	local datadir="${real_homedir}/.local/share/badwolf"

	# config
	if [ -d "${configdir}" ]; then
		"${RJAIL}" -j "${jail}" -u "${user}" \
			mkdir -p "/home/${user}/.config/badwolf"
		appjail fstab jail "${jail}" \
			set -n ${nro} -d "${configdir}" -m "/home/${user}/.config/badwolf"
	fi
	
	# data
	if [ -d "${datadir}" ]; then
		"${RJAIL}" -j "${jail}" -u "${user}" \
			mkdir -p "/home/${user}/.local/share/badwolf"
		appjail fstab jail "${jail}" \
			set -n $((nro+1)) -d "${datadir}" -m "/home/${user}/.local/share/badwolf"
	fi

	# compile and mount
	appjail fstab jail "${jail}" compile
	appjail fstab jail "${jail}" mount -a

	#
	# We will have to change the owner and group of the newly mounted directories to
	# match the user and group of the jail.
	#
	
	if appjail cmd jexec "${jail}" test -d "${configdir}"; then
		appjail cmd jexec "${jail}" \
			chown -R "${user}:${user}" "/home/${user}/.config/badwolf"
	fi

	if appjail cmd jexec "${jail}" test -d "${datadir}"; then
		appjail cmd jexec "${jail}" \
			chown -R "${user}:${user}" "/home/${user}/.local/share/badwolf"
	fi
}

usage()
{
	echo "usage: ajnullfs_badwolf.sh -j jail -u user" >&2
	exit ${EX_USAGE}
}

main "$@"

scripts/rjail.sh: it is the same as using the copy option.

Repeat the process again: build, start but instead of using AppJail explicitly, we can run badwolf.appjail with a non-root user.

After running the script we will have another user and we will also have more entries in the fstab.

Afterword

Create a Makejail, automate everything.