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.
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.
Flatpak uses Bubblewrap (bwrap) as its sandbox primitive. Bubblewrap is a small C binary that creates Linux namespace containers without requiring root.
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:
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.
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
# ...
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.
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>
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.
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.
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 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.
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
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.
.flatpak-info File: Sandbox Self-InspectionEvery 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 come from two sources: the app’s manifest (what the developer declared) and overrides (what the user has modified).
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.
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.
--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 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.
The review process for Flathub submissions:
Notable review requirements:
io.github.username.AppName)--filesystem=host for non-file-manager apps)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.
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
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.
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.
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.
Tracing the full execution path for flatpak run org.gimp.GIMP:
~/.local/share/flatpak/app/org.gimp.GIMP/... to find the apporg.gnome.Platform/x86_64/45bwrap arguments defining the sandbox filesystemxdg-desktop-portal on D-Bus, registers the sessionFLATPAK_ID, XDG_RUNTIME_DIR, XDG_DATA_HOME, HOME (pointing to ~/.var/app/org.gimp.GIMP).flatpak-info to /run/user/<uid>/flatpak/app/org.gimp.GIMP/bwrap with the full argument listbwrap exec()s /app/bin/gimp-2.10Portal calls during GIMP’s life:
org.freedesktop.portal.FileChooser.OpenFile() → native picker shown → file descriptor returnedFileChooser.SaveFile() → native save dialogOpenURI → your browser opensWhen GIMP exits, bwrap tears down the namespace and all bind mounts disappear.
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