Using bubblewrap to sandbox applications in Linux

Summary

The objective is to show various flags used in bubblewrap, as well as to create a sandboxed environment using bubblewrap. Using a custom kernel (such as linux-hardened) is necessary, for we shall play around with namespaces. Additionally, we shall also explore primitive graphics using Xorg and Xephyr, but performance is limited. However, the blogpost shall not cover audio playback.

Note: I am using Arch Linux, so your configuration might vary.

Background

What is bubblewrap

You may easily read about bubblewrap from the ArchWiki, but it allows programs to be “sandboxed”, wherein they may only be allowed to read from and write to certain folders (and no more). This enables us to run programs that would cause unwanted modification in our, say, home folder (e.g. programs that may modify the ~/.config folder).

This guide shall allow us to establish our very own

Warning: The assumption is that programs are “safe”, wherein they won’t do privilege escalation by, say, exploiting vulnerabilities in the Kernel. Running untrustworthy code is still unsafe.

Motivation: Why not a separate user account?

The motivation is to create a variable amount of sandboxes for different “profiles”. For example, we can create a profile for work, a profile for personal things, a profile for gaming, etc. These profiles may be arbitrarily created or destroyed.

The only similarity between this and creating a new user account is creating different “home folders”; but, with containers, one can explicitly tell the program to, “hey,you are only allowed to access (some list of folders), and no more!”.

However, this guide does NOT cover audio support, so one is limited to the shell or to basic Xorg. I may update this sometime in the future. For the meantime, check out Firejail.

Prerequisites

Add user namespace by installing linux-hardened

For Arch users, they may easily install the user namespace feature by installing linux-hardened, which you may read in the Security ArchWiki page. The reason why we need to use a hardened kernel is to use the user namespace, among other useful tools you may want to use later.

If you don’t want to use a hardened kernel, you may add CONFIG_USER_NS=y as a build option when compiling your kernel.

After installation, run grub-mkconfig -o /boot/grub.cfg as root to update your GRUB configuration.

Installing bubblewrap

For Arch users, you may install bubblewrap from the official repositories, or compile them from their GitHub page down below. For other distributions, read your local distro’s reference.

Using bubblewrap

Note with firejail

If you have firejail installed, it will create symbolic links in /usr/local/bin linking to firejail. Therefore, if you run, for example, less, instead of running /usr/bin/less, it runs /usr/local/bin/less, which links to /usr/bin/firejail. This might create an error.

A way to alleviate this is to set the PATH environment variable, with greater priority to /usr/bin instead of /usr/local/bin. In this case, other non-affected applications in /usr/local/bin can still be run, while applications only in /usr/local/bin are still available.

RTFM

For those who are more inclined or used to in using the man page, you may do so by running man bwrap. The man page isn’t very descriptive. I wrote this blogpost to describe the

Syntax

bwrap should be run with the following syntax:

$ bwrap [OPTIONS] [COMMAND]

Note that all of [OPTIONS] start with two hyphens. Also note that, when using multiple options, the latter options are given priority instead of the former ones. So, if you want to have read-only access to / but write-access to $HOME, set the read-only flag before setting the write-access flag.

Example 1: Read-only everything

Try running the following:

$ bwrap --dev-bind / / bash

Invoking --dev-bind SRC DEST will cause the SRC to be mounted to DEST in the sandbox, wherein it may be read and written into.Test this out by creating a directory in your current folder by using mkdir, and getting out of the bwrap instance by ^D or exit. You’ll see a newly-created folder in your current one.

Letting bwrap read and write to any file in / is absurd, since that makes the sandboxing useless.

Note: The above command does not necessarily allow the user to, say, remove everything in /usr. bwrap is still subject to the permissions of its parent, i.e. the current user running bwrap. So, if the current user cannot remove /usr, so can the bwrap instance.

Another note: --dev-bind is not entirely useless, of course. We shall be using this later on.

Now, try running the following command:

$ bwrap --ro-bind / / bash

And try running mkdir again. As the name states, --ro-bind SRC DEST binds SRC to DEST, wherein it shall be read-only. This is where the usefulness of sandboxing begins to show itself.

Of course, we can’t do much if we’re read-only to the system, especially if a program we want to run requires reading and writing to /tmp/. We will fix this later.

Example 2: Namespaces

Try running the following command again:

$ bwrap --ro-bind / / bash

Now, try running pidof PROGRAM, where PROGRAM is the proper name of a program currently running (e.g. firefox). Then, try running kill -9 PID, where PID is the result of the command pidof, Despite being in a so-called “sandbox”, we still have access to the process IDs.

Now, try adding the --unshare-pid option, and try doing the same. This time, it will output some errors if you do,

bash: kill: (29619) - No such process
bash: kill: (29460) - No such process
bash: kill: (29399) - No such process
bash: kill: (4354) - No such process
bash: kill: (4205) - No such process

It is important to note that it can (and does) read the running PIDs, but it has no access to them.

Try running the same bwrap command again, and run id. You should get the following, or similar:

bash-4.4$ id
uid=1000(regginator729) gid=1000(regginator729) groups=1000(regginator729),14(uucp),90(network),92(audio),95(storage),100(users),1001(sudo)

I don’t know exactly why, but this doesn’t look good, since it looks like I, in a sandbox, belong to the groups audio, network, and storage.

Now, try adding the --unshare-user option. Try running id, and note that the sandbox doesn’t know which groups I belong to. It effectively isolates those other groups.

bash-4.4$ id
uid=1000(regginator729) gid=1000(regginator729) groups=1000(regginator729),65534(nobody)

Now, instead of just --unshare-user, try adding the options --uid 256 and --gid 512. Try running id, and notice that the sandbox doesn’t know who you are anymore.

Note that you may replace 256 and 512 with any numbers you choose fit, but I chose 256 and 512 since they are okay numbers. Just make sure that, to be safe, check cut -d: -f1,3 /etc/passwd to view a list of users and their uid‘s, and getent group to list all groups and their gid‘s. Try to make sure that your chosen uid and gid do not conflict with the given ones.

Another useful flag is --unshare-net, which shall create a new network namespace (in short, disconnects you from the current network). We will get back to this later.

You may choose to create other namespaces, those which are listed in the manpage. To create all possible namespaces, so that we may not type --unshare-X multiple times, use the --unshare-all flag.

For increased anonymization, you may add the –hostname HOSTNAME flag, wherein you may specify what hostname you wish to use. This requires the –unshare-uts flag, which will be triggered when –unshare-all is set.

Now, the problem with --unshare-all is that it will indiscriminately unshare everything–including Internet access. However, the --share-net flag is available (albeit not being described in the man page) to fix this problem. Remember that latter flags have higher priority than former flags, so you may do --unshare-all --share-net to unshare all namespaces, but keep Internet access.

Example 3: Scripting

Before we can go any further, instead of manually typing our commands in a shell, we could just run a script. This shall be useful once we have learned which flags to use and to not use, and to run our program with our flags easily.

So far, our code is the following:

#!/bin/sh 

bwrap --unshare-all \
 --share-net \
 --ro-bind / / \
 --hostname Wrappd \ 
 --uid 512 --gid 256 \ 
 "$@"

Save it as bwrapped.sh (or some other name), and don’t forget to chmod +x it. Note that I used "$@" as my command, so that I may run ./bwrapped.sh bash or ./bwrapped.sh --dev /dev bash (i.e. I may plug in arguments easily without editing the shell script).

Example 4: Binding directories

Do note that --ro-bind / / is a very useful tool, but, if we make everything read-only, we would be too restricted, especially if a program reads and writes temporary data (either to /tmp or to our home folder, e.g. config files). Let us extend our script.

Let us first outline what we want for our configuration:

  • Choose which folders to bind as read-only (i.e. not the entire root folder)
    • If we bind the entire root folder /, we will have to create a lot of flags to not give the sandbox access to /boot, /root, among many other folders.
  • Bind /usr to its corresponding folder
    • The /usr folder shall be an important folder in the file system, for this contains the executables, libraries, header files, and configuration. Sharing the /usr/share folder is risky; see below for further information.
    • The option responsible for this is --ro-bind /usr /usr
  • Create symbolic links for /bin, /sbin, /lib, /lib64
    • You may view what these files really are, if you want to, by running file /bin /sbin /lib /lib64. You’ll know that these are just symbolic links, for compatibility purposes
    • Bind them by adding options --symlink usr/bin /bin --symlink usr/bin /sbin and --symlink usr/lib /lib --symlink usr/lib /lib64
  • Bind /opt to its corresponding folder
    • The /opt folder is for third-party software outside regular circles. Some installers may put their programs here to avoid discrepancies with /usr/bin.
    • Bind by using the option --ro-bind /opt /opt
  • Bind /etc to its corresponding folder
    • Sharing the /etc folder is risky; see below for further information.
    • The /etc folder is for system-specific configuration. Please note that /etc may contain very sensitive files, such as time zone, pacman server configuration, hosts file, among others.
    • Bind by using the option --ro-bind /etc /etc. For now, let us just do that.
  • Create a devtmpfs file system, and mount it in /dev.
    • We might need access special files, such as /dev/urandom or /dev/null, but we don’t need the program to access /dev/sda or /dev/ttyX, for isolation purposes.
    • The option reserved for this is --dev /dev
  • Have temporary /var and /tmp folders.
    • The /var folder contains system files and other sensitive data that you don’t want to share with the sandbox (such as /var/backups and /var/log which are extremely sensitive!).
    • The /tmp folder is used for most temporary files, which are vital to programs currently running on your Linux box. When running our sandbox, we do not want sandboxed applications looking at potentially revealing /tmp files, and we can’t make /tmp read only either.
    • The option reserved for this is --tmpfs /var --tmpfs /temp
  • Have temporary /run folder
    • Note that other distributions might not have this folder.
    • The /run folder is similar to the /tmp folder, containing primarily temporary files, but note that it contains other, more important, files. This is also the directory where external devices mount to. We don’t want the sandbox touching this folder.
    • Additionally, we need to create a user folder /run/user/$UID, which is similar to a “temporary folder” but whose scope is only for the user.
    • The option for this is --tmpfs /run --dir /run/user/$UID
  • Have an isolated /proc folder
    • The /proc folder is responsible for linking programs with the kernel. To put it bluntly, it deals with processes.
    • Mount one by adding --proc /proc
  • Create a dedicated directory for our new $HOME
    • The home folder is the only directory that our sandboxed programs may store indefinitely. For example, configuration data on our previously run apps. We don’t want our .supertux2 folder getting deleted!
    • We could dedicate a separate folder for our “new” home, for example, $HOME/bubblewrap/env1, and we shall bind that folder to $HOME by using --bind $HOME/bubblewrap/env1 $HOME
    • Note that the home folder is persistent, that is, it stays there even after the sandbox is closed. For those who need it, you may create a temporary filesystem by using --tmpfs $HOME
  • Change $PATH to prioritize /usr/bin
    • I used whatever $PATH I had (by running echo $PATH), disabled those which used $HOME, and prioritized /usr/bin over /usr/local/bin.
    • My configuration is in the shell script. Yours may vary.
  • Copy over $HOME/.Xauthority
    • This file is important when it comes to communicating with an Xorg session.
    • Just bind it using --ro-bind $HOME/.Xauthority $HOME/.Xauthority

Our (current) shell script shall look like this:

#!/bin/sh

bwrap \
 --ro-bind /usr /usr \
 --symlink usr/bin /bin --symlink usr/bin /sbin \
 --symlink usr/lib /lib --symlink usr/lib /lib64 \
 --ro-bind /opt /opt \
 --dev /dev \
 --tmpfs /var \
 --tmpfs /tmp \
 --tmpfs /run --dir /run/user/$UID \
 --ro-bind /etc /etc \
 --proc /proc \
 --bind $HOME/bubblewrap/env1 $HOME \
 --ro-bind $HOME/.Xauthority $HOME/.Xauthority \
 --unshare-all \
 --share-net \
 --setenv PATH /usr/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl \
 "$@"

Note: For those having problems, do not add whitespaces after the \ symbol.

The shell script works as intended. It is as if we have a separate user account, but with additional barriers.

Warning: remember that there are risks to sharing /usr/share and /etc. However many applications would not function without those files. If you are very paranoid with applications reading such files, you may use --tmpfs /usr/share --tmpfs /etc to force the sandbox to not read those files, or you may just use a separate computer or virtual machine.

Another warning: Note that environment variables are preserved. This might be a major risk for those seeking to not identify themselves. Once again, for those seeking anonimity, use a separate computer or a virtual machine.

Note: D-Bus

Before we run the next steps, most applications communicate using some protocol. This interprocess protocol is called D-Bus, and is necessary for many programs, especially on Xorg.

To have our D-Bus instance, let us run export $(dbus-launch). This may be tested by running dbus-monitor, and seeing if there aren’t any errors.

Example 5: Xorg

Before I continue, bubblewrap doesn’t really play well with graphical applications, PulseAudio, and others. If that’s your thing, check out Firejail. I’ll do a blogpost on Firejail some other time. I talked about Firejail in this blogpost.

I first ran an instance of Xorg using Xephyr, wherein it listens to connections to server number 1. Note that this must be run outside the sandbox. The command is as follows:

Xephyr -ac -screen 800x600 +extension RANDR :1

Of course, you may change the dimensions of your screen (or you may do fullscreen by using -fullscreen instead of -screen WxH). Additionally, :1 means run Xephyr and assign it as display 1. If display 1 is taken (and this happens when there’s already an Xorg session at that display), choose another port.

Once that is run, an Xorg instance has already been created in the window.

screen1.png
It’s an Xorg session, but there’s nothing inside yet.

Now, to run applications inside this Xorg session, we need to set the DISPLAY env variable to whatever socket it listens at (in this case, :1). For example, running DISPLAY=:1 okular creates an Okular window inside Xephyr. (Of course, it doesn’t look pretty since we don’t have our window manager yet.)

screen
Not the prettiest window, but it does work! (Hint: try pressing Ctrl+O.)

What we want is to have some desktop manager running inside that Xephyr window. Let us run just that. In our bubblewrap environment, run the following command:

DISPLAY=:1 startlxqt &

It means that we want to run startlxqt with DISPLAY=:1. Of course, you may choose any desktop environment/window manager, but don’t expect everything to work properly. (Running startkde in my environment doesn’t work at all.)

screen.png
And there we go. We now have a fairly stable Xorg session.

You may notice that it takes a long time to “boot up”. I personally ran SuperTux2 in the environment, and its performance is very terrible.

Note that Xorg sometimes doesn’t work properly, and audio doesn’t work at all. Bubblewrap is designed for smaller applications (for example, daemons connected to the network), and not much for graphics and the like. I shall document Firejail in another blogpost.

Further reading

bubblewrap ArchWiki page

bubblewrap GitHub page

Online bubblewrap man page

This article on LWN about user namespaces

For those interested in Linux filesystem hierarchy, and a version for Arch

Advertisements

2 thoughts on “Using bubblewrap to sandbox applications in Linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s