Small guide: Sandboxing apps with bubblejail in Arch distros

Bubblejail

Bubblejail is a great tool for sandboxing programs in linux. You may install it from the AUR (bubblejail or bubblejail-git). You should also install the slirp4netns package from the main repositories.

It offers a graphical interface for creating and customizing sandboxes, however it is recommended to familiarize yourself with the configuration format, as some important features are unfortunately missing from the GUI app.

To create a new profile, you may do so from the GUI, under “Create instance”, or via the CLI: bubblejail create <instance name>. You may select a default profile to base your newly created profile from. These default template profiles are stored under /usr/share/bubblejail/profiles and may also be placed under ~/.config/bubblejail/profiles. You should copy the generic profile to the local config directory and tweak it later into a better template for your sandboxes.

In order to edit an already existing profile with your $EDITOR, you may do so using the command bubblejail edit <profile name>.

To launch your sandbox, use bubblejail run <sandbox name> <command>. If a command is not specified, the default executable will be run if the sandbox is not running. If the instance is already active and a command is specified, it will be run as a separate subprocess inside the sandbox, and if the instance is already active but no command is specified, it will do nothing and exit.

Whenever you create a new profile, a desktop file is also generated which you can also use to launch your sandbox.

Each individual sandbox’s home directory is stored under ~/.local/share/bubblejail/instances/<profile name>/home/, and its configuration is stored under ~/.local/share/bubblejail/instances/<profile name>/services.toml

In this section, We’ll be covering some things that are not easy to figure out and set up solely by relying on Bubblejail’s user friendly GUI application.

D-Bus name

Most apps run fine even if you don’t specify a D-Bus name, but others will break.

To figure this out, you can utilise the --debug-log-dbus option of the run command, e.g.: bubblejail run --debug-log-dbus <instance> <command>. A lot of output is returned, but the requested dbus name should be present if you search for org.freedesktop.DBus.RequestName.

You could also launch the application unsandboxed (preferably in a virtual machine) and use a tool like qdbus or d-spy.

Wayland

While it’s safe to give direct access to the main wayland socket under GNOME, users of other compositors should avoid doing so. This is because except for GNOME, all other compositors as of time of writing implement privileged protocols that are available by default to all clients, meaning that any client by default is allowed to e.g. take full desktop screenshots and read the contents of the clipboard at any time. In order to restrict this, your compositor must support the security context wayland protocol. Currently, KDE, Cosmic, sway, niri, Hyprland and Jay implement it.

Creating a socket in a security context

You can use way-secure to create a socket, which can then be passed to the sandbox. After compiling the program as per the instructions in the repository, you should use s6-ipcserver-socketbinder as suggested per the documentation to bind the socket. The s6 package is available in the AUR.

s6-ipcserver-socketbinder $XDG_RUNTIME_DIR/way-secure way-secure --socket-fd 0

This assumes that the way-secure binary is in your $PATH.

Running this command will create a new wayland socket in the user runtime directory (typically /run/user/1000). You may test that the socket works as intended by launching a terminal through it, e.g.: WAYLAND_DISPLAY=way-secure foot. Inside this terminal, you may try to take a privileged action such as taking a screenshot using grim. If this worked correctly, you should get an error such as: compositor doesn't support wlr-screencopy-unstable-v1.

You should add the command above to your compositor’s autostart, so that the socket is always available.

Sharing the newly created socket with the sandbox

In order to share the socket with the sandbox, you should use the [debug] service. If it’s not already present in your instance, you should add it as follows:

[debug]
raw_bwrap_args = [
    "--bind",
    "/run/user/1000/way-secure",
    "/run/user/1000/wayland-0",
    "--setenv",
    "WAYLAND_DISPLAY",
    "wayland-0",
    "--setenv",
    "XDG_SESSION_TYPE",
    "wayland",
]

You may also find helpful to set some additional variables such as XDG_CURRENT_DESKTOP and XDG_SESSION_DESKTOP accordingly.

Ensure that the [wayland] service is not enabled.

Using XWayland securely

If you need to use X programs such as wine, it’s not recommended to use the [X11] service directly. This is because all X windows running under the same X server can see and send arbitrary inputs to one another. If you use the same X server outside the sandbox you may allow for a sandbox escape. Sharing the X server across sandboxes, while not as bad, still breaks isolation between them. In fact, to be very safe, you may choose to disable XWayland entirely in your compositor.

To use X securely, you may choose to simply run a nested wayland microcompositor, such as gamescope, to launch your app.

Alternatively, you can use xwayland-satellite. It is available in the AUR. In order to use it, you should create a simple script that launches xwayland-satellite first, and then executes your program. Example:

#!/bin/sh
xwayland-satellite > /dev/null 2>&1 &
sleep 0.1
DISPLAY=:0 /usr/bin/<program>

Network

To give access to the network, you should not use the [network] service if possible. Instead, use [slirp4netns].

Audio

You can create restricted pipewire sockets with the pw-container program. Here’s a simple script to run a sandbox, passing a restricted pw socket to it:

#!/bin/bash

SANDBOX=$1

stdbuf -o0 pw-container > /tmp/pwoutput.txt 2>&1 &
sleep 0.1
SOCKET=$(awk '{print $3}' /tmp/pwoutput.txt)
rm /tmp/pwoutput.txt

bubblejail run --debug-bwrap-args bind $SOCKET /run/user/$UID/pipewire-0 -- $SANDBOX

Of course, ensure that the [pipewire] service is not enabled.

Namespace limits

If you have unprivileged namespaces enabled in your kernel configuration, you may want to restrict this in your sandboxed apps to reduce attack surface. In my experience, most apps run fine with this setting:

[namespaces_limits]
user = 1
mount = 0
pid = 0
ipc = 0
net = 0
time = 0
uts = 0
cgroup = 0

For web browsers and electron apps, you may want to be extra cautious and not enable this feature at all, but if you want to, the following configurations are known to work:

Firefox

[namespaces_limits]
user = -1
mount = 0
pid = 1
ipc = -1
net = -1
time = 0
uts = 0
cgroup = 0

Chromium

[namespaces_limits]
user = 4
mount = 0
pid = -1
ipc = 0
net = 1
time = 0
uts = 0
cgroup = 0

Debug section

The [debug] section is currently the only way do things like setting environment variables, sharing files under $HOME as read-only, and allowing access to session dbus interfaces. It is structured as follows:

[debug]
raw_bwrap_args = []
raw_dbus_session_args = []
raw_dbus_system_args = []

You may use bwrap arguments such as bind and and setenv under raw_bwrap_args, and dbus arguments under raw_dbus_session_args.

Below are some examples that you may find useful.

Pass your GTK and Qt config files (if using qt{5,6}ct)

[debug]
raw_bwrap_args = [
    "--ro-bind",
    "/home/<username>/.config/gtk-3.0",
    "/home/<username>/.config/gtk-3.0",
    "--ro-bind",
    "/home/<username>/.config/gtk-4.0",
    "/home/<username>/.config/gtk-4.0",
    "--ro-bind",
    "/home/<username>/.config/qt5ct",
    "/home/<username>/.config/qt5ct",
    "--ro-bind",
    "/home/<username>/.config/qt6ct",
    "/home/<username>/.config/qt6ct",
    ]

It’s worth noting that this will not only give access to your GTK theme files, but also to e.g. information regarding what directories you have bookmarked as well as ssh/sftp server bookmarks. You may want to pass the .css files directly instead of the entire directory.

Make apps that rely on dconf work inside the sandbox

[debug]
raw_bwrap_args = [
    "--setenv",
    "GSETTINGS_BACKEND",
    "keyfile",
    ]

Flatpak also does this. It’s worth noting that it’s a bad idea to give access to the dconf dbus interface itself, as it may result in a sandbox escape.

Set your GTK and Qt themes

[debug]
raw_bwrap_args = [
    "--setenv",
    "GTK_THEME",
    "Breeze",
    "--setenv",
    "QT_QPA_PLATFORMTHEME",
    "qt5ct",
    ]

Disable hardened_malloc in a specific sandbox (if you have it enabled system-wide)

[debug]
raw_bwrap_args = [
    "--ro-bind",
    "/dev/null",
    "/etc/ld.so.preload",
    ]

Give access to the desktop portal for screensharing

[debug]
raw_dbus_session_args = [
    "--talk=org.freedesktop.portal.Desktop",
    ]

Give access to the feral gamemode dbus interface

[debug]
raw_dbus_session_args = [
    "--talk=com.feralinteractive.GameMode",
    ]
5 Likes

Bubblejail author here.

Thank you for writing this guide. I have a few comments.

If the instance is already active, it will do nothing and exit.

If an instance is already running the given arguments will be run as a separate subprocess inside the sandbox. For example, you can run a terminal inside already running sandbox: bubblejail run <sandox_name> alacritty. I added a message in a recent version sandbox is already running and the command will be sent to the instance.

In order to figure this out, you need to launch the application unsandboxed (preferably in a virtual machine) and use a tool like qdbus or d-spy.

There is a --debug-log-dbus option of a run command which enables the D-Bus proxy logging to stdout. Its output is not very straight forward but it should be possible to figure out what names it tries to acquire.

1 Like

Thanks! I updated the guide accordingly.