Skip to main content

Custom Application Sandbox

I’ve been using Fedora Silverblue as my daily driver for a while now. Over time, I’ve fully adopted Flatpak, which I really enjoy using alongside Flatseal. I particularly appreciate how Flatpak gives each application its own isolated directory structure, making backups incredibly straightforward.

However, I eventually hit a wall when it came to proprietary GPU drivers for workloads like media decoding, 3D rendering, and running local LLMs. In these scenarios, Flatpak’s runtime abstraction can sometimes become a liability.

While this can sometimes be solved by enabling the Flathub repository or installing custom packages, I wanted to avoid that route. After comparing various alternatives, I decided to write my own lightweight application launcher.

The Problem

Flatpak achieves portability by bundling apps inside standardized runtime environments (like the Freedesktop or GNOME runtimes). While this solves the “dependency hell” of the 2000s, it introduces a subtle friction point for heavy graphics workloads. The host system runs a specific kernel, Mesa build, or proprietary NVIDIA driver stack. Flatpak attempts to bridge this by downloading matched runtime extensions (e.g., org.freedesktop.Platform.GL.default), but version drift, Vulkan layer mismatches, and synchronization quirks still occur.

Where Existing Tools Fall Short

Before writing my own launcher, I surveyed the common options:

  • Flathub: Installing GPU drivers directly from Flathub can theoretically enable GPU support for apps like Blender. However, I couldn’t find an easy way to prevent Flatpak from automatically pulling in other packages from the repo. Normally, enabling a repository implies trusting everything in it, which I wanted to avoid.
  • Custom Flatpak Packages: Packaging arbitrary binary distributions into a custom Flatpak would be nice, allowing me to use standard Flatseal configurations and directory structures. Unfortunately, this requires packaging host-system libraries and keeping them constantly in sync—a non-trivial and highly annoying process.
  • Firejail: Relies on setuid-root binaries (increasing the attack surface) and uses complex, brittle profile formats that are easy to misconfigure.
  • Bubblejail / BubbleBox: These are great community efforts built on Bubblewrap. However, they either lean too heavily into desktop GUI managers or create fully isolated home directory trees rather than standard XDG state management. Furthermore, they lack the common, coarse-grained toggles (e.g., a simple switch for network or GPU access) that I wanted. That said, I did eventually borrow some design elements from them.
  • Raw Shell Scripts: Writing simple bwrap scripts is fast, and it’s actually how I started. But once you have more than a couple of applications, maintaining raw scripts quickly becomes unmanageable.

I needed a sweet spot: a single-file, zero-dependency launcher that takes a simple app.toml file, manages persistent state cleanly, and handles desktop integration.

High-Level Design

Vertically, I want to grant each application a minimal set of permissions and expose only the bare minimum of system information. Horizontally, I need a way to manage all these options effortlessly—recognizing common patterns while still accommodating the differences between individual applications.

One of Flatpak’s best ideas is isolating application data into dedicated subdirectories. I adopted this exact pattern:

  • Every application lives under ~/applications/<app>/.
  • The launcher automatically provisions three isolated subdirectories: config/, data/, and cache/.
  • Inside the sandbox, $HOME is mounted as an empty tmpfs, and standard environment variables (XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME) are mapped directly to these provisioned directories.
  • The application can never see or accidentally pollute your real $HOME.

Configuration

I wanted to eliminate boilerplate and repeated patterns wherever possible. The solution was to create a per-application configuration file that ultimately translates into Bubblewrap flags.

I decided to use TOML. I’d normally reach for JSON, but TOML is now supported in the Python standard library, and Bubblebox uses it to great effect. It just looks cleaner.

For each application, the app.toml looks something like this:

[vars]
bin_dir = "~/opt/my-app"

[app]
executable = "${bin_dir}/binary"
args = ["--no-sandbox"]

[permissions]
gpu     = true
audio   = false
network = false
dbus    = true

[[bind]]
src = "${bin_dir}"

[[bind]]
src = "/mnt/storage/assets"
dest = "/workspace/assets"

[dbus.rules]
"org.freedesktop.Notifications" = "none"

[desktop]
name = "App"
icon = "${bin_dir}/icon.svg"
categories = ["Graphics", "3DGraphics"]

The goal is to provide coarse-grained switches with sensible defaults, while still allowing fine-grained control when necessary.

Extra Notes

  • Installing a new application means manually downloading the binaries and configuring the app.toml. It also means I have to handle updates manually. It’s not a perfect system, but it’s a trade-off I’m willing to make for complete control.
  • The launcher script is written entirely in Python using only the standard library. At its core, it simply translates the TOML configuration into bwrap flags and executes them.
  • Passing a special flag to the launcher will automatically generate and install a .desktop file based on the configuration.

Comments

Popular posts from this blog

A Rocky Migration: Moving from docker-compose to Podman and gVisor

I've been running a few containers for several years. They were all running under rootless Docker with a single user. Initially, I planned to  migrate the containers to VMs , but I couldn't get a stable workflow after about two months of effort. Later,  gVisor caught my attention , and I decided to migrate to Podman with gVisor instead. The new plan is to run each container with  --userns=auto  and use Quadlet for systemd integration. This approach provides better isolation and makes writing firewall rules easier. I'm now close to migrating all my containers. Here are a couple of rough edges I'd like to share. Network Layout I compared  various networking options  and spent a few hours trying the one-interface-per-group approach before giving up. I settled on a single macvlan network and decided to use static IP addresses for my containers. To prevent a randomly assigned IP address from conflicting with a predefined one, I allocated a large IP range for my ...

Fix Google Security Code

Google Security Code (http://g.co/sc) is one type of 2-step verification. This is particularly useful when security keys and passkeys are not available. I have been using it in my LXC containers, until today I found out that it stopped working. It just kept saying "The code is invalid". It is easy to rule out some factors: The code works on other browsers on my laptop. The code works on other devices that are directly connected to the router. So it appears that Google also checks IP addresses besides the security code. Recently I have IPv6 enabled, so most devices that are directly connected to the router have both IPv4 and IPv6 addresses. But  I only enabled IPv4 for my LXC containers. So I guess when a code is generated by device A and used by device B, Google should be able to check that device A and device B are closely located. But in my case, IPv6 address appears on device A but not on device B, which may look suspicious. To fix the problem, I just needed to disable IPv...

Exploring Immutable Distros and Declarative Management

My current server setup, based on Debian Stable and Docker, has served me reliably for years. It's stable, familiar, and gets the job done. However, an intriguing article I revisited recently about Fedora CoreOS, rpm-ostree, and OSTree native containers sparked my curiosity and sent me down a rabbit hole exploring alternative approaches to system management. Could there be a better way? Core Goals & Requirements Before diving into new technologies, I wanted to define what "better" means for my use case: The base operating system must update automatically and reliably. Hosted services (applications) should be updatable either automatically or manually, depending on the service. Configuration and data files need to be easy to modify, and crucially, automatically tracked and backed up. Current Setup: Debian Stable + Docker My current infrastructure consists of several servers, all running Debian Stable. System Updates are andled automatically via unattended-upgrades. Se...