Bubblewrap
Bubblewrap is a lightweight sandbox application developed from Flatpak with a small installation footprint and minimal resource requirements. While the package is named bubblewrap, the actual command-line interface is bwrap(1). Bubblewrap is expected to anchor the sandbox mechanism of the Tor Browser (Linux) in the future. Notable features include support for cgroup/IPC/mount/network/PID/user/UTS namespaces and seccomp filtering. Note that bubblewrap drops all capabilities within a sandbox and that child tasks cannot gain greater privileges than its parent. Notable feature exclusions include the lack of explicit support for blacklisting/whitelisting file paths.
Installation
Install bubblewrap or bubblewrap-gitAUR.
- For information about user_namespaces(7) support in Arch Linux kernels see Security#Sandboxing applications.
- linux-hardened users may need to install bubblewrap-suid instead of the packages mentioned above. See FS#63316 for more information.
Configuration
Bubblewrap can be called directly from the command-line and/or within shell scripts as part of a complex wrapper. Unlike applications such as Firejail which automatically set /var
and /etc
to read-only within the sandbox, Bubblewrap makes no such operating assumptions. It is up to the user to determine which configuration options to pass in accordance to the application being sandboxed. Bubblewrap does not automatically create user namespaces when running with setuid privileges and can accommodate typical environment variables including $HOME
and $USER
.
It is highly recommended that you download strace to see what files the program you are trying to sandbox needs access to.
Usage examples
No-op
A no-op bubblewrap invocation is as follows:
$ bwrap --dev-bind / / bash
This will spawn a Bash process which should behave exactly as outside a sandbox in most cases. If a sandboxed program misbehaves, you may want to start from the above no-op invocation, and work your way towards a more secure configuration step-by-step.
nobody
if the owner or group is not the current one, which suggests running some program like sudo
will not work properly.Bash
Create a simple Bash sandbox:
- Determine available kernel namespaces
$ ls /proc/self/ns cgroup ipc mnt net pid user uts
user
indicates that the kernel has exposed support for user namespaces with CONFIG_USER_NS=y
- Bind as read-only the entire host
/
directory to/
in the sandbox - Create a new user namespace and set the user ID to
256
and the group ID to512
$ bwrap --ro-bind / / --unshare-user --uid 256 --gid 512 bash bash-4.4$ id uid=256 gid=512 groups=512,65534(nobody) bash-4.4$ ls -l /usr/bin/bash -rwxr-xr-x 1 nobody nobody 811752 2017-01-01 04:20 /usr/bin/bash
dhcpcd
Create a simple dhcpcd sandbox:
- Determine available kernel namespaces
$ ls /proc/self/ns cgroup ipc mnt net pid uts
user
indicates that the kernel has been built with CONFIG_USER_NS=n
or is user namespace restricted.- Bind as read-write the entire host
/
directory to/
in the sandbox - Mount a new devtmpfs filesystem to
/dev
in the sandbox - Create new IPC and control group namespaces
- Create a new UTS namespace and set
dhcpcd
as the hostname
# /usr/bin/bwrap --bind / / --dev /dev --unshare-ipc --unshare-cgroup --unshare-uts --hostname dhcpcd /usr/bin/dhcpcd -q -b
Unbound
Create a more granular and complex Unbound sandbox:
- Bind as read-only the system
/usr
directory to/usr
in the sandbox - Create a symbolic link from the system
/usr/lib
directory to/lib64
in the sandbox - Bind as read-only the system
/etc
directory to/etc
in the sandbox - Create empty
/var
and/run
directories within the sandbox - Mount a new devtmpfs filesystem to
/dev
in the sandbox - Create new IPC and PID and control group namespaces
- Create a new UTS namespace and set
unbound
as the hostname
# /usr/bin/bwrap --ro-bind /usr /usr --symlink usr/lib /lib64 --ro-bind /etc /etc --dir /var --dir /run --dev /dev --unshare-ipc --unshare-pid --unshare-cgroup --unshare-uts --hostname unbound /usr/bin/unbound -d
unbound.service
Desktop
Leverage Bubblewrap within desktop entries:
- Bind as read-write the entire host
/
directory to/
in the sandbox - Re-bind as read-only the
/var
and/etc
directories in the sandbox - Mount a new devtmpfs filesystem to
/dev
in the sandbox - Create a tmpfs filesystem over the sandboxed
/run
directory - Disable network access by creating new network namespace
[Desktop Entry] Name=nano Editor Exec=bwrap --bind / / --dev /dev --tmpfs /run --unshare-net st -e nano -o . %f Type=Application MimeType=text/plain;
--dev /dev
is required to write to /dev/pty
- Example MuPDF desktop entry incorporating a
mupdf.sh
shell wrapper:
[Desktop Entry] Name=MuPDF Exec=mupdf.sh %f Icon=application-pdf.svg Type=Application MimeType=application/pdf;application/x-pdf;
mupdf.sh
is located within your executable PATH e.g. PATH=$PATH:$HOME/bwrap
MuPDF
The power and flexibility of bwrap is best revealed when used to create an environment within a shell wrapper:
- Bind as read-only the host
/usr/bin
directory to/usr/bin
in the sandbox - Bind as read-only the host
/usr/lib
directory to/usr/lib
in the sandbox - Create a symbolic link from the system
/usr/lib
directory to/lib64
in the sandbox - Create a tmpfs filesystem overlaying
/usr/lib/gcc
in the sandbox- This effectively blacklists the contents of
/usr/lib/gcc
from appearing in the sandbox
- This effectively blacklists the contents of
- Create a new tmpfs filesystem as the
$HOME
directory in the sandbox - Bind as read-only an
.Xauthority
file and Documents directory into the sandbox- This effectively whitelists the
.Xauthority
file and Documents directory with recursion
- This effectively whitelists the
- Create a new tmpfs filesystem as the
/tmp
directory in the sandbox - Whitelist the X11 socket by binding it into the sandbox as read-only
- Clone and create private containers for all namespaces supported by the running kernel
- If the kernel does not support non-privileged user namespaces, skip its creation and continue
- Do not place network components into a private namespace
- This allows for network access to follow URI hyperlinks
#!/bin/sh #~/bwrap/mupdf.sh (exec bwrap \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib /usr/lib \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/gcc \ --tmpfs $HOME \ --ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --ro-bind $HOME/Documents $HOME/Documents \ --tmpfs /tmp \ --ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \ --unshare-all \ --share-net \ /usr/bin/mupdf "$@")
$ bwrap \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib /usr/lib \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/gcc \ --tmpfs $HOME \ --ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --ro-bind $HOME/Desktop $HOME/Desktop \ --tmpfs /tmp \ --ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \ --unshare-all \ --share-net \ /usr/bin/sh bash-4.4$ ls -AF .Xauthority Documents/
Perhaps the most important rule to consider when building a bubblewrapped filesystem is that commands are executed in the order they appear. From the MuPDF example above:
- A tmpfs system is created followed by the bind mounting of an
.Xauthority
file and a Documents directory:
--tmpfs $HOME \ --ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --ro-bind $HOME/Documents $HOME/Documents \
bash-4.4$ ls -a . .. .Xauthority Desktop
- A tmpfs filesystem is created after the bind mounting of
.Xauthority
and overlays it so that only the Documents directory is visible within the sandbox:
--ro-bind $HOME/.Xauthority $HOME/.Xauthority \ --tmpfs $HOME \ --ro-bind $HOME/Desktop $HOME/Desktop \
bash-4.4$ ls -a . .. Desktop
p7zip
Applications which have not yet been patched against known vulnerabilities constitute prime candidates for bubblewrapping:
- Bind as read-only the host
/usr/bin/7za
executable path to the sandbox - Create a symbolic link from the system
/usr/lib
directory to/lib64
in the sandbox - Blacklist the sandboxed contents of
/usr/lib/modules
and/usr/lib/systemd
with tmpfs overlays - Mount a new devtmpfs filesystem to
/dev
in the sandbox - Bind as read-write the host
/sandbox
directory to the/sandbox
directory in the sandbox-
7za will only run in the host
/sandbox
directory and/or its subdirectories when called from the shell wrapper
-
7za will only run in the host
- Create new cgroup/IPC/network/PID/UTS namespaces for the application and its processes
- If the kernel does not support non-privileged user namespaces, skip its creation and continue
- Creation of a new network namespace prevents the sandbox from obtaining network access
- Add a custom or an arbitrary hostname to the sandbox such as
p7zip
- Unset the
XAUTHORITY
environment variable to hide the location of the X11 connection cookie- 7za does not need to connect to an X11 display server to function properly
- Start a new terminal session to prevent keyboard input from escaping the sandbox
#!/bin/sh #~/bwrap/pz7ip.sh (exec bwrap \ --ro-bind /usr/bin/7za /usr/bin/7za \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/modules \ --tmpfs /usr/lib/systemd \ --dev /dev \ --bind /sandbox /sandbox \ --unshare-all \ --hostname p7zip \ --unsetenv XAUTHORITY \ --new-session \ /usr/bin/7za "$@")
bwrap \ --ro-bind /usr/bin/7za /usr/bin/7za \ --ro-bind /usr/bin/ls /usr/bin/ls \ --ro-bind /usr/bin/sh /usr/bin/sh \ --symlink usr/lib /lib64 \ --tmpfs /usr/lib/modules \ --tmpfs /usr/lib/systemd \ --dev /dev \ --bind /sandbox /sandbox \ --unshare-all \ --hostname p7zip \ --unsetenv XAUTHORITY \ --new-session \ /usr/bin/sh bash: no job control in this shell bash-4.4$ ls -AF dev/ lib64@ usr/ bash-4.4$ ls -l /usr/lib/modules total 0 bash-4.4$ ls -l /usr/lib/systemd total 0 bash-4.4$ ls -AF /dev console full null ptmx@ pts/ random shm/ stderr@ stdin@ stdout@ tty urandom zero bash-4.4$ ls -A /usr/bin 7za ls sh
Firefox
Network facing applications with large surface attack areas are also ideal candidates to be bubblewrapped:
- Transmission included in the sandbox to launch with magnet and torrent links
- Example wrap supports audio (PulseAudio) and printing (CUPS/Avahi) under GNOME (Wayland)
- Paths in
~/.config/transmission/settings.json
should reflect the--setenv HOME
variable
- Paths in
- Full paths are used to allow for keyboard bindings in environments which do not support variable expansion.
- WebRenderer and hardware (accelerated) compositing support included
bwrap \ --symlink usr/lib /lib \ --symlink usr/lib64 /lib64 \ --symlink usr/bin /bin \ --symlink usr/bin /sbin \ --ro-bind /usr/lib /usr/lib \ --ro-bind /usr/lib64 /usr/lib64 \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib/firefox /usr/lib/firefox \ --ro-bind /usr/share/applications /usr/share/applications \ --ro-bind /usr/share/gtk-3.0 /usr/share/gtk-3.0 \ --ro-bind /usr/share/fontconfig /usr/share/fontconfig \ --ro-bind /usr/share/icu /usr/share/icu \ --ro-bind /usr/share/drirc.d /usr/share/drirc.d \ --ro-bind /usr/share/fonts /usr/share/fonts \ --ro-bind /usr/share/glib-2.0 /usr/share/glib-2.0 \ --ro-bind /usr/share/glvnd /usr/share/glvnd \ --ro-bind /usr/share/icons /usr/share/icons \ --ro-bind /usr/share/libdrm /usr/share/libdrm \ --ro-bind /usr/share/mime /usr/share/mime \ --ro-bind /usr/share/X11/xkb /usr/share/X11/xkb \ --ro-bind /usr/share/icons /usr/share/icons \ --ro-bind /usr/share/mime /usr/share/mime \ --ro-bind /etc/fonts /etc/fonts \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --ro-bind /usr/share/ca-certificates /usr/share/ca-certificates \ --ro-bind /etc/ssl /etc/ssl \ --ro-bind /etc/ca-certificates /etc/ca-certificates \ --dir /run/user/"$(id -u)" \ --ro-bind /run/user/"$(id -u)"/pulse /run/user/"$(id -u)"/pulse \ --ro-bind /run/user/"$(id -u)"/wayland-1 /run/user/"$(id -u)"/wayland-1 \ --dev /dev \ --dev-bind /dev/dri /dev/dri \ --ro-bind /sys/dev/char /sys/dev/char \ --ro-bind /sys/devices/pci0000:00 /sys/devices/pci0000:00 \ --proc /proc \ --tmpfs /tmp \ --bind /home/example/.mozilla /home/example/.mozilla \ --bind /home/example/.config/transmission /home/example/.config/transmission \ --bind /home/example/Downloads /home/example/Downloads \ --setenv HOME /home/example \ --setenv GTK_THEME Adwaita:dark \ --setenv MOZ_ENABLE_WAYLAND 1 \ --setenv PATH /usr/bin \ --hostname RESTRICTED \ --unshare-all \ --share-net \ --die-with-parent \ --new-session \ /usr/bin/firefox
Enhancing privacy
- Further restrictions can be made by removing specific entries
- Remove the following entry to remove audio support:
--ro-bind /run/user/"$(id -u)"/pulse /run/user/"$(id -u)"/pulse \
-
/sandbox
represents an arbitrary location defined by the user to hold desired profile information- This allows for the use of a sanitized profile copied into
/sandbox
via a script/cron job or manually e.g.
- This allows for the use of a sanitized profile copied into
$ cp -pR ~/.mozilla /sandbox/
The location can be a network share, a USB mount, or a local filesystem or ramfs/tmpfs location
- Set
/home/r
to obscure the actual/home/example
- Set new user ID and group ID values
/etc/passwd
and
/etc/groups
.bwrap \ .... --bind /sandbox/.mozilla /home/r/.mozilla \ --bind /sandbox/Downloads /home/r/Downloads \ ... --setenv HOME /home/r \ ... --uid 200 --gid 400 \ ... /usr/bin/firefox --no-remote --private-window
Chromium
A simple chromium sandbox on wayland and with pipewire:
bwrap \ --symlink usr/lib /lib \ --symlink usr/lib64 /lib64 \ --symlink usr/bin /bin \ --symlink usr/bin /sbin \ --ro-bind /usr/lib /usr/lib \ --ro-bind /usr/lib64 /usr/lib64 \ --ro-bind /usr/bin /usr/bin \ --ro-bind /etc /etc \ --ro-bind /usr/lib/chromium /usr/lib/chromium \ --ro-bind /usr/share /usr/share \ --dev /dev \ --dev-bind /dev/dri /dev/dri \ --proc /proc \ --ro-bind /sys/dev/char /sys/dev/char \ --ro-bind /sys/devices /sys/devices \ --ro-bind /run/dbus /run/dbus \ --dir "/run/user/$(id -u)" \ --ro-bind "/run/user/$(id -u)/wayland-1" "/run/user/$(id -u)/wayland-1" \ --ro-bind "/run/user/$(id -u)/pipewire-0" "/run/user/$(id -u)/pipewire-0" \ --ro-bind "/run/user/$(id -u)/pulse" "/run/user/$(id -u)/pulse" \ --tmpfs /tmp \ --dir $HOME/.cache \ --bind $HOME/.config/chromium $HOME/.config/chromium \ --bind $HOME/Downloads $HOME/Downloads \ /usr/bin/chromium --enable-features=UseOzonePlatform --ozone-platform=wayland
kernel.unprivileged_userns_clone
sysctl being set to 0. You can set it to 1, however, this is not recommended FS#36969.-
PipeWire:
--ro-bind "/run/user/$(id -u)/pipewire-0" "/run/user/$(id -u)/pipewire-0" \
- If you are not using pipewire then feel free to remove this line
-
--bind $HOME/.config/chromium $HOME/.config/chromium \
mounts your chromium configuration directory in the sandbox as readable and writable -
--bind $HOME/Downloads $HOME/Downloads \
mounts your ~/Downloads directory in the sandbox as readable and writable - This example can be further improved for more isolation.
Skype for Linux
skypeforlinux-stable-binAUR should be started with /usr/share/skypeforlinux/skypeforlinux
instead of /usr/bin/skypeforlinux
, because the latter is just a wrapper script which forks the main process in the background and terminates, which conflicts with the --die-with-parent
bwrap option.
The following example provides these features:
-
env -i
ensures that all environment variables are unset. - Network is shared with the host (
--share-net
),/etc/resolv.conf
is bind-mounted. -
Xorg access: bind the
/tmp/.X11-unix/X0
socket, set$DISPLAY
. -
D-Bus: bind the
/run/user/$UID/bus
socket, set$DBUS_SESSION_BUS_ADDRESS
. - Audio: bind the PulseAudio socket.
- Video: dev-bind the
/dev/video0
device.
The directory on the host where you want to keep the Skype profile can be configured with $HOST_PROFILE_PATH
.
env -i bwrap \ --ro-bind /usr /usr \ --dir /home/r \ --dir /tmp \ --dir /var \ --dir /run/user/$UID \ --proc /proc \ --dev /dev \ --symlink usr/lib /lib \ --symlink usr/lib64 /lib64 \ --symlink usr/bin /bin \ --symlink usr/sbin /sbin \ --symlink ../tmp /var/tmp \ --bind "$HOST_PROFILE_PATH" /home/r/.config/skypeforlinux \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --ro-bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 \ --ro-bind /run/user/$UID/bus /run/user/$UID/bus \ --ro-bind /run/user/$UID/pulse /run/user/$UID/pulse \ --dev-bind /dev/video0 /dev/video0 \ --chdir / \ --unshare-all \ --share-net \ --hostname RESTRICTED \ --die-with-parent \ --new-session \ --setenv PATH /usr/bin \ --setenv HOME /home/r \ --setenv XDG_RUNTIME_DIR "/run/user/$UID" \ --setenv DISPLAY "$DISPLAY" \ --setenv DBUS_SESSION_BUS_ADDRESS "unix:path=/run/user/$UID/bus" \ /usr/share/skypeforlinux/skypeforlinux
Filesystem isolation
To further hide the contents of the file system (such as those in /var
, /usr/bin
and /usr/lib
) and to sandbox even the installation of software, pacman can be made to install Arch packages into isolated filesystem trees.
In order to use pacman for installing software into the filesystem trees, you will need to install fakeroot and fakechroot.
Suppose you want to install the xterm
package with pacman into an isolated filesystem tree. You should prepare your tree like this:
$ MYPACKAGE=xterm $ mkdir -p ~/sandboxes/${MYPACKAGE}/files/var/lib/pacman $ mkdir -p ~/sandboxes/${MYPACKAGE}/files/etc $ cp /etc/pacman.conf ~/sandboxes/${MYPACKAGE}/files/etc/pacman.conf
You may want to edit ~/sandboxes/${MYPACKAGE}/files/etc/pacman.conf
and adjust the pacman configuration used:
- Remove any undesired custom repositories and
IgnorePkg
,IgnoreGroup
,NoUpgrade
andNoExtract
settings that are needed only for the host system. - You may need to remove the
CheckSpace
option so pacman will not complain about errors finding the root filesystem for checking disk space.
Then install the base
group along with the needed fakeroot into the isolated filesystem tree:
$ fakechroot fakeroot pacman -Syu \ --root ~/sandboxes/${MYPACKAGE}/files \ --dbpath ~/sandboxes/${MYPACKAGE}/files/var/lib/pacman \ --config ~/sandboxes/${MYPACKAGE}/files/etc/pacman.conf \ base fakeroot
Since you will be repeatedly calling bubblewrap with the same options, make an alias:
$ alias bw-install='bwrap \ --bind ~/sandboxes/${MYPACKAGE}/files/ / \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --tmpfs /tmp \ --proc /proc \ --dev /dev \ --chdir / '
You will need to set up the locales by editing ~/sandboxes/${MYPACKAGE}/files/etc/locale.gen
and running:
$ bw-install locale-gen
Then set up pacman’s keyring:
$ bw-install fakeroot pacman-key --init $ bw-install fakeroot pacman-key --populate
Now you can install the desired xterm
package.
$ bw-install fakeroot pacman -S ${MYPACKAGE}
If the pacman command fails here, try running the command for populating the keyring again.
Congratulations. You now have an isolated filesystem tree containing xterm
. You can use bw-install
again to upgrade your filesystem tree.
You can now run your software with bubblewrap. command
should be xterm
in this case.
$ bwrap \ --ro-bind ~/sandboxes/${MYPACKAGE}/files/ / \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --tmpfs /tmp \ --proc /proc \ --dev /dev \ --chdir / \ command
Note that some files can be shared between packages. You can hardlink to all files of an existing parent filesystem tree to reuse them in a new tree:
$ cp -al ~/sandboxes/${MYPARENTPACKAGE} ~/sandboxes/${MYPACKAGE}
Then proceed with the installation as usual by calling pacman from bw-install fakechroot fakeroot pacman …
.
Troubleshooting
Using X11
Bind mounting the host X11 socket to an alternative X11 socket may not work:
--bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X8 --setenv DISPLAY :8
A workaround is to bind mount the host X11 socket to the same socket within the sandbox:
--bind /tmp/.X11-unix/X0 /tmp/.X11-unix/X0 --setenv DISPLAY :0
Sandboxing X11
While bwrap provides some very nice isolation for sandboxed application, there is an easy escape as long as access to the X11 socket is available. X11 does not include isolation between applications and is completely insecure. The only solution to this is to switch to a wayland compositor with no access to the Xserver from the sandbox.
There are however some workarounds that use xpra or xephyr to run in a new X11 environment. This would work with bwrap as well.
To test X11 isolation, run 'xinput test <id>' where <id> is your keyboard id which you can find with 'xinput list' When run without additional X11 isolation, this will show that any application with X11 access can capture keyboard input of any other application, which is basically what a keylogger would do.
The optimal solution to eliminate the X11 weak point is to switch to a wayland compositor.
Opening URLs from wrapped applications
When a wrapped IRC or email client attempts to open a URL, it will usually attempt to launch a browser process, which will run within the same sandbox as the wrapped application. With a well-wrapped application, this will likely not work. The approach used by Firejail is to give wrapped applications all the privileges of the browser as well, however this implies a good amount of permission creep.
A better solution to this problem is to communicate opened URLs to outside the sandbox. This can be done using snapd-xdg-open
as follows:
- Install snapd-xdg-open-gitAUR
- On your
bwrap
command line, add:
$ bwrap ... \ --ro-bind /run/user/$UID/bus /run/user/$UID/bus \ --ro-bind /usr/lib/snapd-xdg-open/xdg-open /usr/bin/xdg-open \ --ro-bind /usr/lib/snapd-xdg-open/xdg-open /usr/bin/chromium \ ...
The /usr/bin/chromium
bind is only necessary for programs not using XDG conventions, such as Mozilla Thunderbird.
New session
There is a security issue with TIOCSTI, (CVE-2017-5226) which allows sandbox escape. To prevent this, bubblewrap has introduced the new option '--new-session' which calls setsid(). However this causes some behavioural issues that are hard to work with in some cases. For instance, it makes shell job control not work for the bwrap command.
It is recommended to use this if possible, but if not the developers recommend that the issue is neutralized in some other way, for instance using SECCOMP, which is what flatpak does: https://github.com/flatpak/flatpak/commit/902fb713990a8f968ea4350c7c2a27ff46f1a6c4