I recently stumbled across bubblewrap, an unprivileged sandboxing tool made by the folks over at Project Atomic. Bubblewrap lets you run applications in a minimal container-like environment. The key thing that makes it different from something like Docker, is that the filesystem for the containerized process is built using bind mounts (rather than a filesystem image). This is a good enough for cases where you want additional isolation for a process, but don't need the complexity or overhead of something like Docker or a virtual machine.
The program whose security I'm most concerned about is my web browser. Web browsers are supposed to parse, interpret, and execute untrusted data sent by possibly malicious peers. Browser security has come a long way in recent years, through the use of modern sandboxing mechanisms like process isolation and seccomp-bpf filters. But these mechanisms aren't perfect, and some browser components still need to run unsandboxed. There is a small chance that hackers will find a new sandbox escape in Firefox or Chrome that allows them to access or modify sensitive parts of your home directory, such as your GPG or SSH keys, or your Bitcoin wallet.
A popular existing solution for sandboxing desktop programs like Firefox is Firejail. Firejail is easier to set up, but it's a big, complex C program that requires being setuid root to run. Bubblewrap is a smaller, less complex C program that is unprivileged, meaning it does not need to be setuid root. This article is going to walk through the process of running Firefox in a bubblewrap sandbox. You can find the shell script and XDG desktop file presented here on my GitHub: github.com/eklitzke/firefox-bwrap
Bubblewrapping Firefox
Bubblewrap is in the Fedora 27 repositories, and can be installed using dnf
:
$ dnf install bubblewrap
If you're using another Linux distro, you should be able to compile bubblewrap using the instructions on the bubblewrap GitHub project page.
The bubblewrap binary is actually called bwrap
. After a bit of
experimentation, I came up with the following script that launches a sandboxed
Firefox instance that has proper support with the expected amenities like
PulseAudio integration and accelerated graphics:
#!/bin/bash
#
# Launch Firefox in a bubblewrap container.
# The --dev flag needs to go first, to avoid shadowing /dev/dri
CMD=(bwrap --dev /dev)
# Helper for adding bind mounts.
addbind() {
local flag="$1"
shift 1
for arg in "$@"; do
if [ -L "$arg" ]; then
# Fedora has a unified /usr, Ubuntu and other distros may not. See
# https://fedoraproject.org/wiki/Features/UsrMove for more info.
CMD+=(--symlink "$(readlink "$arg")" "$arg")
else
CMD+=("$flag" "$arg" "$arg")
fi
done
}
# Firefox is allowed to write (and read!) from these directories.
addbind --bind ~/.cache/mozilla ~/.mozilla ~/Downloads
# It's OK for Firefox to communicate with these selected components. Other
# runtime services (like gvfs) are off limits. If you don't care about this
# level of security, replace this part with "addbind --bind /run/user/$UID".
addbind --bind /run/user/"$UID"/{bus,dconf,pulse}
# These things are read-only.
addbind --ro-bind /bin /dev/dri /etc /sys/dev /sys/devices /usr
# Library directories; these are symlinks on Fedora.
addbind --ro-bind /lib{,64}
# Uncomment the line below if you intend to use netns isolation.
# addbind --ro-bind /tmp/.X11-unix
# The remaining arguments.
CMD+=(
--proc /proc
--new-session
--unshare-all
--share-net
--hostname firefox
firefox "$@"
)
# *fingers crossed*
exec "${CMD[@]}"
Once Firefox launches you should still see your old bookmarks and settings. You can check that the process is actually sandboxed using the "Open File" dialog (Ctrl+O). If the sandboxed worked you should only see "Desktop" and "Downloads" in your home directory. If you have problems, check that you've closed all other Firefox processes before running the script.
The --bind
options used in the script give read/write access to the listed
directories. I let Firefox access three parts of my home directory:
~/.cache/mozilla
, ~/.mozilla
, and ~/Downloads
. Firefox itself owns the
first two (they're where things like the browser session, bookmarks, cookies,
etc. are stored), and the "Downloads" directory lets me access downloaded files
from outside the sandbox. The other major parts of the filesystem are mounted
read-only. If you want Firefox to be able to access local files from other
directories (e.g. ~/Pictures
, or from a mounted USB drive) you'll need to add
more --bind
or --ro-bind
options.
GNOME Integration
I use GNOME 3, and want to be able to launch Firefox through my application
tray. To do this, you need to create an XDG desktop
file
in ~/.local/share/applications
. The basic idea is to start with a copy of
/usr/share/applications/firefox.desktop
(the one installed by the Firefox
RPM), and then change the Exec=
lines as necessary. You should be able to use
the firefox-bwrap.desktop
file in my
firefox-bwrap repository.
If you run into issues with the desktop file, there's a good chance that it's
due to gnome-session
having a value for $PATH
that doesn't match your shell.
This is due to the fact that on certain versions of GNOME/Wayland, the GNOME
session does not source .profile
. For more on this problem, see this LWN
article.
Once you have the desktop location in the right place, run the following to regenerate your desktop database:
update-desktop-database ~/.local/share/applications
Now the new application entry should be visible to GNOME. You may also need to log out and back in to get GNOME to see the update.
Network Namespace Isolation
One thing notably missing from this post is isolation of the network (or "netns") namespace. I'm not providing a fully worked example here, because network bridging is complicated and unfortunately there's not a good one-size-fits-all solution.
If you're interested in pursuing this on your own, you need to make at least the following changes:
- Remove
--share-net
from thebwrap
invocation if you want a brand new namespace, or wrap thebwrap
command withip netns exec
to run the container in a namespace you create separately. - Expose the X11 socket to the container. On my laptop, it was sufficient to
read-only bind mount
/tmp/.X11-unix
(a directory containing the actual X11 socket). - To actually communicate with the outside world, you'll need to create some kind of bridged network interface between the container and the host. This is going to be specific to your use case and needs. Therefore, this step is left as an exercise to the reader.
One of the main reasons you might want netns isolation for a browser process is
to ensure that all traffic from the browser is sent over a secure VPN. If that's
your use case, I recommend taking a look at
namespaced-openvpn. This
project was written by a friend of mine, specifically to combat browser privacy
and deanonymization issues. It creates a bridged OpenVPN instance in an isolated
namespace, called protected
by default. You can then wrap the bwrap
command
such that it executes in the protected
netns namespace:
# Run this after you set up namespaced-openvpn.
sudo -E ip netns exec protected sudo -u $USER bwrap --unshare-all --share-net ...
As usual, the best way to give me comments or feedback on this post is using email (or Twitter).