Sandboxing Firefox With Bubblewrap

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:

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).