Flatpak Deep Dive: Portal Architecture and the Bubblewrap Sandbox (Part 4)

Posted by Andrew Denner on March 24, 2026 · 26 mins read

Flatpak Deep Dive: Portal Architecture and the Bubblewrap Sandbox

OSTree, User Namespaces, D-Bus Portals, and Why ~/.var/app Is So Large

Andrew Denner · March 2026 · denner.co Part 4 of the Linux Universal Packages series


Flatpak is the one I use daily. It’s also the one I understand well enough to know exactly what it’s doing when it annoys me. This part goes into the Bubblewrap sandbox, user namespaces, how OSTree stores runtimes, and the portal system that most people have seen without knowing what it is.


The Design Philosophy

Flatpak’s design differs from Snap’s in a few fundamental ways worth stating upfront:

Decentralized: Flatpak has no central authority or mandatory store. You can run a Flatpak from any remote. Flathub is the de facto central repository but it’s not baked into the architecture. You can add your own remote, your company’s internal Flatpak repo, or install local .flatpak bundles.

User namespaces, not setuid: Flatpak’s sandbox uses Linux user namespaces rather than setuid binaries (Snap’s snap-confine is setuid). This is a philosophical difference — user namespaces allow sandbox creation without elevated privileges. The tradeoff is more complex setup, and on some systems user namespaces have had security vulnerabilities.

OSTree for runtime management: Runtimes (shared libraries, GTK, Qt) are stored and versioned using OSTree — a content-addressable, git-like filesystem. This is a genuinely novel approach to dependency management.

Portal-mediated resource access: Rather than granting apps direct access to system resources (microphone, camera, files), Flatpak routes access through XDG portals — OS-managed D-Bus services that apply additional policy.


Bubblewrap: The Sandbox Engine

Flatpak uses Bubblewrap (bwrap) as its sandbox primitive. Bubblewrap is a small C binary that creates Linux namespace containers without requiring root.

Linux User Namespaces

The key technology: user namespaces (CLONE_NEWUSER). A user namespace creates a mapping between user IDs inside the namespace and UIDs outside. Inside a user namespace, unprivileged code can simulate being root (UID 0) while the kernel maps that back to a real unprivileged UID.

// Simplified concept of what bwrap does:
clone(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS, ...)
// Creates: new user ns + new mount ns + new pid ns + new ipc ns + new uts ns
// Then writes the UID/GID mapping:
// /proc/<pid>/uid_map: "0 <real_uid> 1"   (inside uid 0 = outside uid $USER)
// /proc/<pid>/gid_map: "0 <real_gid> 1"

This namespace stack is what allows Flatpak to create a sandbox without root. Inside the sandbox:

  • The app sees itself as running as UID 0 in some operations (for filesystem setup)
  • But the kernel knows this maps to your real unprivileged UID
  • Root operations that require real root (loading kernel modules, changing system time) still fail

What bwrap Actually Creates

When you launch a Flatpak application, Flatpak invokes bwrap with a complex set of arguments that define the sandbox’s filesystem. A simplified version:

bwrap \
  --ro-bind /usr/share/flatpak/exports/share/runtime/org.gnome.Platform/x86_64/45/files /usr \
  --ro-bind /app /app \
  --bind /run/user/1000/flatpak/app/org.gimp.GIMP /run/user/1000 \
  --bind /home/user/.var/app/org.gimp.GIMP /home/user \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --unshare-pid \
  --unshare-ipc \
  --new-session \
  -- /app/bin/gimp-2.10

The app’s filesystem view is a construction of bind mounts:

  • /usr is the runtime (GNOME Platform), not the system’s /usr
  • /app is the Flatpak app installation directory (read-only)
  • ~ is a redirected path pointing to ~/.var/app/org.gimp.GIMP (the app’s isolated data dir)
  • /tmp is a fresh tmpfs (isolated)

The app never sees your real /usr, never sees other apps’ data, and its ~ is a sandboxed directory, not your real home.

The Mount Namespace

Inside the mount namespace, Flatpak can set up an arbitrary filesystem hierarchy using bind mounts — mapping paths from the outside namespace into specific locations inside. The kernel enforces that nothing inside can see mounts from outside unless explicitly bound in.

# You can inspect a running Flatpak's mount namespace:
FLATPAK_PID=$(pgrep -f "gimp")
cat /proc/$FLATPAK_PID/mounts | head -20

# Shows the bind-mounted directories that make up the sandbox view
# /var/lib/flatpak/runtime/org.gnome.Platform/.../files on /usr type ext4 (ro,...)
# tmpfs on /tmp type tmpfs
# ...

OSTree: Git for Filesystems

Flatpak uses OSTree to store and distribute runtimes. OSTree is the same technology used in Fedora CoreOS, RHEL for Edge, and some automotive embedded Linux systems. Understanding OSTree explains Flatpak’s storage model.

The Content-Addressable Store

OSTree stores file content as objects keyed by SHA256 hash, just like git stores blobs. Files with identical content are stored once, regardless of how many apps or runtime versions reference them.

The OSTree repository on a Flatpak system:

/var/lib/flatpak/repo/           # System Flatpak repo
~/.local/share/flatpak/repo/     # User Flatpak repo

repo/
├── config                       # Remote configuration
├── refs/
│   └── remotes/
│       └── flathub/
│           └── runtime/
│               └── org.gnome.Platform/x86_64/45  # → <commit hash>
├── objects/
│   ├── aa/                      # Files stored by hash prefix
│   │   └── bb1234...            # Content object
│   └── ...
└── state/

When you install GNOME Platform 45, OSTree downloads and stores each file by its content hash. When GNOME Platform 45 is updated to 45.1, only changed files are downloaded — unchanged files are already in the object store.

# Inspect the OSTree repo:
ostree --repo=/var/lib/flatpak/repo refs
# Shows all installed runtimes and apps as OSTree refs

# See what changed between two commits:
ostree --repo=/var/lib/flatpak/repo diff <old-commit> <new-commit>

Deployment: Checking Out Runtimes

When Flatpak needs to use a runtime, it “deploys” the OSTree checkout — creating a directory containing the runtime files, with OSTree’s hardlink optimization:

/var/lib/flatpak/runtime/org.gnome.Platform/x86_64/45/
└── active/
    ├── deploy              # OSTree metadata
    └── files/              # The actual runtime files
        ├── bin/
        ├── lib/
        └── share/

Critically, OSTree deployments use hardlinks to the object store wherever possible. Multiple runtime versions that share files share those files on disk (same inode). This is why the “500MB runtime” number is misleading — if GNOME Platform 44 and 45 share 80% of their files, the second installation uses much less disk than the first.

# Check actual disk usage with deduplication accounted for:
flatpak disk-usage
# This accounts for shared files; raw du output does not

The du -sh ~/.var/app trick I mention in the talk only shows the app data directory. The runtimes live in /var/lib/flatpak/runtime/ (system install) or ~/.local/share/flatpak/runtime/ (user install). Those are the ones that grow.


XDG Desktop Portals: The Broker System

This is Flatpak’s most architecturally interesting idea. Instead of giving apps direct access to sensitive resources, they communicate through portals — D-Bus services provided by the host desktop environment.

Why Portals Exist

A sandboxed app can’t open /home/user/Documents/ directly. But it can send a D-Bus message to org.freedesktop.portal.FileChooser.OpenFile(). The portal service (running outside the sandbox) shows a native file picker dialog. When the user selects a file, the portal returns a file descriptor to the app. The app never sees the filesystem path — it just gets a file descriptor to read.

This means:

  • The app can only access files the user explicitly chose
  • The portal can apply additional policy (e.g., respecting the user’s sandboxed directory)
  • The file picker looks native because it is native — it’s run by the host, not the app

Portal Types

The XDG portal stack defines over a dozen portal interfaces:

org.freedesktop.portal.FileChooser: Open/save file dialogs. The most commonly used portal. Every time a Flatpak app shows a file chooser, this is what’s happening.

org.freedesktop.portal.OpenURI: Open a URI using the system default handler. When a Flatpak app opens a URL, it asks this portal — which then uses your system’s configured browser.

org.freedesktop.portal.Screenshot: Take a screenshot. The portal shows a confirmation dialog if desired.

org.freedesktop.portal.ScreenCast: Screen recording/sharing (used by Zoom, OBS, Pipewire-based screensharing). The portal negotiates a PipeWire stream rather than giving the app direct /dev/video* access.

org.freedesktop.portal.Camera: Similar — camera access via PipeWire stream, with user confirmation.

org.freedesktop.portal.Notifications: Desktop notifications. Apps can’t write to the notification system directly.

org.freedesktop.portal.Settings: Read desktop settings (color scheme, font, etc.) from the host environment. This is how Flatpak apps know whether to use dark mode.

org.freedesktop.portal.Background: Request permission to run in the background/at startup.

org.freedesktop.portal.Email: Compose an email (opens the default mail client).

org.freedesktop.portal.Secret: Access the keyring/secret service.

The Portal Architecture

Flatpak App (sandboxed)
  │
  │ D-Bus message to portal
  ▼
org.freedesktop.portal.Desktop (xdg-desktop-portal)
  │
  │ Routes to implementation
  ▼
org.freedesktop.impl.portal.gtk  (GNOME implementation)
org.freedesktop.impl.portal.kde  (KDE implementation)
org.freedesktop.impl.portal.wlr  (wlroots compositors)
  │
  │ Shows native UI (file picker, etc.)
  ▼
Result → file descriptor / URI / permission granted
  │
  ▼
Back to sandboxed app

The portal implementation is chosen based on your desktop environment. On GNOME, xdg-desktop-portal-gnome provides native GNOME file pickers. On KDE, xdg-desktop-portal-kde provides KDE dialogs. This is why Flatpak apps can show native-looking file pickers even when running outside their “home” desktop.

# Running portals on your system:
ps aux | grep xdg-desktop-portal
# xdg-desktop-portal (the broker)
# xdg-desktop-portal-gnome (or -kde, -gtk)

# D-Bus introspection of available portal interfaces:
busctl --user introspect org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop

PipeWire and ScreenCast: The Modern Media Story

The ScreenCast portal is worth special attention because it solved a genuine problem. Before PipeWire, sandboxed apps had no good way to do screen capture or audio routing. X11 apps could grab the framebuffer, but Wayland compositors deliberately prevent this (no direct framebuffer access for apps).

The solution: the ScreenCast portal negotiates a PipeWire stream with the compositor. The compositor captures the screen and provides it as a PipeWire media stream. The sandboxed app connects to the PipeWire stream and gets video frames without ever having direct access to the display server.

This is why Flatpak apps (like Zoom’s Flatpak) can do screen sharing on Wayland. It’s also why obs-studio as a Flatpak works with PipeWire screen capture.


The .flatpak-info File: Sandbox Self-Inspection

Every running Flatpak app has a .flatpak-info file visible at /run/user/<uid>/flatpak/app/<app-id>/.flatpak-info. This file describes the app’s sandbox configuration.

# While GIMP is running:
cat /run/user/$(id -u)/flatpak/app/org.gimp.GIMP/.flatpak-info

# [Application]
# name=org.gimp.GIMP
# runtime=runtime/org.gnome.Platform/x86_64/45
#
# [Instance]
# instance-id=1234567890
# app-path=/var/lib/flatpak/app/org.gimp.GIMP/...
# runtime-path=/var/lib/flatpak/runtime/org.gnome.Platform/...
#
# [Context]
# shared=network;ipc
# filesystems=host

This file is used by portal services to verify the app’s identity and determine which permissions it has. A portal can read this to decide whether to grant a request.


Flatpak Permissions: The Full Model

Flatpak permissions come from two sources: the app’s manifest (what the developer declared) and overrides (what the user has modified).

The Manifest Context

In a Flatpak manifest (used to build the app), the developer declares what permissions the app needs:

finish-args:
  - --share=network          # Access the network
  - --share=ipc              # Shared memory IPC (X11 apps need this)
  - --socket=wayland         # Wayland display server
  - --socket=fallback-x11    # X11 fallback
  - --socket=pulseaudio      # PulseAudio/PipeWire audio
  - --device=dri             # GPU/DRI access (hardware rendering)
  - --filesystem=home        # Access home directory
  - --filesystem=host        # Access entire filesystem (dangerous)
  - --talk-name=org.gtk.vfs.daemon  # Talk to this D-Bus service
  - --env=GDK_BACKEND=wayland       # Set environment variable

These permissions are compiled into the Flatpak and stored in the repo. When you install an app, these are the declared permissions.

User Overrides

Users can override permissions in both directions — grant more or restrict more:

# Grant filesystem access that the manifest didn't declare:
flatpak override --user --filesystem=~/Projects org.gimp.GIMP

# Remove a permission the manifest declared:
flatpak override --user --nofilesystem=home org.gimp.GIMP

# Grant access to a specific D-Bus name:
flatpak override --user --talk-name=org.freedesktop.NetworkManager org.example.App

# See all overrides:
flatpak override --user --show org.gimp.GIMP

# Reset all overrides:
flatpak override --user --reset org.gimp.GIMP

Flatseal is a GUI front-end for this override system. It reads the manifest’s declared permissions, shows the current override state, and lets you toggle them. This is what I call “transparent permissions” — you can see exactly what each app can do.

The --filesystem=host Problem

--filesystem=host gives the Flatpak full access to the host filesystem (same as your user). Many apps include this because it’s easier than figuring out which specific paths they need.

# Check which Flatpaks have --filesystem=host:
flatpak info --file-access <app-id>
# Or via Flatseal: look for "All user files" in Filesystem section

When an app has --filesystem=host, the sandbox’s filesystem isolation provides almost no protection. The app can read and write your entire home directory. This is legal in Flatpak — the developer declared it and Flathub allows it with justification — but it’s worth knowing.


Flathub: The Store and the Review Process

Flathub is not part of Flatpak — it’s a community-operated Flatpak remote hosted at https://dl.flathub.org/. But it’s so central to the Flatpak ecosystem that understanding it matters.

Submission and Review

The review process for Flathub submissions:

  1. Developer submits a PR to github.com/flathub/flathub with their manifest
  2. Automated checks verify: manifest syntax, allowed build modules, no vendored binaries, no network access during build
  3. Human reviewers check: claimed permissions are reasonable, sources are legitimate, app ID matches domain control
  4. Initial approval creates the app repository
  5. Future updates are published by the developer (no per-update review after initial approval, but automated checks run)

Notable review requirements:

  • No proprietary bundled binaries in open-source apps (must be built from source)
  • App IDs must be reverse-domain with domain ownership (or use io.github.username.AppName)
  • No excessive permissions without justification (reviewers flag --filesystem=host for non-file-manager apps)
  • Sources must be verifiable (checksums/commits required, no bare URLs)

Proprietary apps (Discord, Spotify, Steam) are allowed on Flathub but go through additional review. They’re typically binary bundles since the source isn’t available.

Verification

Flathub supports developer verification — proving you control the app’s domain or GitHub identity. Verified apps show a checkmark in GNOME Software and other store frontends.

# See Flathub metadata including verification status:
flatpak remote-info flathub org.gimp.GIMP

The Storage Architecture: What Takes Disk Space

Let me be precise about where disk usage comes from:

System installation (/var/lib/flatpak/):
├── repo/                           # OSTree objects (content-addressable)
│   └── objects/                    # Shared file content, hardlinked
├── runtime/                        # Deployed runtimes
│   ├── org.gnome.Platform/         # ~600MB-1GB
│   ├── org.kde.Platform/           # ~600MB (if KDE apps installed)
│   └── org.freedesktop.Platform/   # ~300MB (base for many apps)
└── app/                            # Deployed apps
    ├── org.gimp.GIMP/              # App files (not user data)
    └── org.mozilla.firefox/

User installation (~/.local/share/flatpak/):
└── (Same structure, for user-installed apps)

User data (~/.var/app/):
├── org.gimp.GIMP/
│   ├── .config/                    # App configuration
│   ├── .local/                     # Local app data
│   └── cache/                      # App cache (safe to delete)
└── org.mozilla.firefox/
    └── ...

The OSTree object store deduplication means the real disk usage is usually less than du suggests. For accurate measurement:

# This command accounts for shared objects:
flatpak disk-usage --system
flatpak disk-usage --user

# Output shows "Application Bytes" (real unique bytes) vs "Installed Size" (apparent)

If you have GNOME and KDE apps, you’ll have both GNOME Platform and KDE Frameworks runtimes. These don’t share much — different library stacks. This is the realistic scenario where Flatpak’s storage overhead is highest.


The Sandbox Escape: What Breaks Isolation

Flatpak’s sandbox is not perfect. Known and accepted vectors:

--filesystem=host: As described above — full home directory access means no real isolation. Check your apps.

X11 Forwarding: If an app uses X11 (with --socket=x11 or --socket=fallback-x11), it can keylog other X11 apps and capture screen content. X11 has no per-app isolation. This is a fundamental X11 limitation, not a Flatpak bug. Wayland eliminates this.

--socket=session-bus: Full access to the D-Bus session bus means the app can call any service that’s running on the bus — including other apps. This is a large permission to grant.

PulseAudio socket: Direct access to PulseAudio means the app can potentially record audio from the system or other apps, not just play it.

The --device=all permission: Grants access to all devices in /dev. Avoid.

The Flatpak team is aware of these vectors and generally treats the sandbox as “best effort for GUI desktop apps, not a security boundary for untrusted code.” The security model assumes you trust the app developer — the sandbox mostly protects against bugs and accidental data access, not malicious apps.


Building a Flatpak

Understanding the build system shows you why app sizes are what they are.

# org.example.MyApp.yaml (Flatpak manifest)
app-id: org.example.MyApp
runtime: org.gnome.Platform
runtime-version: '45'
sdk: org.gnome.Sdk
command: myapp

finish-args:
  - --share=network
  - --socket=wayland
  - --socket=fallback-x11
  - --device=dri

modules:
  - name: myapp
    buildsystem: cmake-ninja
    sources:
      - type: git
        url: https://github.com/example/myapp
        commit: abc123...

The build runs inside a Flatpak SDK sandbox (isolated from your system). The app is built against the SDK’s libraries (GNOME SDK 45 = GTK4, GLib, etc.). The final image only includes the app’s unique code — runtime libraries come from the shared runtime at install time.

This is why Flatpak apps that use the shared runtime are often smaller than AppImages: GIMP.flatpak doesn’t bundle GTK. GTK comes from the GNOME Platform runtime shared with every other GNOME app.


What Happens Under the Hood: A Full Flatpak Launch

Tracing the full execution path for flatpak run org.gimp.GIMP:

  1. flatpak CLI reads ~/.local/share/flatpak/app/org.gimp.GIMP/... to find the app
  2. Determines the runtime: org.gnome.Platform/x86_64/45
  3. Resolves permissions from manifest + user overrides
  4. Constructs bwrap arguments defining the sandbox filesystem
  5. Sets up portals: connects to xdg-desktop-portal on D-Bus, registers the session
  6. Sets environment variables: FLATPAK_ID, XDG_RUNTIME_DIR, XDG_DATA_HOME, HOME (pointing to ~/.var/app/org.gimp.GIMP)
  7. Writes .flatpak-info to /run/user/<uid>/flatpak/app/org.gimp.GIMP/
  8. Executes bwrap with the full argument list
  9. Inside the sandbox: mounts are set up, new namespaces created
  10. bwrap exec()s /app/bin/gimp-2.10
  11. GIMP starts inside its sandbox

Portal calls during GIMP’s life:

  • File → Open: GIMP calls org.freedesktop.portal.FileChooser.OpenFile() → native picker shown → file descriptor returned
  • File → Export: FileChooser.SaveFile() → native save dialog
  • Help menu: OpenURI → your browser opens

When GIMP exits, bwrap tears down the namespace and all bind mounts disappear.


The OCI Future: Experimental and Worth Watching

One development worth knowing about as of 2025–2026: Flatpak has experimental support for pulling apps and runtimes from OCI registries — standard Docker Hub-compatible image registries, accessible via Podman or any OCI-compatible toolchain.

# Experimental: add an OCI-based remote
flatpak remote-add --from oci registry.example.com/my-flatpak-repo

If this matures, it could meaningfully change the distribution story. OCI registries are already widely available infrastructure — every major cloud provider runs one, and self-hosted options (Harbor, Gitea’s package registry, etc.) are common. Using them as Flatpak distribution channels would reduce the need for the custom OSTree remote infrastructure, potentially making self-hosted Flatpak distribution much easier.

The OSTree deduplication story also looks different in an OCI world: OCI layers use content-addressable storage similar to OSTree, so the storage efficiency benefits could be preserved.

For now, this is experimental. The primary distribution channel for Flatpak remains OSTree-based remotes with Flathub as the de facto central repo. But it’s an interesting direction, and if you’re building internal tooling or evaluating whether to run a private Flatpak repo, it’s worth checking the current state of OCI support in your target Flatpak version.



Next: Part 5 — vs Docker: When Does a Universal Package Become a Container?

Andrew Denner — denner.co — @adenner