* feat(nixpak): sandbox Steam and Heroic in ~/Gaming idols-ai stops preserving ~/.local/share/Steam, ~/.steam, ~/.config/heroic and their game data, which must be moved into ~/Gaming before deploying. Assisted-by: DeepSeek Harness (deepseek-flash) * refactor(nixpak): replace steam-fix.nu with sandboxed protontricks Assisted-by: DeepSeek Harness (claude-opus-5-5) * fix(nixpak): hide host documents and GTK config from game sandboxes Assisted-by: DeepSeek Harness (claude-opus-5-5) * docs(ai): locate the Steam library under ~/Gaming Assisted-by: DeepSeek Harness (claude-opus-5-5) * fix(umu-game): confine writes to ~/Gaming and keep foreign entries Assisted-by: DeepSeek Harness (claude-opus-5-5) * fix(nixpak): keep Steam and Heroic in the application launcher The wrappers only carried bin/, so neither launcher had a desktop entry after replacing programs.steam. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): give the game sandboxes a fontconfig that sees CJK fonts Steam rendered Chinese as tofu: the FHS layers build their own /etc/fonts, whose config lists only DejaVu and the XDG font directory, so neither the host fonts nor the FHS /usr/share/fonts were ever scanned. Point FONTCONFIG_FILE at a small config that includes /usr/share/fonts, and ship WenQuanYi Zen Hei and Noto Sans CJK in all three FHS layers. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): grant the game launchers a StatusNotifier tray icon Steam and Heroic register a tray icon over D-Bus, and the game sandboxes granted no StatusNotifierWatcher access, so the desktop shell had nothing to show for them. Enable desktopIntegration.tray for all three launchers and teach the policy suites to expect it. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): drop the duplicate host CJK font and tidy the font overrides Steam reads the fonts inside its own FHS layer, so the system-wide wqy_zenhei added for it no longer serves any sandbox. Give gaming-policy its own comment for FONTCONFIG_FILE instead of leaving it under the compat-tool one, make the desktop-base value a default, and note the SNI tray grant in the README table. Assisted-by: DeepSeek Harness (deepseek-flash)
Linux Hardening
Deployed and interactively validated on the Linux desktop host (2026-10-31). Policy and limits: SECURITY.md.
Why these apps are sandboxed
The packaging scope is driven by blast radius and input exposure, not by treating the applications as malicious. These apps parse complex local or remote content, load third-party plugins (OBS), or hold powerful network/session privileges (Remmina); a parser, plugin or session compromise would otherwise inherit the user's home, session D-Bus, devices and network. Each wrapper grants only the ports, devices, sockets, writable directories and desktop portals needed by that app's documented workflow.
Shared boundaries
- Cleared environment; filtered session D-Bus; private
/dev/shm; per-app/tmp. Chromium-family browsers share an app-specific/tmpso external URLs reuse one browser instance. - Read-only
/nix/store; GPU and display; fonts and themes; LAN and Internet. - Host timezone:
TZandTZDIRare passed through from the session environment. - Portals for file chooser, URI opening, notifications, input methods and document exports.
- No whole host home, system D-Bus, SSH agent, raw USB/serial or raw camera. The only device passthrough is a security key's FIDO interface for Firefox/Chromium/Chrome; see Limits. Raw PipeWire is granted only to MuseScore for its ALSA-via-PipeWire audio path; see Limits.
- Host loopback only for Firefox/Chromium; all other apps use pasta-isolated loopback. Chromium is an intentional development exception. Brave Origin and Edge are backup browsers and receive no media grants.
App permissions
Media means audio playback, microphone, Camera and ScreenCast portals. Writable paths are listed
explicitly; other files use the document portal. Every app also has private writable state.
| App | Network | Writable paths | Grants |
|---|---|---|---|
| Firefox | Host loopback | ~/.mozilla, personal files, Trash, Firefox artwork |
Audio, browserpass, MPRIS, idle, FIDO2 key; camera/screen sharing intentionally unavailable |
| Chromium | Host loopback | ~/.config/chromium, personal files |
Media, browserpass, MPRIS, idle, FIDO2 key, v12 Secret |
| Chrome | Isolated | ~/.config/google-chrome, Downloads |
Media, browserpass, MPRIS, idle, FIDO2 key, v12 Secret |
| Brave Origin | Isolated | ~/.config/BraveSoftware/Brave-Origin, Downloads |
v12 Secret |
| Edge | Isolated | ~/.config/microsoft-edge, Downloads |
v12 Secret |
| Isolated | Personal files, private app state | Media, notifications, tray, MPRIS, idle, accessibility | |
| Telegram | Isolated | Personal files, private app state | Media, notifications, tray, MPRIS, idle, accessibility, Ayatana indicators |
| Zoom | Isolated | ~/.zoom, zoomus.conf, zoom.conf, Downloads, Documents/Zoom, private app state |
Media, X11/XWayland, FHS, SNI tray, idle, /dev/video0-1, read-only /sys/class/drm |
| Isolated | Documents, Downloads, app-scoped wechat data, private app state |
Media, Wayland, X11 fallback, SNI tray, file chooser portal | |
| Foliate | None | Documents/Downloads/Pictures read-only, app-scoped state | File chooser, OpenURI, notifications, IME, accessibility |
| Krita | None | Documents/Downloads/Pictures writable, app-scoped state | File chooser, OpenURI, print, inhibit, notifications, IME, accessibility |
| Bambu Studio | Isolated | Documents/Downloads/Pictures writable, app-scoped state | X11/XWayland, PulseAudio, cloud/LAN network, file chooser, notifications, IME, accessibility |
| OrcaSlicer | Isolated | Documents/Downloads/Pictures writable, app-scoped state | X11/XWayland, cloud/LAN network, file chooser, notifications, IME, accessibility |
| Remmina | Isolated | ~/.config/remmina, ~/.local/share/remmina, ~/.remmina, Downloads |
PulseAudio, SSH agent, file chooser, OpenURI, notifications, IME, accessibility |
| OBS Studio | Isolated | ~/.config/obs-studio, Videos/Pictures/Downloads |
ScreenCast, V4L2 camera (/dev/video0-1), PulseAudio, tray, notifications, IME, accessibility |
| Blender | Isolated | ~/.config/blender, Documents/Downloads/Pictures/Videos |
X11/XWayland, PulseAudio, notifications, IME, accessibility |
| MuseScore | Isolated | ~/.config/MuseScore, ~/.local/share/MuseScore, Documents/Downloads/Music/Pictures |
ALSA via PipeWire, ALSA sequencer, X11/XWayland, print, notifications, IME, accessibility |
| mpv | Isolated | ~/.config/mpv, ~/.cache/mpv, ~/.local/state/mpv; media dirs read-only |
MPRIS, file chooser, OpenURI |
| imv | None | $HOME read-only |
File chooser, OpenURI |
| umu | Isolated | ~/Gaming only, mounted as the sandbox $HOME |
X11/XWayland, PulseAudio, SNI tray, 32-bit GPU drivers, joystick evdev, sysfs, OpenURI, inhibit, IME |
| Steam | Isolated | ~/Gaming only, mounted as the sandbox $HOME |
X11/XWayland, PulseAudio, SNI tray, 32-bit GPU drivers, joystick evdev, sysfs, OpenURI, inhibit, IME |
| Heroic | Isolated | ~/Gaming only, mounted as the sandbox $HOME |
X11/XWayland, PulseAudio, SNI tray, 32-bit GPU drivers, joystick evdev, sysfs, OpenURI, inhibit, IME |
All browsers get read-only NSS certificates, printing, native notifications and accessibility.
Browser secrets
- Chromium family: app-scoped Secret portal only.
SecretPortalKeyProviderUseForEncryptionwrites v12 cookies; noorg.freedesktop.secretsor broad keyring access. - If the Secret portal is unavailable, Chromium can fall back to v10. Monitor
Local State.os_crypt.portal.prev_init_success. - Firefox uses NSS:
key4.dbandlogins.jsonfor logins,cookies.sqlitefor cookies. It has no Secret portal or keyring grant. A Primary Password protects saved logins, not cookies. - browserpass is separate: it exposes the password store and GPG-agent socket, not cookie encryption.
- Bookmarks, history, localStorage, IndexedDB and extension data remain readable by the browser. nixpak isolates them from other apps, not from browser compromise.
Game launchers
Steam, Heroic and umu share gaming-policy.nix. The goal is that games cannot read host data, not that they are isolated from each other:
~/Gamingis mounted over$HOME; nothing else in the host home is visible. Installed games are in~/Gaming/Games, the sandbox's~/Games, which is where launchers install by default. The per-app XDG directories are off because games write saves straight into$HOME.- All launchers share that data, so they form one trust domain: a compromised game can change another launcher's configuration. Their grants therefore stay identical.
~/Gamingis game-writable, so the host never executes a file from it. Generated umu launchers run throughumu-sandbox(see thenix-config-umu-gameskill), Steam and Heroic launch their games from inside their own sandboxes, and protontricks runs only inside the sandbox:umu-sandbox protontricks <appid> winecfg, orumu-sandbox protontricks -c 'wine regedit' <appid>. A host protontricks would run Wine on those prefixes as the host user.- Replacing
programs.steamcosts its extras: no hoststeam-runor protontricks, and noextest(needs/dev/uinput). Remote Play and LAN discovery need inbound connections that pasta isolation does not forward; online play, the store and downloads are unaffected. - The launchers keep a StatusNotifier tray icon (
desktopIntegration.tray), so closing Steam or Heroic to the tray still works and the desktop shell has an icon to show. Notifications stay off. - Controllers: host udev rules hard-link joystick evdev nodes (not ones that also claim to be a
keyboard) into
/dev/gamepad-input, mounted as the sandbox/dev/input. hidraw (Steam Input's gyro/touchpad remapping, SDL's HIDAPI drivers) and/dev/uinputare not granted; uinput would let a game inject input into the host session, and it is not namespaced. A real controller therefore works through evdev, but Steam Input cannot create virtual gamepads. Like the FIDO2 directory,/dev/gamepad-inputis created at boot; after a first deploy withswitch, runsudo systemd-tmpfiles --create --prefix=/dev/gamepad-inputandsudo udevadm trigger --action=add -s inputbefore starting a launcher. - The sandbox gets Flatpak's
--device=allsysfs set without/sys/block, the udev database and/etc/passwd,/etc/groupand/etc/machine-id, which pressure-vessel and Wine need. - Proton's pressure-vessel container is a third bwrap layer (nixpak, buildFHSEnv, pressure-vessel).
This relies on unprivileged user namespaces and on the inner FHS root having no
/.flatpak-info; with it, pressure-vessel would switch to a Flatpak sub-sandbox it cannot get. - Portals see processes in the FHS layer as host apps, so no file chooser or document portal mount
is granted (they would return host paths and every exported document), and GameMode is
unavailable:
gamemodedcannot reach PIDs in the sandbox's namespace. Desktop entries that Heroic or Steam create for a game land in the sandbox's~/.local/share/applications(~/Gaming/...), so the host application menu does not show them.
Limits
- X11: Zoom, WeChat and the game launchers have shared-display trust; portal permissions cannot enforce portal-only capture.
- PulseAudio grants recording authority. Read-only GPG sockets still permit signing and decryption.
- pasta isolates loopback, not LAN access. Chromecast discovery is unverified.
- Brave Origin and Edge intentionally have no PulseAudio, Camera, ScreenCast or MPRIS grants. They are backup browsers, not media browsers.
- FIDO2 passthrough: host udev rules hard-link only hidraw nodes whose report descriptor declares
the FIDO usage page into
/dev/fido-hidraw; the sandbox binds that directory plus read-only/sys/class/hidrawand the udev database. The CanoKey's KBD and CCID interfaces, other HID devices and raw USB stay outside, so the WebUSB console is unreachable by design (key policy). A compromised browser process can send raw CTAP to the key; the key's PIN and touch policy remain the authentication boundary. The read-only udev database exposes properties of all host devices, much of which is already readable via the sysfs device binds. - FIDO2 hotplug relies on udev events, which only reach the host network namespace. Firefox and
Chromium therefore detect keys plugged in after launch; pasta-isolated Chrome needs the key
present before launch.
/dev/fido-hidrawis created at boot. When first deploying withswitchinstead of a reboot, runsudo systemd-tmpfiles --create --prefix=/dev/fido-hidraw, then replug the key (orsudo udevadm trigger --action=add -s hidraw) and fully restart the browsers: a browser started while the directory was missing has no bind for it. - Firefox camera and remote screen sharing are intentionally disabled. Firefox does not support the
XDG Camera/ScreenCast portal path used here, so granting camera or screen-capture access would
require widening the sandbox with raw
/dev/video*or direct PipeWire access. We choose not to grant that privilege. - Bambu Studio's nixpkgs build omits the
device_pagefrontend bundle, so filament management is unavailable. Its MakerWorld sidebar entry also requires the upstreamfirstguide.finish = "true"workaround; the startup splash may render black under XWayland. - Remmina's SSH Tunnel support receives a read-only SSH agent socket and SSH client config. A compromised Remmina can use the agent to sign or decrypt while the agent is running.
- OBS, Blender, MuseScore and mpv use pasta-isolated networking. Host loopback services are not reachable unless the app is changed to a host-loopback exception.
- MuseScore receives the raw PipeWire socket so its ALSA driver can use the host
pcm.!defaultPipeWire plugin. PipeWire is a general media graph, so this is broader than a single audio stream: a compromised MuseScore could enumerate other user-visible PipeWire media nodes. The alternative, direct/dev/snd, confines the media graph but bypasses PipeWire device selection, per-app volume and mixing. - OBS on Linux defaults its recording path to
$HOME. The sandbox intentionally binds only the declared home subdirectories, so recording output must targetVideos,PicturesorDownloads; a file written to the home root exists only in the sandbox's transient mount namespace.
Validation
- Passed:
just fmt,just test,ai-niribuild,shoukei-nirieval and all wrapper builds. - Interactive validation passed for Firefox, Chrome, Chromium, Brave Origin, Edge, QQ, Telegram, Zoom, Bambu Studio and OrcaSlicer. Chromium's host-loopback/personal-file exception and the Brave/Edge no-media policy are intentional.
- OBS Studio passed interactive validation for PipeWire screen capture, V4L2 camera capture, desktop/microphone audio, local NVENC H.264 MP4 recording and local RTMP streaming. External provider authentication and browser sources remain unvalidated.
- Remmina, Blender, mpv and imv build and pass the policy suite; interactive validation is still pending. umu runs Wuthering Waves and Star Rail from their desktop entries on the RTX 4090, with a working controller; audio and the in-sandbox isolation check are pending. MuseScore passed launch, XWayland and ALSA-to-PipeWire routing checks; audible playback and user workflows still need confirmation.
- Chromium smoke test passed: renderer sandbox, environment clearing, bind scope, disposable-profile persistence and loopback policy.
- FIDO2 browser passthrough is enabled for Firefox/Chromium/Chrome only. WebAuthn with the CanoKey passed in Firefox and Chrome; Chromium and plugging the key in after launch still need validation.
- WeChat uses the native nixpkgs-master package with nixpak as the only bwrap layer. Build/policy pass; interactive Wayland validation is still pending.
- Evidence and repeat commands: runtime validation, policy test, which auto-loads every suite under tests/policy.
Code map
- nixpaks/: wrappers and Home Manager package overlay.
- desktop-base.nix, xdg-storage.nix: shared runtime and per-app XDG storage.
- gaming-policy.nix, nixpaks/umu.nix, nixpaks/steam.nix, nixpaks/heroic.nix: game launcher sandbox and the three launcher wrappers.
- portal-groups.nix, desktop-integration.nix: portal and desktop service grants.
- browser-permissions.nix, fido-bwrap.nix, chromium-browser.nix: browser scopes, FIDO2 passthrough wrapper and launcher.
- nixpaks/wechat.nix: WeChat package and data setup.
- kernel-hardening.nix, apparmor/: kernel sysctl and AppArmor profiles.