mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:36 +01:00
feat(nixpak): unify desktop app sandboxes (#491)
* refactor(nixpak): unify desktop app sandboxes Move common GUI, portal, and network policy into reusable modules and migrate browsers plus Zoom/WeChat/QQ/Telegram to the same boundary. Assisted-by: DeepSeek Harness (deepseek-flash)
This commit is contained in:
1 parent
707da35996
commit
818965a11f
35 files changed
+1853
-560
No files matched your search
+19
-7
@@ -323,13 +323,25 @@ isolation. Source review and socket metadata—not reads of private credentials
|
||||
- `ssh -G` reports forwarding for explicit `192.168.*` destinations, but not the sampled `shoryu`
|
||||
alias or GitHub destination. Do not treat forwarding as enabled for every LAN alias, or remove it
|
||||
without checking deployment/build workflows.
|
||||
- Nixpak apps retain outbound networking (through pasta, without the host's loopback),
|
||||
GUI/audio/portal/GPU access, selected writable document directories, and a read-only view of the
|
||||
whole `/nix/store`. Firefox additionally retains browserpass/GnuPG integration. These are
|
||||
filesystem sandboxes, not blanket credential or management-network isolation.
|
||||
- WeChat retains networking and X11 through its FHS wrapper. Source arguments alone do not prove
|
||||
complete runtime isolation. App launch, file chooser, IME, audio/video, GPU and disposable-agent
|
||||
probes are still needed before narrowing mounts or sockets.
|
||||
- All nine desktop wrappers use shared nixpak policies: cleared environment, filtered session D-Bus
|
||||
and selected mounts. The [permission inventory](hardening/README.md#app-permissions) lists each
|
||||
app's file, network, credential and media grants.
|
||||
- Only Firefox/Chromium share host networking. Other apps isolate loopback with pasta but can still
|
||||
reach the LAN/Internet. Browser media and browserpass are enabled only for
|
||||
Firefox/Chromium/Chrome; browserpass exposes password-store/GPG data and agent operations.
|
||||
- Nixpak supplements Chromium's renderer sandbox, which remains enabled. Zoom/WeChat retain their
|
||||
FHS runtimes inside the outer boundary; inner mounts cannot recover hidden host resources.
|
||||
- Desktop access has limits: GPU drivers remain exposed, PulseAudio permits recording, read-only
|
||||
sockets permit agent operations, and Zoom/WeChat's X11 connections have shared-display trust.
|
||||
Zoom's 7.2.1 client receives only `/dev/video0` and `/dev/video1` because it captures the webcam
|
||||
directly via V4L2; no other app gets raw camera nodes and no raw PipeWire access is added.
|
||||
Chromium-family browsers use only the app-scoped Secret portal for v12 cookie encryption; no
|
||||
browser receives broad GNOME Keyring authority. Firefox uses its own NSS storage.
|
||||
- **Rollout note (WA-045):** v12 is verified on a disposable profile, but if the portal is
|
||||
unavailable Chromium may fall back to weaker v10 storage; portal initialization is monitored.
|
||||
- Runtime evidence is partial: Brave/Edge headless export times out; interactive login, media, IME
|
||||
and file workflows require acceptance. See the
|
||||
[validation records](hardening/README.md#validation).
|
||||
|
||||
#### Agent-first diagnostics by host role
|
||||
|
||||
|
||||
+2
-1
@@ -83,10 +83,11 @@ A known limitation with no fix yet.
|
||||
An unfinished feature or gap.
|
||||
|
||||
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
|
||||
| ------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ---------- | ------------------------ | ------ |
|
||||
| ------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------ | ---------- |
|
||||
| WA-013 | NixOS tests — `outputs/README.md` | full-host tests still rebuild custom packages (no private cache) and need the host ssh key to decrypt agenix; the shared-firewall security-exporters VM test now runs in CI (KVM); `outputs/x86_64-linux/nixos-tests/idols-ruby.nix` is exposed as `packages.x86_64-linux.ruby-nixos-tests` but is not in `checks` or CI | Resolve when a private cache exists and tests can run per-service | 2024-03-13 | quarterly | active |
|
||||
| WA-027 | NFS golden store `no_root_squash` — `hosts/12kingdoms-youko/nfs-golden.nix` | the existing PVC subdirectories are root-owned, so enabling `root_squash` would break running workloads until they are migrated; the CSI node plugin is hostNetwork and sources from the allowlisted node IPs | Drop when the PVC subdirectories are migrated and `root_squash` is enabled | 2026-10-07 | next maintenance window | active |
|
||||
| WA-044 | transmission AppArmor lock permission — `hosts/12kingdoms-youko/homelab-services/transmission.nix` | nixpkgs' transmission 4.1.4 profile grants `rw` on the daemon's config dir but not `k`, while 4.1.4 takes an fcntl lock on `~/.config/transmission-daemon/lock`; the daemon exits with "Another process is already using". A `local/bin.transmission-daemon` include adds `rwk` for that lock file only | Drop once the nixpkgs profile includes the lock permission | 2026-10-31 | on the next nixpkgs bump | active |
|
||||
| WA-045 | Chromium cookie crypto — `hardening/nixpaks/modules/chromium-browser.nix` | Chromium-family browsers use the app-scoped Secret portal and write v12 cookies. If the portal is unavailable, Chromium may fall back to weak v10 storage. Brave/Edge headless export also times out. | Drop once the v10 fallback is eliminated and the Brave/Edge headless checks pass; monitor portal initialization and interactive browser checks | 2026-10-31 | on Chromium updates | active gap |
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
+63
-95
@@ -1,111 +1,79 @@
|
||||
# Linux Hardening
|
||||
|
||||
> Work in progress.
|
||||
Deployed and interactively validated on `ai` (2026-10-31). Policy and limits:
|
||||
[SECURITY.md](../SECURITY.md).
|
||||
|
||||
Application sandboxing (nixpak/bubblewrap) and Linux hardening knowledge for this repository. For
|
||||
the repository security policy, architecture and audit status, see [`SECURITY.md`](../SECURITY.md);
|
||||
for temporary exceptions and known gaps, see [`WORKAROUNDS.md`](../WORKAROUNDS.md).
|
||||
## Shared boundaries
|
||||
|
||||
## Goal
|
||||
- Cleared environment; filtered session D-Bus; private `/dev/shm`; per-app `/tmp`. Chromium-family
|
||||
browsers share an app-specific `/tmp` so external URLs reuse one browser instance.
|
||||
- Read-only `/nix/store`; GPU and display; fonts and themes; LAN and Internet.
|
||||
- Portals for file chooser, URI opening, notifications, input methods and document exports.
|
||||
- No whole host home, system D-Bus, SSH agent, USB/serial, raw camera or raw PipeWire socket.
|
||||
- Host loopback only for Firefox/Chromium; all other apps use pasta-isolated loopback.
|
||||
|
||||
- **System Level**: Protect critical files from being accessed by untrusted applications.
|
||||
1. Such as browser cookies, SSH keys, etc.
|
||||
- **Per-App Level**: Prevent untrusted applications(such as closed-source apps) from:
|
||||
1. Accessing files they shouldn't.
|
||||
- Such as a malicious application accessing your browser's cookies, SSH keys, etc.
|
||||
1. Accessing the network when they don't need to.
|
||||
1. Accessing hardware devices they don't need.
|
||||
## App permissions
|
||||
|
||||
## Current Structure
|
||||
`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.
|
||||
|
||||
### 1. **System Level**
|
||||
| App | Network | Writable paths | Grants |
|
||||
| ------------ | ------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| Firefox | Host loopback | `~/.mozilla`, personal files, Trash, Firefox artwork | Media, browserpass, MPRIS, idle |
|
||||
| Chromium | Host loopback | `~/.config/chromium`, personal files | Media, browserpass, MPRIS, idle, v12 Secret |
|
||||
| Chrome | Isolated | `~/.config/google-chrome`, Downloads | Media, browserpass, MPRIS, idle, v12 Secret |
|
||||
| Brave Origin | Isolated | `~/.config/BraveSoftware/Brave-Origin`, Downloads | v12 Secret |
|
||||
| Edge | Isolated | `~/.config/microsoft-edge`, Downloads | v12 Secret |
|
||||
| QQ | 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` |
|
||||
| WeChat | Isolated | `Documents/WeChat_Data`, its HOME mapping and `xwechat_files` link, private app state | Media, X11/xcb, FHS, SNI tray |
|
||||
|
||||
- **AppArmor** (`apparmor/`): AppArmor profiles and configuration
|
||||
- **Kernel & process hardening** (`kernel-hardening.nix`): sysctl baseline and unused-module deny
|
||||
list; rationale and verification live in [`SECURITY.md`](../SECURITY.md).
|
||||
All browsers get read-only NSS certificates, printing, native notifications and accessibility.
|
||||
|
||||
### 2. **Per-App Level**
|
||||
## Browser secrets
|
||||
|
||||
- **Nixpak** (`nixpaks/`): Bubblewrap-based sandboxing for applications
|
||||
- Firefox configuration
|
||||
- QQ (Chinese messaging app) configuration
|
||||
- Telegram Desktop configuration
|
||||
- Modular system with reusable components
|
||||
- **Bubblewrap** (`bwraps/`): Direct bubblewrap configurations
|
||||
- WeChat sandboxing configuration
|
||||
- Chromium family: app-scoped Secret portal only. `SecretPortalKeyProviderUseForEncryption` writes
|
||||
v12 cookies; no `org.freedesktop.secrets` or 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.db` and `logins.json` for logins, `cookies.sqlite` for 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.
|
||||
|
||||
## Current Implementation Status
|
||||
## Limits
|
||||
|
||||
| Component | Status | Notes |
|
||||
| ----------------- | --------- | ----------------------------- |
|
||||
| AppArmor Profiles | 🚧 WIP | Basic structure in place |
|
||||
| Nixpak Firefox | ✅ Active | Firefox sandboxing via nixpak |
|
||||
| Nixpak QQ | ✅ Active | QQ application sandboxing |
|
||||
| Nixpak Telegram | ✅ Active | Telegram Desktop sandboxing |
|
||||
| Bubblewrap WeChat | ✅ Active | WeChat specific sandboxing |
|
||||
- X11: Zoom and WeChat 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/Edge renderer and permission checks pass, but headless page export still times out.
|
||||
|
||||
## Directory Structure
|
||||
## Validation
|
||||
|
||||
```
|
||||
hardening/
|
||||
├── README.md
|
||||
├── kernel-hardening.nix # sysctl baseline + unused-module deny list
|
||||
├── apparmor/ # AppArmor security profiles
|
||||
│ └── default.nix
|
||||
├── bwraps/ # Direct bubblewrap configurations
|
||||
│ ├── default.nix
|
||||
│ └── wechat.nix
|
||||
└── nixpaks/ # Nixpak application sandboxing
|
||||
├── default.nix
|
||||
├── firefox.nix
|
||||
├── qq.nix
|
||||
├── telegram-desktop.nix
|
||||
└── modules/ # Reusable nixpak modules
|
||||
├── custom-policy.nix
|
||||
├── gui-base.nix
|
||||
└── network.nix
|
||||
```
|
||||
- Passed: `just fmt`, `just test`, `ai-niri` build and `shoukei-niri` eval.
|
||||
- Deployed on `ai`; interactive startup and tray checks passed for Chrome, Firefox, QQ, Telegram,
|
||||
Zoom and WeChat.
|
||||
- Chromium/Chrome: renderer sandbox, mounts, rendering and disposable-profile persistence passed.
|
||||
Chromium reaches host loopback; Chrome cannot.
|
||||
- Firefox: disposable headless screenshot passed.
|
||||
- Brave/Edge: renderer and permission checks passed; headless page export timed out.
|
||||
- Evidence: [browsers](tests/browser-runtime.md),
|
||||
[Firefox/QQ/Telegram](tests/unified-app-runtime.md), [Zoom](tests/zoom-runtime.md),
|
||||
[WeChat](tests/wechat-runtime.md).
|
||||
|
||||
## System Hardening
|
||||
## Code map
|
||||
|
||||
- Apparmor: [roddhjav/apparmor.d](https://github.com/roddhjav/apparmor.d)
|
||||
- https://gitlab.com/apparmor/apparmor/-/wikis/Documentation
|
||||
- AppArmor.d is a set of over 1500 AppArmor profiles whose aim is to confine most Linux based
|
||||
applications and processes.
|
||||
- But its profiles assume an FHS filesystem, so most of them never match the Nix store paths a
|
||||
NixOS system actually executes.
|
||||
- Apparmor on NixOS Roadmap:
|
||||
- https://discourse.nixos.org/t/apparmor-on-nixos-roadmap/57217
|
||||
- https://github.com/LordGrimmauld/aa-alias-manager
|
||||
- SELinux: too complex, not recommended for personal use.
|
||||
|
||||
## Application Sandboxing
|
||||
|
||||
- [Bubblewrap](https://github.com/containers/bubblewrap):
|
||||
[nixpak](https://github.com/nixpak/nixpak), more secure than firejail, but no batteries included.
|
||||
- NixOS's FHSEnv is implemented using bubblewrap by default.
|
||||
- [Firejail](https://github.com/netblue30/firejail/tree/master/etc): A SUID security sandbox with
|
||||
hundreds of security profiles for many common applications in the default installation.
|
||||
- https://wiki.nixos.org/wiki/Firejail
|
||||
- Firejail needs SUID to work, which is considered a security risk -
|
||||
[Does firejail improve the security of my system?](https://github.com/netblue30/firejail/discussions/4601)
|
||||
- [Systemd/Hardening](https://wiki.nixos.org/wiki/Systemd/Hardening): Systemd also provides some
|
||||
sandboxing features.
|
||||
|
||||
## NOTE
|
||||
|
||||
**Running untrusted code is never safe, kernel hardening & sandboxing cannot change this**; run
|
||||
untrusted code in a VM on an isolated network. The threat model lives in
|
||||
[`SECURITY.md`](../SECURITY.md).
|
||||
|
||||
## References
|
||||
|
||||
- [Harden your NixOS workstation - dataswamp](https://dataswamp.org/~solene/2022-01-13-nixos-hardened.html)
|
||||
- [Linux Insecurities - Madaidans](https://madaidans-insecurities.github.io/linux.html)
|
||||
- [Sandboxing all programs by default - NixOS Discourse](https://discourse.nixos.org/t/sandboxing-all-programs-by-default/7792)
|
||||
- [Paranoid NixOS Setup - xeiaso](https://xeiaso.net/blog/paranoid-nixos-2021-07-18/)
|
||||
- [nix-mineral](https://github.com/cynicsketch/nix-mineral): NixOS module for convenient system
|
||||
hardening.
|
||||
- apparmor configs:
|
||||
- https://github.com/zramctl/dotfiles/blob/4fe177f6984154960942bb47d5a375098ec6ed6a/modules/nixos/security/apparmor.nix#L4
|
||||
- https://git.grimmauld.de/Grimmauld/grimm-nixos-laptop/src/branch/main/hardening
|
||||
- [nixpaks/](nixpaks/): wrappers and Home Manager package overlay.
|
||||
- [desktop-base.nix](nixpaks/modules/desktop-base.nix): shared runtime and XDG storage.
|
||||
- [portal-groups.nix](nixpaks/modules/portal-groups.nix),
|
||||
[desktop-integration.nix](nixpaks/modules/desktop-integration.nix): portal and desktop service
|
||||
grants.
|
||||
- [browser-permissions.nix](nixpaks/modules/browser-permissions.nix),
|
||||
[chromium-browser.nix](nixpaks/modules/chromium-browser.nix): browser scopes and launcher.
|
||||
- [bwraps/wechat.nix](bwraps/wechat.nix): WeChat FHS and data setup.
|
||||
- [kernel-hardening.nix](kernel-hardening.nix), [apparmor/](apparmor/): kernel sysctl and AppArmor
|
||||
profiles.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
callPackage,
|
||||
mkNixPak,
|
||||
brave-origin,
|
||||
...
|
||||
}:
|
||||
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
|
||||
package = brave-origin;
|
||||
appId = "com.brave.Origin";
|
||||
profile = "BraveSoftware/Brave-Origin";
|
||||
command = "brave-origin";
|
||||
desktopName = "Brave Origin";
|
||||
desktopFile = "brave-origin";
|
||||
browserpass = false;
|
||||
secretPortal = true;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
callPackage,
|
||||
mkNixPak,
|
||||
chromium,
|
||||
...
|
||||
}:
|
||||
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
|
||||
package = chromium;
|
||||
appId = "org.chromium.Chromium";
|
||||
profile = "chromium";
|
||||
command = "chromium";
|
||||
desktopName = "Chromium";
|
||||
desktopFile = "chromium-browser";
|
||||
browserpass = true;
|
||||
media = true;
|
||||
hostLoopback = true;
|
||||
personalFiles = true;
|
||||
secretPortal = true;
|
||||
}
|
||||
@@ -21,6 +21,12 @@ in
|
||||
qq = wrapper pkgs-master ./qq.nix;
|
||||
telegram-desktop = wrapper super ./telegram-desktop.nix;
|
||||
firefox = wrapper super ./firefox.nix;
|
||||
google-chrome = wrapper super ./google-chrome.nix;
|
||||
chromium = wrapper super ./chromium.nix;
|
||||
brave-origin = wrapper super ./brave-origin.nix;
|
||||
microsoft-edge = wrapper super ./microsoft-edge.nix;
|
||||
zoom = wrapper super ./zoom.nix;
|
||||
wechat = wrapper super ./wechat.nix;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
@@ -21,9 +21,6 @@ let
|
||||
sloth,
|
||||
...
|
||||
}:
|
||||
let
|
||||
passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store";
|
||||
in
|
||||
{
|
||||
app = {
|
||||
package = firefox;
|
||||
@@ -32,14 +29,19 @@ let
|
||||
flatpak.appId = appId;
|
||||
|
||||
imports = [
|
||||
./modules/gui-base.nix
|
||||
./modules/browser-permissions.nix
|
||||
./modules/network.nix
|
||||
./modules/custom-policy.nix
|
||||
];
|
||||
|
||||
browserPermissions = {
|
||||
media = true;
|
||||
personalFiles = true;
|
||||
browserpass = true;
|
||||
};
|
||||
|
||||
# Firefox keeps the host network: local development servers and the
|
||||
# mihomo controller live on 127.0.0.1, and pasta's isolated loopback
|
||||
# would hide them. The other sandboxed apps do not need host loopback.
|
||||
# would hide them. Chromium uses the same personal-browser policy.
|
||||
pasta.enable = lib.mkForce false;
|
||||
|
||||
bubblewrap = {
|
||||
@@ -55,16 +57,6 @@ let
|
||||
# path over MPRIS. Without this bind it lands under the sandboxed
|
||||
# XDG_DATA_HOME and Noctalia (outside) cannot read it.
|
||||
(sloth.mkdir (sloth.concat' sloth.homeDir "/.local/share/firefox-mpris"))
|
||||
|
||||
# Files deleted from Firefox go to the real trash instead of a
|
||||
# private one under the sandboxed XDG_DATA_HOME.
|
||||
(sloth.mkdir (sloth.concat' sloth.homeDir "/.local/share/Trash"))
|
||||
|
||||
sloth.xdgDocumentsDir
|
||||
sloth.xdgDownloadDir
|
||||
sloth.xdgMusicDir
|
||||
sloth.xdgVideosDir
|
||||
sloth.xdgPicturesDir
|
||||
];
|
||||
bind.ro = [
|
||||
"/sys/bus/pci"
|
||||
@@ -72,25 +64,7 @@ let
|
||||
"${config.app.package}/lib/firefox"
|
||||
"/app/etc/firefox"
|
||||
]
|
||||
|
||||
# ================ for browserpass extension ===============================
|
||||
"/etc/gnupg"
|
||||
(sloth.concat' sloth.homeDir "/.gnupg") # gpg's config
|
||||
passwordStore # my secrets
|
||||
(sloth.concat' sloth.runtimeDir "/gnupg") # for access gpg-agent socket
|
||||
|
||||
# Match the host's default-app associations and XDG user dirs.
|
||||
(sloth.concat' sloth.homeDir "/.config/mimeapps.list")
|
||||
(sloth.concat' sloth.homeDir "/.config/user-dirs.dirs")
|
||||
|
||||
# Shared NSS client-certificate store (imported outside Firefox).
|
||||
(sloth.concat' sloth.homeDir "/.pki/nssdb")
|
||||
|
||||
# dconf settings. Not verified as needed; candidate for removal.
|
||||
(sloth.concat' sloth.xdgConfigHome "/dconf")
|
||||
];
|
||||
# browserpass looks for the store here, not at pass's default path.
|
||||
env.PASSWORD_STORE_DIR = passwordStore;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
callPackage,
|
||||
mkNixPak,
|
||||
google-chrome,
|
||||
...
|
||||
}:
|
||||
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
|
||||
package = google-chrome;
|
||||
appId = "com.google.Chrome";
|
||||
profile = "google-chrome";
|
||||
command = "google-chrome";
|
||||
desktopName = "Google Chrome";
|
||||
desktopFile = "google-chrome";
|
||||
browserpass = true;
|
||||
media = true;
|
||||
secretPortal = true;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
callPackage,
|
||||
mkNixPak,
|
||||
microsoft-edge,
|
||||
...
|
||||
}:
|
||||
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
|
||||
package = microsoft-edge;
|
||||
appId = "com.microsoft.Edge";
|
||||
profile = "microsoft-edge";
|
||||
command = "microsoft-edge";
|
||||
desktopName = "Microsoft Edge";
|
||||
desktopFile = "microsoft-edge";
|
||||
browserpass = false;
|
||||
secretPortal = true;
|
||||
privateWindowFlag = "--inprivate";
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
# Shared browser capabilities. Network topology and application profiles are
|
||||
# selected by the app; personal files, browserpass, Secret portal and media are explicit opt-ins.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
sloth,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.browserPermissions;
|
||||
passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store";
|
||||
in
|
||||
{
|
||||
imports = [ ./desktop-base.nix ];
|
||||
options.browserPermissions = {
|
||||
media = lib.mkEnableOption "audio playback/recording, camera and desktop screen sharing";
|
||||
personalFiles = lib.mkEnableOption "personal Documents, media directories and Trash";
|
||||
browserpass = lib.mkEnableOption "browserpass access to the password store and GPG agent";
|
||||
secretPortal = lib.mkEnableOption "the app-scoped Secret portal for v12 cookie encryption";
|
||||
};
|
||||
config = {
|
||||
portalGroups = {
|
||||
fileChooser = true;
|
||||
openUri = true;
|
||||
notifications = true;
|
||||
screenCast = cfg.media;
|
||||
camera = cfg.media;
|
||||
print = true;
|
||||
inhibit = cfg.media;
|
||||
inputMethod = true;
|
||||
secret = cfg.secretPortal;
|
||||
};
|
||||
desktopIntegration = {
|
||||
notifications = true;
|
||||
mpris = cfg.media;
|
||||
accessibility = true;
|
||||
};
|
||||
bubblewrap = {
|
||||
sockets.pulse = cfg.media;
|
||||
bind.rw = [
|
||||
sloth.xdgDownloadDir
|
||||
]
|
||||
++ lib.optionals cfg.personalFiles [
|
||||
sloth.xdgDocumentsDir
|
||||
sloth.xdgMusicDir
|
||||
sloth.xdgVideosDir
|
||||
sloth.xdgPicturesDir
|
||||
(sloth.mkdir (sloth.concat' sloth.homeDir "/.local/share/Trash"))
|
||||
];
|
||||
bind.ro = [
|
||||
(sloth.concat' sloth.homeDir "/.config/mimeapps.list")
|
||||
(sloth.concat' sloth.homeDir "/.config/user-dirs.dirs")
|
||||
(sloth.concat' sloth.homeDir "/.pki/nssdb")
|
||||
]
|
||||
++ lib.optionals cfg.browserpass [
|
||||
"/etc/gnupg"
|
||||
(sloth.concat' sloth.homeDir "/.gnupg")
|
||||
passwordStore
|
||||
(sloth.concat' sloth.runtimeDir "/gnupg")
|
||||
];
|
||||
env = lib.optionalAttrs cfg.browserpass { PASSWORD_STORE_DIR = passwordStore; };
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Chromium-family wrapper: nixpak adds filesystem, D-Bus and network boundaries;
|
||||
# the upstream Chromium renderer sandbox remains enabled.
|
||||
{
|
||||
lib,
|
||||
mkNixPak,
|
||||
buildEnv,
|
||||
makeDesktopItem,
|
||||
runCommandLocal,
|
||||
makeWrapper,
|
||||
...
|
||||
}:
|
||||
{
|
||||
package,
|
||||
appId,
|
||||
profile,
|
||||
command,
|
||||
desktopName,
|
||||
desktopFile ? command,
|
||||
browserpass ? false,
|
||||
media ? false,
|
||||
secretPortal ? false,
|
||||
hostLoopback ? false,
|
||||
personalFiles ? false,
|
||||
privateWindowFlag ? "--incognito",
|
||||
}:
|
||||
let
|
||||
# Chromium 156 disables PipeWire camera capture by default; media apps need
|
||||
# the portal FD path instead of raw video devices.
|
||||
# Keep the Wayland feature from nixpkgs: the last --enable-features wins.
|
||||
enabledFeatures = lib.concatStringsSep "," (
|
||||
[ "WaylandWindowDecorations" ]
|
||||
++ lib.optional media "WebRtcPipeWireCamera"
|
||||
++ lib.optional secretPortal "SecretPortalKeyProviderUseForEncryption"
|
||||
);
|
||||
usernsPackage =
|
||||
runCommandLocal "${package.name}-userns"
|
||||
{
|
||||
nativeBuildInputs = [ makeWrapper ];
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${package}/bin/${command} $out/bin/${command} \
|
||||
--add-flags "--enable-features=${enabledFeatures}"
|
||||
'';
|
||||
wrapped = mkNixPak {
|
||||
config =
|
||||
{ sloth, ... }:
|
||||
{
|
||||
imports = [
|
||||
./browser-permissions.nix
|
||||
./network.nix
|
||||
];
|
||||
app = {
|
||||
package = usernsPackage;
|
||||
binPath = "bin/${command}";
|
||||
};
|
||||
flatpak = { inherit appId; };
|
||||
# Firefox/Chromium need host loopback for local services; Chrome and the rest use pasta.
|
||||
pasta.enable = lib.mkForce (!hostLoopback);
|
||||
pasta.mode = "isolate";
|
||||
browserPermissions = {
|
||||
inherit
|
||||
browserpass
|
||||
personalFiles
|
||||
media
|
||||
secretPortal
|
||||
;
|
||||
};
|
||||
bubblewrap.bind.rw = [
|
||||
(sloth.mkdir (sloth.concat' sloth.homeDir "/.config/${profile}"))
|
||||
];
|
||||
};
|
||||
};
|
||||
exe = "${wrapped.config.script}/bin/${command}";
|
||||
# Copy desktop assets only; the full package would expose unwrapped binaries.
|
||||
assets = runCommandLocal "${command}-desktop-assets" { } ''
|
||||
mkdir -p $out/share
|
||||
for directory in icons pixmaps; do
|
||||
if [ -d ${package}/share/$directory ]; then
|
||||
ln -s ${package}/share/$directory $out/share/$directory
|
||||
fi
|
||||
done
|
||||
'';
|
||||
in
|
||||
buildEnv {
|
||||
inherit (wrapped.config.script) name meta passthru;
|
||||
paths = [
|
||||
wrapped.config.script
|
||||
assets
|
||||
(makeDesktopItem {
|
||||
name = appId;
|
||||
inherit desktopName;
|
||||
genericName = "Web Browser";
|
||||
exec = "${exe} %U";
|
||||
icon = desktopFile;
|
||||
terminal = false;
|
||||
startupNotify = true;
|
||||
categories = [
|
||||
"Network"
|
||||
"WebBrowser"
|
||||
];
|
||||
mimeTypes = [
|
||||
"text/html"
|
||||
"application/xhtml+xml"
|
||||
"x-scheme-handler/http"
|
||||
"x-scheme-handler/https"
|
||||
];
|
||||
actions = {
|
||||
new-window = {
|
||||
name = "New Window";
|
||||
exec = "${exe} --new-window %U";
|
||||
};
|
||||
new-private-window = {
|
||||
name = "New Private Window";
|
||||
exec = "${exe} ${privateWindowFlag} %U";
|
||||
};
|
||||
};
|
||||
extraConfig.X-Flatpak = appId;
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
# Custom, repo-local policy for the sandboxed desktop apps (Firefox, QQ,
|
||||
# Telegram). Unlike ./gui-base.nix, which forks nixpak/pkgs's gui-base and is
|
||||
# meant to track upstream, this file is hand-written for this repository: it
|
||||
# grants the D-Bus surface those apps need (portals, MPRIS, tray, input
|
||||
# methods, a11y), redirects the XDG dirs Flatpak-style, and adds the remaining
|
||||
# document/device mounts. Keep only app-specific mounts in the app files.
|
||||
#
|
||||
# Adapted from:
|
||||
# - https://github.com/mnixry/nixos-config/blob/74913c2b90d06e31170bbbaa0074f915721da224/desktop/packages/nixpaks-common.nix
|
||||
# - https://github.com/Kraftland/portable/blob/09c4a4227538a3f42de208a6ecbdc938ac9c00dd/portable.sh
|
||||
# - https://flatpak.github.io/xdg-desktop-portal/docs/api-reference.html
|
||||
{
|
||||
lib,
|
||||
sloth,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (config.flatpak) appId;
|
||||
in
|
||||
{
|
||||
config = {
|
||||
# list all dbus services:
|
||||
# ls -al /run/current-system/sw/share/dbus-1/services/
|
||||
# ls -al /etc/profiles/per-user/ryan/share/dbus-1/services/
|
||||
dbus = {
|
||||
# `--see`: The bus name can be enumerated by the application.
|
||||
# `--talk`: The application can send messages to, and receive replies and signals from, the bus name.
|
||||
# `--own`: The application can own the bus name
|
||||
policies = {
|
||||
"${appId}" = "own";
|
||||
"${appId}.*" = "own";
|
||||
"org.freedesktop.DBus" = "talk";
|
||||
"ca.desrt.dconf" = "talk";
|
||||
"org.freedesktop.appearance" = "talk";
|
||||
"org.freedesktop.appearance.*" = "talk";
|
||||
}
|
||||
// (builtins.listToAttrs (
|
||||
map (id: lib.nameValuePair "org.kde.StatusNotifierItem-${toString id}-1" "own") (
|
||||
lib.lists.range 2 29
|
||||
)
|
||||
))
|
||||
// {
|
||||
# --- MPRIS Media Control ---
|
||||
# Allows the app to register as a media player. These are derived from the appID.
|
||||
"org.mpris.MediaPlayer2.${appId}" = "own";
|
||||
"org.mpris.MediaPlayer2.${appId}.*" = "own";
|
||||
"org.mpris.MediaPlayer2.${lib.lists.last (lib.strings.splitString "." appId)}" = "own";
|
||||
"org.mpris.MediaPlayer2.${lib.lists.last (lib.strings.splitString "." appId)}.*" = "own";
|
||||
|
||||
# --- General Desktop Integration ---
|
||||
"com.canonical.AppMenu.Registrar" = "talk"; # For Ubuntu AppMenu
|
||||
"org.freedesktop.FileManager1" = "talk";
|
||||
"org.freedesktop.Notifications" = "talk";
|
||||
"org.kde.StatusNotifierWatcher" = "talk";
|
||||
"org.gnome.Shell.Screencast" = "talk";
|
||||
|
||||
# --- Accessibility (a11y) 无障碍服务 ---
|
||||
"org.a11y.Bus" = "see";
|
||||
|
||||
# --- Portal Access ---
|
||||
# "org.freedesktop.portal.*" = "talk";
|
||||
"org.freedesktop.portal.Documents" = "talk";
|
||||
"org.freedesktop.portal.FileTransfer" = "talk";
|
||||
"org.freedesktop.portal.FileTransfer.*" = "talk";
|
||||
"org.freedesktop.portal.Notification" = "talk";
|
||||
"org.freedesktop.portal.OpenURI" = "talk";
|
||||
"org.freedesktop.portal.OpenURI.OpenFile" = "talk";
|
||||
"org.freedesktop.portal.OpenURI.OpenURI" = "talk";
|
||||
"org.freedesktop.portal.Print" = "talk";
|
||||
"org.freedesktop.portal.Request" = "see";
|
||||
|
||||
# --- Input Method Portals ---
|
||||
"org.freedesktop.portal.Fcitx" = "talk";
|
||||
"org.freedesktop.portal.Fcitx.*" = "talk";
|
||||
"org.freedesktop.portal.IBus" = "talk";
|
||||
"org.freedesktop.portal.IBus.*" = "talk";
|
||||
};
|
||||
# '--call' rules permit specific method calls on D-Bus interfaces.
|
||||
rules.call = {
|
||||
# --- Accessibility (a11y) 无障碍服务 ---
|
||||
"org.a11y.Bus" = [
|
||||
"org.a11y.Bus.GetAddress@/org/a11y/bus"
|
||||
"org.freedesktop.DBus.Properties.Get@/org/a11y/bus"
|
||||
];
|
||||
|
||||
# --- General Portal Rules ---
|
||||
"org.freedesktop.FileManager1" = [ "*" ];
|
||||
"org.freedesktop.Notifications.*" = [ "*" ];
|
||||
"org.freedesktop.portal.Documents" = [ "*" ];
|
||||
"org.freedesktop.portal.FileTransfer" = [ "*" ];
|
||||
"org.freedesktop.portal.FileTransfer.*" = [ "*" ];
|
||||
"org.freedesktop.portal.Fcitx" = [ "*" ];
|
||||
"org.freedesktop.portal.Fcitx.*" = [ "*" ];
|
||||
"org.freedesktop.portal.IBus" = [ "*" ];
|
||||
"org.freedesktop.portal.IBus.*" = [ "*" ];
|
||||
"org.freedesktop.portal.Notification" = [ "*" ];
|
||||
"org.freedesktop.portal.OpenURI" = [ "*" ];
|
||||
"org.freedesktop.portal.OpenURI.OpenFile" = [ "*" ];
|
||||
"org.freedesktop.portal.OpenURI.OpenURI" = [ "*" ];
|
||||
"org.freedesktop.portal.Print" = [ "*" ];
|
||||
"org.freedesktop.portal.Request" = [ "*" ];
|
||||
|
||||
# --- Main Desktop Portal Interface ---
|
||||
# A comprehensive list of permissions for interacting with the desktop environment.
|
||||
"org.freedesktop.portal.Desktop" = [
|
||||
# Properties & Settings
|
||||
"org.freedesktop.DBus.Properties.GetAll"
|
||||
"org.freedesktop.DBus.Properties.Get@/org/freedesktop/portal/desktop"
|
||||
"org.freedesktop.portal.Session.Close"
|
||||
"org.freedesktop.portal.Settings.ReadAll"
|
||||
"org.freedesktop.portal.Settings.Read"
|
||||
"org.freedesktop.portal.Account.GetUserInformation"
|
||||
|
||||
# Network & Proxy
|
||||
"org.freedesktop.portal.NetworkMonitor"
|
||||
"org.freedesktop.portal.NetworkMonitor.*"
|
||||
"org.freedesktop.portal.ProxyResolver.Lookup"
|
||||
"org.freedesktop.portal.ProxyResolver.Lookup.*"
|
||||
|
||||
# Screenshot / Screen Capture & Sharing
|
||||
"org.freedesktop.portal.ScreenCast"
|
||||
"org.freedesktop.portal.ScreenCast.*"
|
||||
"org.freedesktop.portal.Screenshot"
|
||||
"org.freedesktop.portal.Screenshot.Screenshot"
|
||||
|
||||
# Device Access(Camera / USB)
|
||||
"org.freedesktop.portal.Camera"
|
||||
"org.freedesktop.portal.Camera.*"
|
||||
"org.freedesktop.portal.Usb"
|
||||
"org.freedesktop.portal.Usb.*"
|
||||
|
||||
# Remote Desktop
|
||||
"org.freedesktop.portal.RemoteDesktop"
|
||||
"org.freedesktop.portal.RemoteDesktop.*"
|
||||
|
||||
# File Operations
|
||||
"org.freedesktop.portal.Documents"
|
||||
"org.freedesktop.portal.Documents.*"
|
||||
"org.freedesktop.portal.FileChooser"
|
||||
"org.freedesktop.portal.FileChooser.*"
|
||||
"org.freedesktop.portal.FileTransfer"
|
||||
"org.freedesktop.portal.FileTransfer.*"
|
||||
|
||||
# Notifications & Printing
|
||||
"org.freedesktop.portal.Notification"
|
||||
"org.freedesktop.portal.Notification.*"
|
||||
"org.freedesktop.portal.Print"
|
||||
"org.freedesktop.portal.Print.*"
|
||||
|
||||
# Open/Launch Handlers
|
||||
"org.freedesktop.portal.OpenURI"
|
||||
"org.freedesktop.portal.OpenURI.*"
|
||||
"org.freedesktop.portal.Email.ComposeEmail"
|
||||
|
||||
# Input Methods
|
||||
"org.freedesktop.portal.Fcitx"
|
||||
"org.freedesktop.portal.Fcitx.*"
|
||||
"org.freedesktop.portal.IBus"
|
||||
"org.freedesktop.portal.IBus.*"
|
||||
|
||||
# Secrets (Keyring)
|
||||
"org.freedesktop.portal.Secret"
|
||||
"org.freedesktop.portal.Secret.RetrieveSecret"
|
||||
|
||||
# Get/Update GlobalShortcuts
|
||||
# "org.freedesktop.portal.GlobalShortcuts"
|
||||
# "org.freedesktop.portal.GlobalShortcuts.*"
|
||||
|
||||
# -- get the user's location
|
||||
# "org.freedesktop.portal.Location"
|
||||
# "org.freedesktop.portal.Location.*"
|
||||
|
||||
# -- inhibit the user session from ending, suspending, idling or getting switched away.
|
||||
"org.freedesktop.portal.Inhibit"
|
||||
"org.freedesktop.portal.Inhibit.*"
|
||||
|
||||
# Generic Request Fallback
|
||||
"org.freedesktop.portal.Request"
|
||||
];
|
||||
};
|
||||
|
||||
# 'broadcast' rules permit receiving signals from D-Bus names.
|
||||
rules.broadcast = {
|
||||
"org.freedesktop.portal.*" = [ "@/org/freedesktop/portal/*" ];
|
||||
};
|
||||
args = [
|
||||
"--filter"
|
||||
"--sloppy-names"
|
||||
];
|
||||
};
|
||||
|
||||
etc.sslCertificates.enable = true;
|
||||
bubblewrap = {
|
||||
network = lib.mkDefault true;
|
||||
|
||||
# Desktop-wide mounts (Wayland/at-spi/gvfsd, the app cache, fontconfig and
|
||||
# Mesa caches, GTK config, /dev/shm) are provided by ./gui-base.nix, so
|
||||
# each path is bound exactly once. This is what keeps a read-only bind
|
||||
# from shadowing a writable one. Only app-specific mounts belong in this
|
||||
# file.
|
||||
bind.rw = with sloth; [
|
||||
[
|
||||
(mkdir appDataDir)
|
||||
xdgDataHome
|
||||
]
|
||||
[
|
||||
(mkdir appConfigDir)
|
||||
xdgConfigHome
|
||||
]
|
||||
|
||||
# FileChooser may return a Document Portal path even for a directly mapped XDG directory;
|
||||
# keep the FUSE mount writable so SaveFile can create the selected file.
|
||||
(sloth.concat' sloth.runtimeDir "/doc")
|
||||
|
||||
(sloth.concat' sloth.runtimeDir "/dconf")
|
||||
|
||||
(sloth.concat' sloth.xdgCacheHome "/mesa_shader_cache_db")
|
||||
(sloth.concat' sloth.xdgCacheHome "/radv_builtin_shaders")
|
||||
];
|
||||
bind.ro = [
|
||||
(sloth.concat' sloth.xdgConfigHome "/kdeglobals")
|
||||
(sloth.concat' sloth.xdgConfigHome "/dconf")
|
||||
];
|
||||
bind.dev = map (id: "/dev/video${toString id}") (lib.lists.range 0 9);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Shared desktop runtime; portal and native services are opt-in. No host GVfs,
|
||||
# shared caches, raw camera or PipeWire socket.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
sloth,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./portal-groups.nix
|
||||
./desktop-integration.nix
|
||||
];
|
||||
gpu = {
|
||||
enable = lib.mkDefault true;
|
||||
provider = "nixos";
|
||||
bundlePackage = pkgs.mesa.drivers;
|
||||
};
|
||||
fonts = {
|
||||
enable = true;
|
||||
# Preserve nixpak's default coverage and add Chinese/Japanese/Korean glyphs.
|
||||
fonts = with pkgs; [
|
||||
cantarell-fonts
|
||||
dejavu_fonts
|
||||
liberation_ttf
|
||||
gyre-fonts
|
||||
source-sans
|
||||
source-code-pro
|
||||
noto-fonts-color-emoji
|
||||
noto-fonts-cjk-sans
|
||||
];
|
||||
};
|
||||
locale.enable = true;
|
||||
dbus = {
|
||||
policies = {
|
||||
"${config.flatpak.appId}" = "own";
|
||||
"${config.flatpak.appId}.*" = "own";
|
||||
};
|
||||
rules.call."org.freedesktop.portal.Desktop" = [
|
||||
"org.freedesktop.DBus.Properties.Get@/org/freedesktop/portal/desktop"
|
||||
"org.freedesktop.DBus.Properties.GetAll@/org/freedesktop/portal/desktop"
|
||||
"org.freedesktop.portal.Settings.Read@/org/freedesktop/portal/desktop"
|
||||
"org.freedesktop.portal.Settings.ReadAll@/org/freedesktop/portal/desktop"
|
||||
"org.freedesktop.portal.Request.Close@/org/freedesktop/portal/desktop/request/*"
|
||||
"org.freedesktop.portal.Session.Close@/org/freedesktop/portal/desktop/session/*"
|
||||
];
|
||||
rules.broadcast."org.freedesktop.portal.Desktop" = [
|
||||
"org.freedesktop.portal.Settings.SettingChanged@/org/freedesktop/portal/desktop"
|
||||
"@/org/freedesktop/portal/desktop/*"
|
||||
];
|
||||
};
|
||||
bubblewrap = {
|
||||
network = lib.mkDefault false;
|
||||
newSession = true;
|
||||
dieWithParent = true;
|
||||
clearEnv = true;
|
||||
sockets = {
|
||||
wayland = true;
|
||||
x11 = lib.mkDefault false;
|
||||
pulse = lib.mkDefault true;
|
||||
pipewire = false;
|
||||
};
|
||||
bind.rw = with sloth; [
|
||||
[
|
||||
(mkdir appCacheDir)
|
||||
xdgCacheHome
|
||||
]
|
||||
[
|
||||
(mkdir appConfigDir)
|
||||
xdgConfigHome
|
||||
]
|
||||
[
|
||||
(mkdir appDataDir)
|
||||
xdgDataHome
|
||||
]
|
||||
[
|
||||
(mkdir (concat' appDir "/state"))
|
||||
xdgStateHome
|
||||
]
|
||||
(concat' runtimeDir "/doc")
|
||||
# Chrome/Firefox/Telegram/etc. use /tmp for single-instance IPC. Share
|
||||
# this directory across launches of the same app only; app IDs differ.
|
||||
[
|
||||
(mkdir (concat' runtimeDir "/nixpak-${config.flatpak.appId}-tmp"))
|
||||
"/tmp"
|
||||
]
|
||||
];
|
||||
bind.ro = [
|
||||
"/etc/localtime"
|
||||
"/etc/zoneinfo"
|
||||
(sloth.concat' sloth.xdgConfigHome "/gtk-2.0")
|
||||
(sloth.concat' sloth.xdgConfigHome "/gtk-3.0")
|
||||
(sloth.concat' sloth.xdgConfigHome "/gtk-4.0")
|
||||
(sloth.concat' sloth.xdgConfigHome "/kdeglobals")
|
||||
"/sys/devices/platform"
|
||||
]
|
||||
++ lib.optionals config.gpu.enable [
|
||||
"/etc/egl"
|
||||
"/etc/static/egl"
|
||||
];
|
||||
# nixpak's NixOS GPU provider maps DRM nodes, but NVIDIA also needs its
|
||||
# dedicated interfaces. Missing nodes are skipped by --dev-bind-try.
|
||||
bind.dev = lib.optionals config.gpu.enable [
|
||||
"/dev/nvidia0"
|
||||
"/dev/nvidiactl"
|
||||
"/dev/nvidia-modeset"
|
||||
"/dev/nvidia-uvm"
|
||||
];
|
||||
tmpfs = [
|
||||
"/dev/shm"
|
||||
];
|
||||
env = {
|
||||
HOME = sloth.homeDir;
|
||||
USER = sloth.env "USER";
|
||||
XDG_RUNTIME_DIR = sloth.runtimeDir;
|
||||
XDG_CACHE_HOME = sloth.xdgCacheHome;
|
||||
XDG_CONFIG_HOME = sloth.xdgConfigHome;
|
||||
XDG_DATA_HOME = sloth.xdgDataHome;
|
||||
XDG_STATE_HOME = sloth.xdgStateHome;
|
||||
XDG_SESSION_TYPE = "wayland";
|
||||
# Selects the Wayland path for portal clients.
|
||||
XDG_CURRENT_DESKTOP = sloth.envOr "XDG_CURRENT_DESKTOP" "niri";
|
||||
WAYLAND_DISPLAY = sloth.envOr "WAYLAND_DISPLAY" "wayland-0";
|
||||
NIXOS_OZONE_WL = "1";
|
||||
GTK_USE_PORTAL = "1";
|
||||
GTK_A11Y = sloth.envOr "GTK_A11Y" "0";
|
||||
XDG_DATA_DIRS = lib.makeSearchPath "share" [
|
||||
pkgs.papirus-icon-theme
|
||||
pkgs.bibata-cursors
|
||||
pkgs.shared-mime-info
|
||||
];
|
||||
XCURSOR_PATH = "${pkgs.bibata-cursors}/share/icons:${pkgs.bibata-cursors}/share/pixmaps";
|
||||
# Fontconfig configuration mounted by nixpak fonts.enable.
|
||||
FONTCONFIG_FILE = "/etc/fonts/fonts.conf";
|
||||
NIXOS_XDG_OPEN_USE_PORTAL = "1";
|
||||
}
|
||||
// lib.genAttrs [
|
||||
"LANG"
|
||||
"LANGUAGE"
|
||||
"LC_ALL"
|
||||
"LC_ADDRESS"
|
||||
"LC_COLLATE"
|
||||
"LC_CTYPE"
|
||||
"LC_IDENTIFICATION"
|
||||
"LC_MEASUREMENT"
|
||||
"LC_MESSAGES"
|
||||
"LC_MONETARY"
|
||||
"LC_NAME"
|
||||
"LC_NUMERIC"
|
||||
"LC_PAPER"
|
||||
"LC_TELEPHONE"
|
||||
"LC_TIME"
|
||||
"XCURSOR_THEME"
|
||||
"XCURSOR_SIZE"
|
||||
"NO_AT_BRIDGE"
|
||||
"QT_QPA_PLATFORMTHEME"
|
||||
"QT_WAYLAND_DISABLE_WINDOWDECORATION"
|
||||
] (name: sloth.envOr name "");
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Non-portal desktop services are separate opt-ins from portal capabilities.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
sloth,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.desktopIntegration;
|
||||
appId = config.flatpak.appId;
|
||||
shortName = lib.last (lib.splitString "." appId);
|
||||
in
|
||||
{
|
||||
options.desktopIntegration = {
|
||||
notifications = lib.mkEnableOption "native desktop notifications";
|
||||
tray = lib.mkEnableOption "StatusNotifier tray integration";
|
||||
mpris = lib.mkEnableOption "MPRIS media controls for this application";
|
||||
accessibility = lib.mkEnableOption "the accessibility bus and AT-SPI socket";
|
||||
};
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf cfg.notifications {
|
||||
dbus.policies."org.freedesktop.Notifications" = "talk";
|
||||
})
|
||||
(lib.mkIf cfg.tray {
|
||||
dbus.policies = {
|
||||
# Qt probes NameHasOwner/GetNameOwner before registering its SNI.
|
||||
"org.freedesktop.DBus" = "talk";
|
||||
"org.kde.StatusNotifierWatcher" = "talk";
|
||||
}
|
||||
# SNI name is <pid>-<Qt tray instance ID>; WeChat 4.1.13 uses <pid>-2.
|
||||
// builtins.listToAttrs (
|
||||
lib.concatMap (
|
||||
id:
|
||||
map (
|
||||
instance: lib.nameValuePair "org.kde.StatusNotifierItem-${toString id}-${toString instance}" "own"
|
||||
) (lib.range 1 5)
|
||||
) (lib.range 2 5)
|
||||
);
|
||||
})
|
||||
(lib.mkIf cfg.mpris {
|
||||
# Chromium's MPRIS name is lowercase even though its app ID ends in
|
||||
# Chromium; retain the full ID and toolkit spelling as well.
|
||||
dbus.policies = builtins.listToAttrs (
|
||||
lib.concatMap
|
||||
(name: [
|
||||
(lib.nameValuePair "org.mpris.MediaPlayer2.${name}" "own")
|
||||
(lib.nameValuePair "org.mpris.MediaPlayer2.${name}.*" "own")
|
||||
])
|
||||
(
|
||||
lib.unique [
|
||||
appId
|
||||
shortName
|
||||
(lib.toLower shortName)
|
||||
]
|
||||
)
|
||||
);
|
||||
})
|
||||
(lib.mkIf cfg.accessibility {
|
||||
dbus.policies."org.a11y.Bus" = "see";
|
||||
dbus.rules.call."org.a11y.Bus" = [
|
||||
"org.a11y.Bus.GetAddress@/org/a11y/bus"
|
||||
"org.freedesktop.DBus.Properties.Get@/org/a11y/bus"
|
||||
];
|
||||
bubblewrap.bind.ro = [ (sloth.concat' sloth.runtimeDir "/at-spi/bus") ];
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
# Fork of nixpak/pkgs's gui-base.nix: the shared GUI runtime plumbing (GPU,
|
||||
# fonts, desktop caches, /dev/shm, and the audio/display sockets) that lets a
|
||||
# GUI app run under bubblewrap. Paths bound here must not be bound again in
|
||||
# ./custom-policy.nix or the app configs: nixpak emits read-only binds last, so
|
||||
# a duplicate read-only bind would silently shadow a writable one.
|
||||
#
|
||||
# https://github.com/nixpak/pkgs/blob/master/pkgs/modules/gui-base.nix
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
sloth,
|
||||
...
|
||||
}:
|
||||
let
|
||||
envSuffix = envKey: suffix: sloth.concat' (sloth.env envKey) suffix;
|
||||
# nixpak cannot pass a variable only when it is set, so an unset one arrives
|
||||
# empty; only list variables whose readers treat empty as unset.
|
||||
passHostEnv = names: lib.genAttrs names (name: sloth.envOr name "");
|
||||
# cursor & icon's theme should be the same as the host's one.
|
||||
cursorTheme = pkgs.bibata-cursors;
|
||||
iconTheme = pkgs.papirus-icon-theme;
|
||||
in
|
||||
{
|
||||
config = {
|
||||
dbus.policies = {
|
||||
"${config.flatpak.appId}" = "own";
|
||||
# we add other policies in ./custom-policy.nix
|
||||
};
|
||||
# https://github.com/nixpak/nixpak/blob/master/modules/gpu.nix
|
||||
# The NixOS provider exposes host drivers at /run/opengl-driver (read-only)
|
||||
# and all /dev/dri nodes (device access), including Intel, AMD and Asahi DRM devices.
|
||||
# Nixpak also maps /sys/dev/char and /sys/devices/pci0000:00 read-only for discovery.
|
||||
# These mappings grant access; they do not select the GPU used by an application.
|
||||
gpu = {
|
||||
enable = lib.mkDefault true;
|
||||
provider = "nixos";
|
||||
bundlePackage = pkgs.mesa.drivers;
|
||||
};
|
||||
# https://github.com/nixpak/nixpak/blob/master/modules/gui/fonts.nix
|
||||
# it works not well, bind system's /etc/fonts directly instead
|
||||
fonts.enable = false;
|
||||
# https://github.com/nixpak/nixpak/blob/master/modules/locale.nix
|
||||
locale.enable = true;
|
||||
bubblewrap = {
|
||||
# Nixpak uses --ro-bind-try / --dev-bind-try: absent host paths are skipped,
|
||||
# so shared mappings work on Intel/NVIDIA hosts and Apple Silicon alike.
|
||||
network = lib.mkDefault false;
|
||||
# setsid keeps the app off the launching terminal (TIOCSTI), and the
|
||||
# sandbox goes down with the launcher instead of outliving it.
|
||||
newSession = true;
|
||||
dieWithParent = true;
|
||||
|
||||
# nixpak binds sockets read-only, which is sufficient: connect() only needs
|
||||
# the socket inode's write bit, and the kernel's read-only-filesystem check
|
||||
# does not apply to sockets. Every app here is Wayland-only, so X11 stays
|
||||
# disabled (the nixpak default, spelled out for intent) and PipeWire is
|
||||
# shared for screensharing.
|
||||
sockets = {
|
||||
wayland = true;
|
||||
pipewire = true;
|
||||
pulse = true;
|
||||
x11 = false;
|
||||
};
|
||||
|
||||
bind.rw = [
|
||||
[
|
||||
(envSuffix "HOME" "/.var/app/${config.flatpak.appId}/cache")
|
||||
sloth.xdgCacheHome
|
||||
]
|
||||
(sloth.concat' sloth.xdgCacheHome "/fontconfig")
|
||||
(sloth.concat' sloth.xdgCacheHome "/mesa_shader_cache")
|
||||
|
||||
# gvfsd is a directory that holds FUSE mounts, not a socket, so it must
|
||||
# stay writable.
|
||||
(envSuffix "XDG_RUNTIME_DIR" "/gvfsd")
|
||||
];
|
||||
bind.ro = [
|
||||
# Sockets only need to be connectable, and connect() works on a read-only
|
||||
# bind (see the note on `sockets` above).
|
||||
(envSuffix "XDG_RUNTIME_DIR" "/at-spi/bus")
|
||||
|
||||
(sloth.concat' sloth.xdgConfigHome "/gtk-2.0")
|
||||
(sloth.concat' sloth.xdgConfigHome "/gtk-3.0")
|
||||
(sloth.concat' sloth.xdgConfigHome "/gtk-4.0")
|
||||
(sloth.concat' sloth.xdgConfigHome "/fontconfig")
|
||||
|
||||
"/etc/fonts" # for fontconfig
|
||||
"/etc/localtime" # this is a symlink to /etc/zoneinfo/xxx
|
||||
"/etc/zoneinfo"
|
||||
|
||||
# Host EGL configuration (read-only); needed by some driver setups.
|
||||
"/etc/egl"
|
||||
"/etc/static/egl"
|
||||
|
||||
# Asahi GPU discovery follows /sys/dev/char links into platform devices.
|
||||
# Expose their targets read-only; this includes non-GPU platform metadata too.
|
||||
"/sys/devices/platform"
|
||||
];
|
||||
bind.dev = [
|
||||
"/dev/shm" # Shared Memory
|
||||
|
||||
# Additional NVIDIA interfaces, retained for primary-GPU and offload use.
|
||||
# Unlike /dev/dri above, numbered NVIDIA nodes are not mapped as a directory.
|
||||
"/dev/nvidia0" # GPU index 0; additional GPUs need explicit mappings.
|
||||
"/dev/nvidiactl" # Driver control interface.
|
||||
"/dev/nvidia-modeset" # Display mode setting.
|
||||
"/dev/nvidia-uvm" # Unified virtual memory.
|
||||
];
|
||||
|
||||
tmpfs = [
|
||||
"/tmp"
|
||||
];
|
||||
|
||||
# Start from an empty environment so host secrets (tokens, credentials)
|
||||
# stay outside; everything the apps need is listed here.
|
||||
clearEnv = true;
|
||||
env = {
|
||||
HOME = sloth.homeDir;
|
||||
USER = sloth.env "USER";
|
||||
XDG_RUNTIME_DIR = sloth.runtimeDir;
|
||||
XDG_CACHE_HOME = sloth.xdgCacheHome;
|
||||
XDG_CONFIG_HOME = sloth.xdgConfigHome;
|
||||
XDG_DATA_HOME = sloth.xdgDataHome;
|
||||
XDG_STATE_HOME = sloth.xdgStateHome;
|
||||
# Same fallback as nixpak's Wayland socket bind, so they always agree.
|
||||
WAYLAND_DISPLAY = sloth.envOr "WAYLAND_DISPLAY" "wayland-0";
|
||||
XDG_SESSION_TYPE = "wayland";
|
||||
# GTK reads an empty GTK_A11Y as an unknown backend; "0" is its unset value.
|
||||
GTK_A11Y = sloth.envOr "GTK_A11Y" "0";
|
||||
|
||||
XDG_DATA_DIRS = lib.mkForce (
|
||||
lib.makeSearchPath "share" [
|
||||
iconTheme
|
||||
cursorTheme
|
||||
pkgs.shared-mime-info
|
||||
]
|
||||
);
|
||||
XCURSOR_PATH = lib.mkForce (
|
||||
lib.concatStringsSep ":" [
|
||||
"${cursorTheme}/share/icons"
|
||||
"${cursorTheme}/share/pixmaps"
|
||||
]
|
||||
);
|
||||
}
|
||||
// passHostEnv [
|
||||
"LANG"
|
||||
"LANGUAGE"
|
||||
"LC_ALL"
|
||||
"LC_ADDRESS"
|
||||
"LC_COLLATE"
|
||||
"LC_CTYPE"
|
||||
"LC_IDENTIFICATION"
|
||||
"LC_MEASUREMENT"
|
||||
"LC_MESSAGES"
|
||||
"LC_MONETARY"
|
||||
"LC_NAME"
|
||||
"LC_NUMERIC"
|
||||
"LC_PAPER"
|
||||
"LC_TELEPHONE"
|
||||
"LC_TIME"
|
||||
|
||||
"NO_AT_BRIDGE"
|
||||
"NIXOS_XDG_OPEN_USE_PORTAL"
|
||||
"NIXOS_OZONE_WL" # QQ's wrapper adds its Wayland flags only when set
|
||||
"XCURSOR_THEME"
|
||||
"XCURSOR_SIZE"
|
||||
"QT_QPA_PLATFORMTHEME"
|
||||
"QT_WAYLAND_DISABLE_WINDOWDECORATION"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# xdg-dbus-proxy rules target interfaces on the Desktop bus name, not fictitious
|
||||
# per-interface bus names. Each capability is opt-in.
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
groups = {
|
||||
fileChooser = [
|
||||
"FileChooser.OpenFile"
|
||||
"FileChooser.SaveFile"
|
||||
"FileChooser.SaveFiles"
|
||||
];
|
||||
openUri = [
|
||||
"OpenURI.OpenURI"
|
||||
"OpenURI.OpenFile"
|
||||
"OpenURI.OpenDirectory"
|
||||
];
|
||||
notifications = [
|
||||
"Notification.AddNotification"
|
||||
"Notification.RemoveNotification"
|
||||
];
|
||||
screenCast = [
|
||||
"ScreenCast.CreateSession"
|
||||
"ScreenCast.SelectSources"
|
||||
"ScreenCast.Start"
|
||||
"ScreenCast.OpenPipeWireRemote"
|
||||
];
|
||||
camera = [
|
||||
"Camera.AccessCamera"
|
||||
"Camera.OpenPipeWireRemote"
|
||||
];
|
||||
print = [
|
||||
"Print.PreparePrint"
|
||||
"Print.Print"
|
||||
];
|
||||
secret = [ "Secret.RetrieveSecret" ];
|
||||
inhibit = [
|
||||
"Inhibit.Inhibit"
|
||||
"Inhibit.CreateMonitor"
|
||||
"Inhibit.QueryEndResponse"
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
options.portalGroups =
|
||||
lib.mapAttrs (name: _: lib.mkEnableOption "the ${name} desktop portal methods") groups
|
||||
// {
|
||||
inputMethod = lib.mkEnableOption "Fcitx and IBus input method portals";
|
||||
};
|
||||
config = {
|
||||
dbus.rules.call."org.freedesktop.portal.Desktop" = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: methods:
|
||||
lib.optionals config.portalGroups.${name} (
|
||||
map (method: "org.freedesktop.portal.${method}@/org/freedesktop/portal/desktop") methods
|
||||
)
|
||||
) groups
|
||||
);
|
||||
dbus.policies = lib.mkIf config.portalGroups.inputMethod {
|
||||
"org.freedesktop.portal.Fcitx" = "talk";
|
||||
"org.freedesktop.portal.IBus" = "talk";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# The existing FHS layer owns WeChat's HOME remapping and xwechat_files link.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
sloth,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./desktop-base.nix
|
||||
./network.nix
|
||||
];
|
||||
pasta.mode = "isolate";
|
||||
desktopIntegration.tray = true;
|
||||
portalGroups = {
|
||||
fileChooser = true;
|
||||
openUri = true;
|
||||
notifications = true;
|
||||
screenCast = true;
|
||||
camera = true;
|
||||
inputMethod = true;
|
||||
};
|
||||
bubblewrap = {
|
||||
sockets.x11 = true;
|
||||
# libdbus aborts without /etc/machine-id; use a synthetic app-scoped value.
|
||||
bind.ro = [
|
||||
[
|
||||
(toString (
|
||||
pkgs.writeText "wechat-machine-id" "${
|
||||
builtins.substring 0 32 (builtins.hashString "sha256" config.flatpak.appId)
|
||||
}\n"
|
||||
))
|
||||
"/etc/machine-id"
|
||||
]
|
||||
];
|
||||
# The inner FHS /home reset replaces base XDG mounts with this backing tree.
|
||||
bind.rw = lib.mkForce [
|
||||
(sloth.mkdir (sloth.concat' sloth.xdgDocumentsDir "/WeChat_Data"))
|
||||
(sloth.concat' sloth.runtimeDir "/doc")
|
||||
[
|
||||
(sloth.mkdir (sloth.concat' sloth.runtimeDir "/nixpak-${config.flatpak.appId}-tmp"))
|
||||
"/tmp"
|
||||
]
|
||||
];
|
||||
env = {
|
||||
DISPLAY = sloth.envOr "DISPLAY" "";
|
||||
XAUTHORITY = sloth.envOr "XAUTHORITY" (sloth.concat' sloth.homeDir "/.Xauthority");
|
||||
XDG_DOCUMENTS_DIR = sloth.xdgDocumentsDir;
|
||||
# The existing pre-FHS script uses mkdir/ln before /usr/bin exists.
|
||||
PATH = lib.makeBinPath [ pkgs.coreutils ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Zoom keeps the upstream FHS runtime inside the outer nixpak boundary.
|
||||
{ lib, sloth, ... }:
|
||||
{
|
||||
imports = [
|
||||
./desktop-base.nix
|
||||
./network.nix
|
||||
];
|
||||
pasta.mode = "isolate";
|
||||
desktopIntegration.tray = true;
|
||||
portalGroups = {
|
||||
fileChooser = true;
|
||||
openUri = true;
|
||||
notifications = true;
|
||||
screenCast = true;
|
||||
camera = true;
|
||||
inhibit = true;
|
||||
inputMethod = true;
|
||||
};
|
||||
bubblewrap = {
|
||||
# Screen sharing enumerates DRM connectors through sysfs. Missing this
|
||||
# directory makes Zoom abort with std::filesystem_error.
|
||||
bind.ro = [ "/sys/class/drm" ];
|
||||
# ZoomLauncher selects xcb; the X11 connection retains X11's display trust.
|
||||
sockets.x11 = true;
|
||||
env = {
|
||||
DISPLAY = sloth.envOr "DISPLAY" "";
|
||||
XAUTHORITY = sloth.envOr "XAUTHORITY" (sloth.concat' sloth.homeDir "/.Xauthority");
|
||||
};
|
||||
bind.dev = [
|
||||
# Zoom 7.2.1 still opens V4L2 device nodes directly for webcam capture.
|
||||
"/dev/video0"
|
||||
"/dev/video1"
|
||||
];
|
||||
bind.rw = [
|
||||
(sloth.mkdir (sloth.concat' sloth.homeDir "/.zoom"))
|
||||
(sloth.concat' sloth.xdgConfigHome "/zoomus.conf")
|
||||
(sloth.concat' sloth.xdgConfigHome "/zoom.conf")
|
||||
(sloth.mkdir (sloth.concat' sloth.xdgDocumentsDir "/Zoom"))
|
||||
sloth.xdgDownloadDir
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -26,11 +26,26 @@ let
|
||||
flatpak.appId = appId;
|
||||
|
||||
imports = [
|
||||
./modules/gui-base.nix
|
||||
./modules/desktop-base.nix
|
||||
./modules/network.nix
|
||||
./modules/custom-policy.nix
|
||||
];
|
||||
|
||||
portalGroups = {
|
||||
fileChooser = true;
|
||||
openUri = true;
|
||||
notifications = true;
|
||||
screenCast = true;
|
||||
camera = true;
|
||||
inhibit = true;
|
||||
inputMethod = true;
|
||||
};
|
||||
desktopIntegration = {
|
||||
notifications = true;
|
||||
tray = true;
|
||||
mpris = true;
|
||||
accessibility = true;
|
||||
};
|
||||
|
||||
bubblewrap = {
|
||||
# To trace all the home files QQ accesses, you can use the following nushell command:
|
||||
# just trace-access qq
|
||||
|
||||
@@ -13,9 +13,8 @@ let
|
||||
{ sloth, ... }:
|
||||
{
|
||||
imports = [
|
||||
./modules/gui-base.nix
|
||||
./modules/desktop-base.nix
|
||||
./modules/network.nix
|
||||
./modules/custom-policy.nix
|
||||
];
|
||||
app.package = telegram-desktop;
|
||||
flatpak = {
|
||||
@@ -30,6 +29,22 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
portalGroups = {
|
||||
fileChooser = true;
|
||||
openUri = true;
|
||||
notifications = true;
|
||||
screenCast = true;
|
||||
camera = true;
|
||||
inhibit = true;
|
||||
inputMethod = true;
|
||||
};
|
||||
desktopIntegration = {
|
||||
notifications = true;
|
||||
tray = true;
|
||||
mpris = true;
|
||||
accessibility = true;
|
||||
};
|
||||
|
||||
bubblewrap = {
|
||||
# nixpak exposes /.flatpak-info, so Telegram uses FLATPAK_ID as its Wayland app ID.
|
||||
env.FLATPAK_ID = appId;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
lib,
|
||||
bwraps,
|
||||
mkNixPak,
|
||||
buildEnv,
|
||||
makeDesktopItem,
|
||||
runCommandLocal,
|
||||
...
|
||||
}:
|
||||
let
|
||||
appId = "com.tencent.WeChat";
|
||||
# As with Zoom, preserve the outer metadata file across the inner FHS layer.
|
||||
# Keep the existing AppImage runtime and WeChat_Data layout unchanged.
|
||||
package = bwraps.wechat.overrideAttrs (old: {
|
||||
# Refuse unexpected layouts before the existing ln -snf can modify them.
|
||||
extraPreBwrapCmds = ''
|
||||
wechat_link="''${XDG_DOCUMENTS_DIR}/WeChat_Data/home/xwechat_files"
|
||||
wechat_files="''${XDG_DOCUMENTS_DIR}/WeChat_Data/xwechat_files"
|
||||
if [[ -e "$wechat_link" || -L "$wechat_link" ]]; then
|
||||
if [[ ! -L "$wechat_link" || "$(readlink "$wechat_link")" != "$wechat_files" ]]; then
|
||||
echo 'WeChat: unexpected xwechat_files layout; refusing to change existing data.' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
''
|
||||
+ (old.extraPreBwrapCmds or "");
|
||||
extraBwrapArgs = (old.extraBwrapArgs or [ ]) ++ [
|
||||
"--ro-bind /.flatpak-info /.flatpak-info"
|
||||
# The inner /home reset must not hide an Xauthority file stored there.
|
||||
''--ro-bind-try "''${XAUTHORITY}" "''${XAUTHORITY}"''
|
||||
];
|
||||
});
|
||||
wrapped = mkNixPak {
|
||||
config = {
|
||||
imports = [ ./modules/wechat-policy.nix ];
|
||||
app = {
|
||||
inherit package;
|
||||
binPath = "bin/wechat";
|
||||
};
|
||||
flatpak = { inherit appId; };
|
||||
};
|
||||
};
|
||||
exe = lib.getExe wrapped.config.script;
|
||||
assets = runCommandLocal "wechat-desktop-assets" { } ''
|
||||
mkdir -p $out/share
|
||||
ln -s ${package}/share/pixmaps $out/share/pixmaps
|
||||
'';
|
||||
in
|
||||
buildEnv {
|
||||
inherit (wrapped.config.script) name passthru;
|
||||
meta = wrapped.config.script.meta // {
|
||||
mainProgram = "wechat";
|
||||
};
|
||||
paths = [
|
||||
wrapped.config.script
|
||||
assets
|
||||
(makeDesktopItem {
|
||||
name = "wechat";
|
||||
desktopName = "WeChat";
|
||||
exec = "${exe} %U";
|
||||
icon = "wechat";
|
||||
terminal = false;
|
||||
startupWMClass = "wechat";
|
||||
categories = [
|
||||
"Network"
|
||||
"InstantMessaging"
|
||||
];
|
||||
mimeTypes = [ "x-scheme-handler/weixin" ];
|
||||
extraConfig.X-Flatpak = appId;
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
lib,
|
||||
zoom-us,
|
||||
buildFHSEnv,
|
||||
mkNixPak,
|
||||
buildEnv,
|
||||
makeDesktopItem,
|
||||
runCommandLocal,
|
||||
...
|
||||
}:
|
||||
let
|
||||
appId = "us.zoom.Zoom";
|
||||
# Preserve the upstream FHS wrapper and /opt mount; its root mounts omit the
|
||||
# metadata file needed for portal identification inside the nested sandbox.
|
||||
package = zoom-us.override {
|
||||
xdgDesktopPortalSupport = true;
|
||||
buildFHSEnv =
|
||||
args:
|
||||
buildFHSEnv (
|
||||
args
|
||||
// {
|
||||
extraBwrapArgs = (args.extraBwrapArgs or [ ]) ++ [
|
||||
"--ro-bind /.flatpak-info /.flatpak-info"
|
||||
];
|
||||
}
|
||||
);
|
||||
};
|
||||
wrapped = mkNixPak {
|
||||
config = {
|
||||
imports = [ ./modules/zoom-policy.nix ];
|
||||
app = {
|
||||
inherit package;
|
||||
binPath = "bin/zoom";
|
||||
};
|
||||
flatpak = { inherit appId; };
|
||||
};
|
||||
};
|
||||
exe = lib.getExe wrapped.config.script;
|
||||
assets = runCommandLocal "zoom-desktop-assets" { } ''
|
||||
mkdir -p $out/share
|
||||
for directory in icons pixmaps; do
|
||||
if [ -d ${package}/share/$directory ]; then
|
||||
ln -s ${package}/share/$directory $out/share/$directory
|
||||
fi
|
||||
done
|
||||
'';
|
||||
in
|
||||
buildEnv {
|
||||
inherit (wrapped.config.script) name meta passthru;
|
||||
paths = [
|
||||
wrapped.config.script
|
||||
assets
|
||||
(makeDesktopItem {
|
||||
name = "Zoom";
|
||||
desktopName = "Zoom Workplace";
|
||||
comment = "Zoom Video Conference";
|
||||
exec = "${exe} %U";
|
||||
icon = "Zoom";
|
||||
terminal = false;
|
||||
startupWMClass = "zoom";
|
||||
categories = [ "Network" ];
|
||||
mimeTypes = [
|
||||
"x-scheme-handler/zoommtg"
|
||||
"x-scheme-handler/zoomus"
|
||||
"application/x-zoom"
|
||||
];
|
||||
extraConfig.X-Flatpak = appId;
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
# Browser runtime verification
|
||||
|
||||
- Run from the repository root as the Linux desktop user, with user D-Bus; no sudo or activation.
|
||||
- Tests use disposable profiles only, synthetic environment canaries, and no environment dumps.
|
||||
- **Keep HOME under `/var/tmp`:** the sandbox's private `/tmp` masks a HOME created there. Rendering
|
||||
reads a host-prepared HTML file through the profile bind; persistence requires a nonempty
|
||||
`Local State` file in the disposable host HOME.
|
||||
|
||||
## Reproduce
|
||||
|
||||
Build against the locked flake inputs:
|
||||
|
||||
```bash
|
||||
mkdir -p browser-validation
|
||||
for app in chromium google-chrome brave-origin microsoft-edge; do
|
||||
APP="$app" nix build --impure --out-link "browser-validation/$app" --expr '
|
||||
let
|
||||
flake = builtins.getFlake (toString ./.);
|
||||
pkgs = import flake.inputs.nixpkgs {
|
||||
system = builtins.currentSystem;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in pkgs.callPackage
|
||||
(./hardening/nixpaks + "/${builtins.getEnv "APP"}.nix")
|
||||
{
|
||||
mkNixPak = flake.inputs.nixpak.lib.nixpak {
|
||||
inherit pkgs;
|
||||
inherit (pkgs) lib;
|
||||
};
|
||||
}
|
||||
'
|
||||
done
|
||||
```
|
||||
|
||||
Run each test separately so a failing browser does not hide the other results:
|
||||
|
||||
```bash
|
||||
python3 hardening/tests/verify-chromium.py browser-validation/chromium/bin/chromium
|
||||
python3 hardening/tests/verify-chromium.py browser-validation/google-chrome/bin/google-chrome
|
||||
python3 hardening/tests/verify-chromium.py browser-validation/brave-origin/bin/brave-origin
|
||||
python3 hardening/tests/verify-chromium.py browser-validation/microsoft-edge/bin/microsoft-edge
|
||||
```
|
||||
|
||||
- Each launch has a 35-second limit; failures exit nonzero. Temporary HOME is cleaned up.
|
||||
- Intermediate JSON records process checks; final JSON appears only after all checks pass.
|
||||
|
||||
## Current measured results
|
||||
|
||||
Host: `ai`, x86_64 Linux. All four rebuilt after the shared policy/media opt-in changes and tested
|
||||
with `/var/tmp` HOME, explicit Fontconfig configuration, CJK fonts, and narrow NVIDIA mappings.
|
||||
|
||||
| Browser | Process/security checks | HTML rendering | Profile persistence | Host loopback | Overall |
|
||||
| ---------------------- | ----------------------- | --------------- | ------------------- | ------------- | -------- |
|
||||
| Chromium 156.0.8078.73 | Pass | Pass | Pass | Pass: allowed | **Pass** |
|
||||
| Chrome 156.0.8078.73 | Pass | Pass | Pass | Pass: blocked | **Pass** |
|
||||
| Brave Origin 1.98.60 | Pass | **35s timeout** | Not reached | Not reached | **Fail** |
|
||||
| Edge 156.0.4314.42 | Pass | **35s timeout** | Not reached | Not reached | **Fail** |
|
||||
|
||||
Measured evidence:
|
||||
|
||||
- All four build and have live renderers with `Seccomp=2`, `NoNewPrivs=1`; no sandbox-disabling
|
||||
flags are present.
|
||||
- Actual bwrap arguments pass user/PID namespace, effective network flag, clear environment, new
|
||||
session, parent-death, and private `/dev/shm` checks. No broad HOME/`/home`/`/dev`, SSH,
|
||||
system-bus, raw video, X11, GVfs, or direct PipeWire binds. Brave/Edge also have no browserpass
|
||||
credential or PulseAudio binds.
|
||||
- All nine canaries are absent: `NIXPAK_VALIDATION_CANARY`, `SSH_AUTH_SOCK`, `GPG_AGENT_INFO`, and
|
||||
upper/lowercase `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`.
|
||||
- Chromium/Chrome render the prepared file without Fontconfig errors and persist `Local State`. Host
|
||||
HTTP control succeeds; Chromium fetches its marker. Chrome reports `ERR_CONNECTION_REFUSED`,
|
||||
returns no marker, and sends zero requests to the host listener.
|
||||
- Brave/Edge stall on `--headless=new --dump-dom` despite `--timeout=10000`. Cause unresolved;
|
||||
system D-Bus/UPower warnings also occur in passing browsers. No upstream-package control was run,
|
||||
so this does not establish a wrapper regression.
|
||||
|
||||
## Current permission checks
|
||||
|
||||
- **Firefox/Chromium/Chrome:** evaluated media opt-in enables PulseAudio, camera/screencast portals,
|
||||
MPRIS, and inhibition. **Brave/Edge:** all disabled; live socket-bind checks pass.
|
||||
- Chromium/Chrome main processes enable `WaylandWindowDecorations,WebRtcPipeWireCamera`. Brave/Edge
|
||||
retain Wayland decorations and have **no camera feature**; all flag checks pass.
|
||||
- Firefox/Chromium share host networking and allow personal files; Chrome/Brave/Edge use pasta
|
||||
isolation without personal-file access. Firefox parity is evaluated, not runtime-tested here.
|
||||
- GPU nodes remain available for webpage rendering. Interactive audio/camera/capture, hardware
|
||||
acceleration, CJK/IME, portals, downloads, and browserpass remain unverified by this headless
|
||||
test.
|
||||
- Inventory: [hardening README](../README.md). Security limits: [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Cookie crypto
|
||||
|
||||
- Chromium-family wrappers enable `SecretPortalKeyProviderUseForEncryption` and receive only
|
||||
`org.freedesktop.portal.Secret.RetrieveSecret`; they do **not** get
|
||||
`--talk=org.freedesktop.secrets`.
|
||||
- A disposable-profile probe under `/var/tmp` served an HTTP `Set-Cookie`, then inspected the
|
||||
profile DB: the cookie prefix was `v12` (`763132`) and
|
||||
`Local State.os_crypt.portal.prev_init_success` was true.
|
||||
- If the portal is unavailable, Chromium can fall back to v10. That is weak; monitor portal startup
|
||||
Portal startup is a monitored degradation, not a fail-closed guarantee.
|
||||
- Firefox uses NSS (`key4.db`/`logins.json`) and `cookies.sqlite`, not Chrome OSCrypt. Set a Firefox
|
||||
Primary Password if saved-login at-rest protection matters.
|
||||
- Sources:
|
||||
[feature defaults](https://github.com/chromium/chromium/blob/156.0.8078.73/chrome/browser/browser_features.cc),
|
||||
[v12 provider](https://github.com/chromium/chromium/blob/156.0.8078.73/components/os_crypt/async/browser/secret_portal_key_provider.cc),
|
||||
[prefix decryption](https://github.com/chromium/chromium/blob/156.0.8078.73/components/os_crypt/async/common/encryptor.cc).
|
||||
|
||||
## Check these files
|
||||
|
||||
```bash
|
||||
ruff format --check hardening/tests/verify-chromium.py
|
||||
ruff check hardening/tests/verify-chromium.py
|
||||
prettier --check hardening/tests/browser-runtime.md
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Manual Chrome media smoke test
|
||||
|
||||
Use the official WebRTC samples. They require no account and do not upload the stream; the preview
|
||||
stays local. Chrome uses pasta-isolated loopback, so a test server on the host is intentionally not
|
||||
reachable.
|
||||
|
||||
- Camera + microphone: <https://webrtc.github.io/samples/src/content/getusermedia/gum/>
|
||||
- Click **Start**, accept Chrome and portal prompts, verify local video preview and audio level.
|
||||
- Screen sharing: <https://webrtc.github.io/samples/src/content/getusermedia/getdisplaymedia/>
|
||||
- Click **Start**, choose a window/screen, verify preview, then **Stop**.
|
||||
- Files: use any site's upload control, then download the same file; confirm the file chooser and
|
||||
Downloads work.
|
||||
@@ -0,0 +1,221 @@
|
||||
# Evaluate with the same pkgs/mkNixPak as the wrappers. Returns true or identifies
|
||||
# the violated policy boundary; does not build or launch an application.
|
||||
{ pkgs, mkNixPak }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
load = name: (pkgs.callPackage (../nixpaks + "/${name}.nix") { inherit mkNixPak; }).config;
|
||||
firefox = load "firefox";
|
||||
chromium = load "chromium";
|
||||
chrome = load "google-chrome";
|
||||
restrictedBrowsers = map load [
|
||||
"brave-origin"
|
||||
"microsoft-edge"
|
||||
];
|
||||
chromiumFamily = map load [
|
||||
"chromium"
|
||||
"google-chrome"
|
||||
"brave-origin"
|
||||
"microsoft-edge"
|
||||
];
|
||||
browsers = map load [
|
||||
"firefox"
|
||||
"chromium"
|
||||
"google-chrome"
|
||||
"brave-origin"
|
||||
"microsoft-edge"
|
||||
];
|
||||
nonBrowserApps = map load [
|
||||
"qq"
|
||||
"telegram-desktop"
|
||||
"zoom"
|
||||
"wechat"
|
||||
];
|
||||
browserAndChatApps =
|
||||
browsers
|
||||
++ map load [
|
||||
"qq"
|
||||
"telegram-desktop"
|
||||
];
|
||||
apps = browsers ++ nonBrowserApps;
|
||||
# Inspect effective proxy arguments, not just capability switches: an extra
|
||||
# direct service grant could otherwise bypass a disabled portal group.
|
||||
forbiddenDbus =
|
||||
argument:
|
||||
lib.any (name: lib.hasInfix name argument) [
|
||||
"org.freedesktop.portal.Usb"
|
||||
"org.freedesktop.portal.RemoteDesktop"
|
||||
"org.gnome.Shell.Screencast"
|
||||
]
|
||||
|| argument == "--talk=org.freedesktop.portal.Desktop";
|
||||
withoutSecretPortal =
|
||||
calls:
|
||||
calls
|
||||
// {
|
||||
"org.freedesktop.portal.Desktop" = lib.filter (
|
||||
arg: !lib.hasInfix "org.freedesktop.portal.Secret" arg
|
||||
) calls."org.freedesktop.portal.Desktop";
|
||||
};
|
||||
mountStrings =
|
||||
app: builtins.filter builtins.isString (lib.flatten (with app.bubblewrap.bind; rw ++ ro ++ dev));
|
||||
forbiddenMount =
|
||||
path:
|
||||
path == "/dev"
|
||||
|| lib.hasPrefix "/dev/video" path
|
||||
|| lib.hasPrefix "/dev/bus/usb" path
|
||||
|| path == "/run/dbus"
|
||||
|| path == "/run/dbus/system_bus_socket";
|
||||
checks = {
|
||||
personalBrowserCapabilitiesMatch =
|
||||
firefox.browserPermissions.media == chromium.browserPermissions.media
|
||||
&& firefox.browserPermissions.personalFiles == chromium.browserPermissions.personalFiles
|
||||
&& firefox.browserPermissions.browserpass == chromium.browserPermissions.browserpass
|
||||
&&
|
||||
(removeAttrs firefox.portalGroups [ "secret" ]) == (removeAttrs chromium.portalGroups [ "secret" ])
|
||||
&& firefox.desktopIntegration == chromium.desktopIntegration;
|
||||
personalBrowserPortalCallsMatch =
|
||||
withoutSecretPortal firefox.dbus.rules.call == withoutSecretPortal chromium.dbus.rules.call;
|
||||
personalBrowserHostLoopback = lib.all (app: app.bubblewrap.network && !app.pasta.enable) [
|
||||
firefox
|
||||
chromium
|
||||
];
|
||||
workBrowserIsolatedLoopback =
|
||||
chrome.bubblewrap.network
|
||||
&& chrome.pasta.enable
|
||||
&& chrome.pasta.mode == "isolate"
|
||||
&& lib.elem "--no-map-gw" chrome.pasta.args;
|
||||
zoomDrmSysfs = lib.elem "/sys/class/drm" (load "zoom").bubblewrap.bind.ro;
|
||||
zoomCameraDevices =
|
||||
let
|
||||
zoom = load "zoom";
|
||||
in
|
||||
lib.all (dev: lib.elem dev zoom.bubblewrap.bind.dev) [
|
||||
"/dev/video0"
|
||||
"/dev/video1"
|
||||
];
|
||||
fhsTrayIntegration =
|
||||
lib.all
|
||||
(
|
||||
app:
|
||||
app.desktopIntegration.tray
|
||||
&& app.dbus.policies."org.freedesktop.DBus" == "talk"
|
||||
&& app.dbus.policies."org.kde.StatusNotifierWatcher" == "talk"
|
||||
&& app.dbus.policies."org.kde.StatusNotifierItem-2-1" == "own"
|
||||
&& app.dbus.policies."org.kde.StatusNotifierItem-2-2" == "own"
|
||||
)
|
||||
(
|
||||
map load [
|
||||
"wechat"
|
||||
"zoom"
|
||||
]
|
||||
);
|
||||
appTmpIsolation =
|
||||
let
|
||||
tmpPath =
|
||||
app:
|
||||
lib.findFirst (
|
||||
entry: builtins.isList entry && builtins.length entry == 2 && builtins.elem "/tmp" entry
|
||||
) null app.bubblewrap.bind.rw;
|
||||
paths = map tmpPath apps;
|
||||
in
|
||||
lib.all (path: builtins.isList path && builtins.length path == 2) paths
|
||||
&& lib.length (lib.unique (map builtins.head paths)) == builtins.length apps;
|
||||
workBrowserMedia =
|
||||
chrome.portalGroups.camera
|
||||
&& chrome.portalGroups.screenCast
|
||||
&& chrome.bubblewrap.sockets.pulse
|
||||
&& chrome.gpu.enable
|
||||
&& lib.elem "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.Camera.OpenPipeWireRemote@/org/freedesktop/portal/desktop" chrome.dbus.args
|
||||
&& lib.elem "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.ScreenCast.OpenPipeWireRemote@/org/freedesktop/portal/desktop" chrome.dbus.args;
|
||||
secretPortalScope =
|
||||
lib.all (
|
||||
app:
|
||||
app.browserPermissions.secretPortal
|
||||
&& lib.any (
|
||||
arg:
|
||||
arg
|
||||
== "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.Secret.RetrieveSecret@/org/freedesktop/portal/desktop"
|
||||
) app.dbus.args
|
||||
&& !lib.any (arg: arg == "--talk=org.freedesktop.secrets") app.dbus.args
|
||||
) chromiumFamily
|
||||
&& !firefox.browserPermissions.secretPortal
|
||||
&& lib.all (
|
||||
app:
|
||||
!(app.browserPermissions.secretPortal or false)
|
||||
&& !lib.any (arg: lib.hasInfix "org.freedesktop.portal.Secret" arg) app.dbus.args
|
||||
) nonBrowserApps;
|
||||
mediaScope =
|
||||
lib.all
|
||||
(
|
||||
app:
|
||||
app.browserPermissions.media
|
||||
&& app.bubblewrap.sockets.pulse
|
||||
&& app.portalGroups.camera
|
||||
&& app.portalGroups.screenCast
|
||||
)
|
||||
[
|
||||
firefox
|
||||
chromium
|
||||
chrome
|
||||
];
|
||||
restrictedBrowserMedia = lib.all (
|
||||
app:
|
||||
!app.browserPermissions.media
|
||||
&& !app.bubblewrap.sockets.pulse
|
||||
&& !app.bubblewrap.sockets.pipewire
|
||||
&& !app.portalGroups.camera
|
||||
&& !app.portalGroups.screenCast
|
||||
&& !app.desktopIntegration.mpris
|
||||
&& !lib.any (
|
||||
arg:
|
||||
lib.hasInfix "org.freedesktop.portal.Camera" arg
|
||||
|| lib.hasInfix "org.freedesktop.portal.ScreenCast" arg
|
||||
|| lib.hasInfix "org.mpris.MediaPlayer2" arg
|
||||
) app.dbus.args
|
||||
&& !lib.any (path: lib.hasPrefix "/dev/snd" path) (mountStrings app)
|
||||
) restrictedBrowsers;
|
||||
personalFileScope =
|
||||
firefox.browserPermissions.personalFiles
|
||||
&& chromium.browserPermissions.personalFiles
|
||||
&& !chrome.browserPermissions.personalFiles;
|
||||
browserpassScope =
|
||||
lib.all (app: app.browserPermissions.browserpass) [
|
||||
firefox
|
||||
chromium
|
||||
chrome
|
||||
]
|
||||
&& lib.all (app: !app.browserPermissions.browserpass) (
|
||||
map load [
|
||||
"brave-origin"
|
||||
"microsoft-edge"
|
||||
]
|
||||
);
|
||||
deniedServices = lib.all (app: !lib.any forbiddenDbus app.dbus.args) apps;
|
||||
noBroadKeyring = lib.all (
|
||||
app: !lib.any (arg: arg == "--talk=org.freedesktop.secrets") app.dbus.args
|
||||
) apps;
|
||||
deniedDevicesAndSystemBus =
|
||||
let
|
||||
zoomVideoDevices = [
|
||||
"/dev/video0"
|
||||
"/dev/video1"
|
||||
];
|
||||
forbiddenMountFor =
|
||||
app:
|
||||
if app.flatpak.appId == "us.zoom.Zoom" then
|
||||
path: forbiddenMount path && !lib.elem path zoomVideoDevices
|
||||
else
|
||||
forbiddenMount;
|
||||
in
|
||||
lib.all (app: !lib.any (forbiddenMountFor app) (mountStrings app)) apps;
|
||||
filteredEnvironmentAndMedia = lib.all (
|
||||
app:
|
||||
app.bubblewrap.clearEnv
|
||||
&& app.dbus.enable
|
||||
&& !app.bubblewrap.sockets.pipewire
|
||||
&& !app.bubblewrap.sockets.x11
|
||||
) browserAndChatApps;
|
||||
};
|
||||
in
|
||||
lib.all (name: checks.${name} || throw "nixpak policy check failed: ${name}") (
|
||||
builtins.attrNames checks
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Unified desktop-policy validation
|
||||
|
||||
Validation on the Linux `ai` host, using the pinned nixpkgs and nixpak inputs. These checks use
|
||||
disposable profiles and do not read existing browser or chat accounts.
|
||||
|
||||
| Check | Result | Scope |
|
||||
| --------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Firefox, QQ, Telegram derivations | Pass | All three wrappers evaluated and built after conversion to the shared desktop policy. |
|
||||
| Browser policy regression checks | Pass | `policy.nix` returned `true`; covers Firefox/Chromium capability and portal-call parity, host loopback, Chrome isolated networking and media grants, browserpass scope, and forbidden service/device access. |
|
||||
| Firefox headless screenshot | Pass | Exit status 0; `about:blank` produced a PNG with a fresh profile and HOME under `/var/tmp`. |
|
||||
| QQ startup | Partial | With a disposable HOME and `--ozone-platform=headless`, initialized private QQ data and stayed running for 12 seconds until explicitly terminated. Headless EGL/GPU initialization errors occurred. No account login or media function was tested. |
|
||||
| Telegram runtime | Not run | Evaluation and build only. |
|
||||
|
||||
The Firefox invocation used the built wrapper with
|
||||
`--headless --no-remote --profile "$HOME/.mozilla/test" --screenshot "$HOME/Downloads/smoke.png" about:blank`.
|
||||
The disposable environment retained only the launcher's PATH, USER, locale, Wayland display and
|
||||
session-bus/runtime-directory variables, with HOME and all XDG storage paths redirected to the
|
||||
temporary tree. Existing account profiles were not mounted. The temporary tree was removed after the
|
||||
process exited.
|
||||
|
||||
Use `/var/tmp`, not `/tmp`, for disposable host profiles. Nixpak emits the sandbox's private `/tmp`
|
||||
mount after filesystem binds; a profile prepared under host `/tmp` is hidden by that mount. An
|
||||
application that creates a fresh profile inside the resulting tmpfs can appear to work without
|
||||
testing persistence.
|
||||
|
||||
The common policy replaces the earlier direct camera-device and PipeWire-socket grants with explicit
|
||||
Camera and ScreenCast portal methods. QQ and Telegram retain their private XDG data and existing
|
||||
personal-file mappings, and opt into notifications, tray, MPRIS and accessibility. Firefox retains
|
||||
its `.mozilla` profile and MPRIS artwork mounts and shares browser capability definitions with
|
||||
Chromium.
|
||||
|
||||
Interactive validation remains necessary for camera capture, microphone input, video playback,
|
||||
desktop/window sharing, file chooser/save operations, IME, tray, notifications and media controls.
|
||||
The QQ headless startup is not evidence that those functions work. Browser encrypted-login behavior
|
||||
is covered in `browser-runtime.md`.
|
||||
Executable
+416
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Runtime smoke test: verify a built nixpak Chromium without existing profiles.
|
||||
|
||||
Usage: python3 hardening/tests/verify-chromium.py /nix/store/.../bin/chromium
|
||||
Requires Linux procfs and an active user D-Bus session. Uses only stdlib.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def processes():
|
||||
result = {}
|
||||
for path in Path("/proc").glob("[0-9]*"):
|
||||
try:
|
||||
status = dict(
|
||||
line.split(":", 1)
|
||||
for line in (path / "status").read_text().splitlines()
|
||||
if ":" in line
|
||||
)
|
||||
args = (path / "cmdline").read_bytes().split(b"\0")
|
||||
result[int(path.name)] = (int(status["PPid"]), status, args)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def descendants(root, table):
|
||||
found = {root}
|
||||
while True:
|
||||
more = {pid for pid, (parent, _, _) in table.items() if parent in found}
|
||||
if more <= found:
|
||||
return found
|
||||
found |= more
|
||||
|
||||
|
||||
def run_browser(command, env):
|
||||
try:
|
||||
return subprocess.run(
|
||||
command, check=False, env=env, capture_output=True, timeout=35
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
detail = (error.stderr or b"").decode(errors="replace")[-6000:]
|
||||
raise AssertionError(
|
||||
"Disposable headless command timed out:\n" + detail
|
||||
) from None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("browser", type=Path)
|
||||
args = parser.parse_args()
|
||||
browser = str(args.browser.absolute())
|
||||
profile = {
|
||||
"chromium": "chromium",
|
||||
"google-chrome": "google-chrome",
|
||||
"microsoft-edge": "microsoft-edge",
|
||||
"brave-origin": "BraveSoftware/Brave-Origin",
|
||||
}[args.browser.name]
|
||||
media = args.browser.name in {"chromium", "google-chrome"}
|
||||
report = {}
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nixpak-chromium-test-", dir="/var/tmp"
|
||||
) as tmp:
|
||||
home = Path(tmp)
|
||||
for directory in [
|
||||
f".config/{profile}",
|
||||
"Downloads",
|
||||
".cache",
|
||||
".local/share",
|
||||
".local/state",
|
||||
]:
|
||||
(home / directory).mkdir(parents=True, exist_ok=True)
|
||||
(home / ".config/user-dirs.dirs").write_text(
|
||||
'XDG_DOWNLOAD_DIR="$HOME/Downloads"\n'
|
||||
)
|
||||
env = {
|
||||
key: os.environ[key]
|
||||
for key in [
|
||||
"PATH",
|
||||
"USER",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"WAYLAND_DISPLAY",
|
||||
"LANG",
|
||||
]
|
||||
if key in os.environ
|
||||
}
|
||||
canaries = [
|
||||
"NIXPAK_VALIDATION_CANARY",
|
||||
"SSH_AUTH_SOCK",
|
||||
"GPG_AGENT_INFO",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
]
|
||||
env.update({name: "disposable-test-value" for name in canaries})
|
||||
env.update(
|
||||
HOME=tmp,
|
||||
XDG_CONFIG_HOME=f"{tmp}/.config",
|
||||
XDG_CACHE_HOME=f"{tmp}/.cache",
|
||||
XDG_DATA_HOME=f"{tmp}/.local/share",
|
||||
XDG_STATE_HOME=f"{tmp}/.local/state",
|
||||
NIXPAK_VALIDATION_CANARY="disposable-test-value",
|
||||
)
|
||||
base = [
|
||||
browser,
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-background-networking",
|
||||
f"--user-data-dir={tmp}/.config/{profile}/validation-profile",
|
||||
]
|
||||
with (home / "browser.log").open("w+") as log:
|
||||
proc = subprocess.Popen(
|
||||
base + ["--remote-debugging-port=0", "about:blank"],
|
||||
env=env,
|
||||
stdout=log,
|
||||
stderr=log,
|
||||
start_new_session=True,
|
||||
)
|
||||
try:
|
||||
deadline = time.monotonic() + 35
|
||||
renderers = []
|
||||
while time.monotonic() < deadline:
|
||||
table = processes()
|
||||
owned = descendants(proc.pid, table)
|
||||
renderers = [
|
||||
pid
|
||||
for pid in owned
|
||||
if pid in table
|
||||
and b"--type=renderer" in b" ".join(table[pid][2]).split()
|
||||
]
|
||||
if renderers or proc.poll() is not None:
|
||||
break
|
||||
time.sleep(0.2)
|
||||
if not renderers:
|
||||
log.seek(0)
|
||||
# This log belongs only to the disposable browser, never a host environment dump.
|
||||
raise AssertionError(
|
||||
"No live renderer; disposable browser log:\n"
|
||||
+ log.read()[-6000:]
|
||||
)
|
||||
report["renderers"] = [
|
||||
{
|
||||
"pid": pid,
|
||||
"Seccomp": table[pid][1]["Seccomp"].strip(),
|
||||
"NoNewPrivs": table[pid][1]["NoNewPrivs"].strip(),
|
||||
}
|
||||
for pid in renderers
|
||||
]
|
||||
assert all(
|
||||
row["Seccomp"] == "2" and row["NoNewPrivs"] == "1"
|
||||
for row in report["renderers"]
|
||||
), report
|
||||
bwrap = [
|
||||
row[2]
|
||||
for pid, row in table.items()
|
||||
if pid in owned and Path(os.fsdecode(row[2][0])).name == "bwrap"
|
||||
]
|
||||
required = [
|
||||
b"--unshare-user-try",
|
||||
b"--unshare-pid",
|
||||
b"--unshare-net",
|
||||
b"--clearenv",
|
||||
b"--new-session",
|
||||
b"--die-with-parent",
|
||||
]
|
||||
assert any(
|
||||
all(flag in command for flag in required) for command in bwrap
|
||||
), "Missing bwrap isolation arguments"
|
||||
report["bwrap_required_flags"] = [
|
||||
os.fsdecode(flag) for flag in required
|
||||
]
|
||||
browser_pids = set()
|
||||
for renderer in renderers:
|
||||
pid = table[renderer][0]
|
||||
while pid in owned and pid in table:
|
||||
command = b" ".join(table[pid][2]).split()
|
||||
if (
|
||||
any(arg.startswith(b"--user-data-dir=") for arg in command)
|
||||
and not any(arg.startswith(b"--type=") for arg in command)
|
||||
and b"--clearenv" not in command
|
||||
):
|
||||
browser_pids.add(pid)
|
||||
break
|
||||
pid = table[pid][0]
|
||||
checked = 0
|
||||
for pid in browser_pids:
|
||||
command = b" ".join(table[pid][2]).split()
|
||||
assert b"--no-sandbox" not in command, "Chromium sandbox disabled"
|
||||
features = [
|
||||
arg.split(b"=", 1)[1].split(b",")
|
||||
for arg in command
|
||||
if arg.startswith(b"--enable-features=")
|
||||
]
|
||||
assert features and b"WaylandWindowDecorations" in features[-1], (
|
||||
"Missing Wayland feature flag"
|
||||
)
|
||||
assert (b"WebRtcPipeWireCamera" in features[-1]) == media, (
|
||||
"Camera feature does not match browser media policy"
|
||||
)
|
||||
|
||||
assert all(
|
||||
b"--no-sandbox" not in b" ".join(table[pid][2]).split()
|
||||
for pid in renderers
|
||||
), "Renderer sandbox disabled"
|
||||
report["camera_and_wayland_features"] = {
|
||||
"WaylandWindowDecorations": True,
|
||||
"WebRtcPipeWireCamera": media,
|
||||
}
|
||||
report["chromium_sandbox_flags"] = (
|
||||
"--no-sandbox absent; renderer Seccomp=2/NoNewPrivs=1"
|
||||
)
|
||||
for pid in browser_pids:
|
||||
try:
|
||||
entries = Path(f"/proc/{pid}/environ").read_bytes().split(b"\0")
|
||||
except PermissionError:
|
||||
continue
|
||||
assert not any(
|
||||
entry.startswith(name.encode() + b"=")
|
||||
for entry in entries
|
||||
for name in canaries
|
||||
), "Canary leaked into browser"
|
||||
checked += 1
|
||||
assert checked, "Could not inspect browser environment"
|
||||
report["browser_environments_without_canary"] = checked
|
||||
report["absent_canary_variables"] = canaries
|
||||
command = next(command for command in bwrap if b"--clearenv" in command)
|
||||
network_flags = [
|
||||
arg
|
||||
for arg in command[: command.index(b"--")]
|
||||
if arg in {b"--unshare-net", b"--share-net"}
|
||||
]
|
||||
expected_network = (
|
||||
b"--share-net"
|
||||
if args.browser.name == "chromium"
|
||||
else b"--unshare-net"
|
||||
)
|
||||
assert network_flags[-1] == expected_network, (
|
||||
"Unexpected effective network namespace policy"
|
||||
)
|
||||
report["effective_network_namespace_flag"] = os.fsdecode(
|
||||
expected_network
|
||||
)
|
||||
|
||||
for forbidden in [b"/dev/video0", b"/tmp/.X11-unix"]:
|
||||
assert forbidden not in command, "Unexpected device/socket bind"
|
||||
assert not any(
|
||||
b"/pipewire-0" in arg or b"/gvfsd" in arg for arg in command
|
||||
), "Unexpected direct desktop socket bind"
|
||||
assert any(
|
||||
command[i : i + 2] == [b"--tmpfs", b"/dev/shm"]
|
||||
for i in range(len(command) - 1)
|
||||
), "Missing private shared memory"
|
||||
report["private_shm_and_no_direct_pipewire_gvfs_camera_x11"] = True
|
||||
# Inspect bind operands, not synthetic --dev /dev or tmpfs mounts.
|
||||
bind_options = {
|
||||
b"--bind",
|
||||
b"--bind-try",
|
||||
b"--ro-bind",
|
||||
b"--ro-bind-try",
|
||||
b"--dev-bind",
|
||||
b"--dev-bind-try",
|
||||
}
|
||||
options = command[: command.index(b"--")]
|
||||
binds = [
|
||||
(options[i + 1], options[i + 2])
|
||||
for i, option in enumerate(options)
|
||||
if option in bind_options
|
||||
]
|
||||
broad_paths = {
|
||||
b"/",
|
||||
b"/home",
|
||||
b"/dev",
|
||||
os.fsencode(tmp),
|
||||
os.fsencode(os.environ["HOME"]),
|
||||
}
|
||||
for source, destination in binds:
|
||||
for path in (source, destination):
|
||||
assert path.rstrip(b"/") not in broad_paths and path != b"/", (
|
||||
"Broad host filesystem bind"
|
||||
)
|
||||
parts = path.split(b"/")
|
||||
if not media:
|
||||
assert b"pulse" not in parts and not any(
|
||||
part.startswith(b"pipewire-") for part in parts
|
||||
), (
|
||||
"Media-disabled browser has an audio/PipeWire socket bind"
|
||||
)
|
||||
|
||||
assert b".ssh" not in parts, "SSH directory bind"
|
||||
assert not (
|
||||
path == b"/run/dbus" or path.startswith(b"/run/dbus/")
|
||||
), "System D-Bus bind"
|
||||
assert not (
|
||||
path.startswith((b"/dev/video", b"/tmp/.X11-unix"))
|
||||
), "Raw camera or X11 bind"
|
||||
if args.browser.name in {"brave-origin", "microsoft-edge"}:
|
||||
assert not (
|
||||
{b".gnupg", b"gnupg", b"password-store"} & set(parts)
|
||||
), "Unexpected browserpass credentials bind"
|
||||
report["no_broad_home_dev_ssh_or_system_bus_bind"] = True
|
||||
report["media_socket_policy"] = (
|
||||
"PulseAudio allowed; direct PipeWire absent"
|
||||
if media
|
||||
else "PulseAudio and PipeWire binds absent"
|
||||
)
|
||||
|
||||
report["browserpass_bind_scope"] = (
|
||||
"allowed only for Chromium/Chrome; absent for Brave/Edge"
|
||||
)
|
||||
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait()
|
||||
|
||||
print(json.dumps(report, indent=2), flush=True)
|
||||
render_file = home / ".config" / profile / "render-canary.html"
|
||||
render_file.write_text("<html><body>NIXPAK_RENDER_OK</body></html>")
|
||||
positive = run_browser(
|
||||
base
|
||||
+ [
|
||||
"--timeout=10000",
|
||||
"--dump-dom",
|
||||
render_file.as_uri(),
|
||||
],
|
||||
env,
|
||||
)
|
||||
assert positive.returncode == 0 and b"NIXPAK_RENDER_OK" in positive.stdout, (
|
||||
"Disposable page failed to render"
|
||||
)
|
||||
assert b"Fontconfig error" not in positive.stderr, (
|
||||
"Fontconfig could not load sandbox configuration"
|
||||
)
|
||||
report["headless_page_render"] = (
|
||||
"host-prepared file rendered; no Fontconfig errors"
|
||||
)
|
||||
state_file = home / ".config" / profile / "validation-profile" / "Local State"
|
||||
assert state_file.is_file() and state_file.stat().st_size > 0, (
|
||||
"Browser profile state did not persist to disposable host HOME"
|
||||
)
|
||||
report["disposable_profile_persistence"] = (
|
||||
"nonempty Local State exists in host HOME"
|
||||
)
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
hits = 0
|
||||
|
||||
def do_GET(self):
|
||||
type(self).hits += 1
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(
|
||||
b"<html><body>NIXPAK_HOST_LOOPBACK_CANARY</body></html>"
|
||||
)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
with http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) as server:
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
url = f"http://127.0.0.1:{server.server_port}/"
|
||||
assert (
|
||||
b"NIXPAK_HOST_LOOPBACK_CANARY"
|
||||
in urllib.request.urlopen(url, timeout=5).read()
|
||||
)
|
||||
Handler.hits = 0
|
||||
try:
|
||||
result = run_browser(base + ["--timeout=10000", "--dump-dom", url], env)
|
||||
assert result.returncode == 0, "Loopback browser command failed"
|
||||
if args.browser.name == "chromium":
|
||||
assert (
|
||||
b"NIXPAK_HOST_LOOPBACK_CANARY" in result.stdout
|
||||
and Handler.hits > 0
|
||||
), "Chromium must reach host loopback"
|
||||
report["host_loopback"] = (
|
||||
"allowed: host and Chromium fetched the listener marker"
|
||||
)
|
||||
else:
|
||||
assert b"ERR_CONNECTION_REFUSED" in result.stdout + result.stderr, (
|
||||
"Expected explicit browser connection refusal"
|
||||
)
|
||||
assert (
|
||||
b"NIXPAK_HOST_LOOPBACK_CANARY" not in result.stdout
|
||||
and Handler.hits == 0
|
||||
), "Browser reached host loopback"
|
||||
report["host_loopback"] = (
|
||||
"blocked: host positive control succeeded; browser ERR_CONNECTION_REFUSED; zero server requests"
|
||||
)
|
||||
finally:
|
||||
server.shutdown()
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
# WeChat nested runtime verification
|
||||
|
||||
WeChat 4.1.13.26 built and opened a `Weixin` X11 window with a disposable profile. The final
|
||||
candidate was `/nix/store/6wr94i7da0674k9hzs23fk2nr1ih4i3f-nixpak-wechat-4.1.13.26`. The existing
|
||||
AppImage extraction, FHS runtime, X11 and fcitx settings remain in `hardening/bwraps/wechat.nix`;
|
||||
the new nixpak wrapper adds an outer boundary.
|
||||
|
||||
The live extracted WeChat binary had three PID namespace identifiers, `NoNewPrivs: 1`, `Seccomp: 0`,
|
||||
a network namespace different from the host, and `/.flatpak-info` identifying `com.tencent.WeChat`.
|
||||
Its device view contained standard synthetic devices, `dri`, and the four explicit NVIDIA nodes; no
|
||||
raw camera, USB or block devices. This smoke test does not establish an internal renderer sandbox
|
||||
guarantee.
|
||||
|
||||
A sentinel in the disposable `Documents/WeChat_Data/home` was readable at WeChat's remapped HOME.
|
||||
Its `xwechat_files` link retained the absolute target `Documents/WeChat_Data/xwechat_files`, which
|
||||
remains bound inside both layers. The final launcher also rejected a separate disposable layout
|
||||
containing a real `home/xwechat_files` directory, exited 1, and preserved its sentinel and contents.
|
||||
It will not silently migrate an unexpected existing layout.
|
||||
|
||||
## Compatibility changes
|
||||
|
||||
As with Zoom, the wrapper preserves the outer `/.flatpak-info` file explicitly: the FHS automatic
|
||||
root mount loop only propagates directories. The inner `--dev-bind /dev /dev` inherits the outer
|
||||
filtered devices. The original inner network setting inherits pasta's isolated outer namespace,
|
||||
including its private loopback.
|
||||
|
||||
The original inner `/home` reset hides the shared desktop base's XDG mounts. WeChat therefore
|
||||
exposes its existing backing tree to the outer sandbox and keeps the original FHS HOME mapping. It
|
||||
supplies `XDG_DOCUMENTS_DIR` and a coreutils-only PATH for the pre-FHS mkdir/link commands.
|
||||
Xauthority is rebound after the inner HOME reset when present.
|
||||
|
||||
The first run aborted in libdbus because no machine ID was available. The WeChat policy supplies a
|
||||
synthetic app-specific ID rather than the host ID. Login/device identity continuity with an existing
|
||||
account remains untested. No credentials or real profile were used for this test.
|
||||
|
||||
## Historical constraint
|
||||
|
||||
Commit `0c7e43a2` removed `nixpaks/wechat-uos.nix`. Its previous TODO said the FHS runtime was
|
||||
problematic inside another sandbox, but supplied no failure log. That old module also disabled X11
|
||||
and set `QT_QPA_PLATFORM=kcb`, despite its xcb-only comment. Commit `28dee277` subsequently changed
|
||||
the AppImage wrapper from Wayland to xcb and added fcitx environment settings. These records do not
|
||||
prove the original failure's cause. Current runtime testing establishes that retaining FHS inside
|
||||
nixpak is feasible with the explicit compatibility changes above.
|
||||
|
||||
## Repeat safely
|
||||
|
||||
Use the same disposable-home command as [Zoom](zoom-runtime.md), with WeChat's output and
|
||||
executable, and additionally set `XDG_DOCUMENTS_DIR="$wechat_probe_dir/Documents"`. Prepare
|
||||
`Documents/WeChat_Data/home` and `Documents/WeChat_Data/xwechat_files` beneath that disposable home
|
||||
before launching. The smoke test stops before QR login and uses no account. Inspect the exact test
|
||||
PID through `/proc` while the timeout is active; verify that its window and processes disappear
|
||||
after timeout.
|
||||
|
||||
Pending manual checks: existing account persistence, QR/login, fcitx input, notifications/tray,
|
||||
portal file chooser and selected-file access, downloads, voice/video calls, camera/screencast portal
|
||||
support and clipboard workflows. Camera and ScreenCast methods are explicit grants; raw capture
|
||||
devices and the host PipeWire socket are not exposed. X11 retains its display trust limitations, so
|
||||
portal permission does not imply all capture requires portal consent.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Zoom nested runtime verification
|
||||
|
||||
The Zoom 7.2.1.5760 candidate built and opened two `Zoom Workplace` X11 windows on the NVIDIA
|
||||
desktop. The final tested output was
|
||||
`/nix/store/yy6sxsgn5rqafvvgs3k53akwqjcfa2r7-nixpak-zoom-7.2.1.5760`.
|
||||
|
||||
The live main process had `NoNewPrivs: 1`, a network namespace different from the host, and
|
||||
`/.flatpak-info` with `name=us.zoom.Zoom`. Its ancestry included ZoomLauncher and the nested
|
||||
bubblewrap processes. Its `/dev` contained only standard synthetic devices, `dri`, `nvidia0`,
|
||||
`nvidiactl`, `nvidia-modeset`, and `nvidia-uvm`; no camera, USB or block devices were exposed.
|
||||
Zoom's main process reported `Seccomp: 0`; this test does not establish a renderer seccomp
|
||||
guarantee.
|
||||
|
||||
The upstream FHS runtime remains intact. Its automatic root mounts propagate directories, so the
|
||||
wrapper explicitly propagates the outer `/.flatpak-info` file for portal identification. Its `/dev`
|
||||
bind inherits the outer sandbox's filtered devices. The outer network uses pasta isolate mode,
|
||||
without host loopback sharing. X11 remains a display trust boundary: explicit ScreenCast portal
|
||||
permission does not prevent access through the X11 connection.
|
||||
|
||||
Initial display testing found that `/tmp` tmpfs shadowed the X11 socket and that NVIDIA OpenGL
|
||||
needed the four device mappings now in `desktop-base.nix`. Zoom therefore keeps the sandbox's
|
||||
private root `/tmp` instead of mounting another tmpfs over the socket, and explicitly passes
|
||||
`DISPLAY` and `XAUTHORITY`.
|
||||
|
||||
## Repeat without touching the normal Zoom profile
|
||||
|
||||
Build the candidate without activating a host, then substitute its output path below. The temporary
|
||||
home must be outside `/tmp` when testing policies that mount a `/tmp` tmpfs. Keep the current
|
||||
display and session bus environment.
|
||||
|
||||
```bash
|
||||
zoom_probe_dir=$(mktemp -d "$HOME/.cache/zoom-nixpak-probe.XXXXXX")
|
||||
mkdir -p "$zoom_probe_dir"/{.config,.cache,.local/share,.local/state,Documents,Downloads}
|
||||
HOME="$zoom_probe_dir" \
|
||||
XDG_CONFIG_HOME="$zoom_probe_dir/.config" \
|
||||
XDG_CACHE_HOME="$zoom_probe_dir/.cache" \
|
||||
XDG_DATA_HOME="$zoom_probe_dir/.local/share" \
|
||||
XDG_STATE_HOME="$zoom_probe_dir/.local/state" \
|
||||
timeout --kill-after=3 30 /nix/store/OUTPUT/bin/zoom
|
||||
```
|
||||
|
||||
While it runs, inspect the exact test process through `/proc/PID/root/dev`,
|
||||
`/proc/PID/root/.flatpak-info`, `/proc/PID/ns/net`, `/proc/PID/status`, and its parent processes. Do
|
||||
not sign in or join a meeting during this smoke test. The timeout terminates the owned launcher and
|
||||
its sandbox. Confirm the test windows and processes are gone afterward.
|
||||
|
||||
The disposable run created `zoomus.conf` under the private application config directory. Existing
|
||||
host `zoomus.conf` and `zoom.conf` are individually mounted when present. Application caches use the
|
||||
private application cache directory.
|
||||
|
||||
Still pending: login and meeting join, microphone and speaker routing, camera portal behavior on
|
||||
supported hardware, screencast selection and PipeWire FD handoff, recording/export paths, file
|
||||
chooser access, URL opening and IME. No successful camera, audio or screencast session is claimed by
|
||||
this smoke test.
|
||||
@@ -1,20 +1,16 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
home.packages =
|
||||
with pkgs;
|
||||
[
|
||||
nixpaks.firefox
|
||||
];
|
||||
|
||||
# source code: https://github.com/nix-community/home-manager/blob/master/modules/programs/chromium.nix
|
||||
|
||||
# open-source upstream Chromium, the base for all the browsers below
|
||||
programs.chromium.enable = true;
|
||||
# the world's most widely used browser, with deep Google account and services integration
|
||||
programs.google-chrome.enable = true;
|
||||
# Brave's privacy-first build (Shields ad/tracker blocking), minus the crypto/AI extras
|
||||
programs.brave-origin.enable = true;
|
||||
# Microsoft's browser, with built-in phishing/malware protection, x86_64 build only
|
||||
programs.microsoft-edge.enable = pkgs.stdenv.hostPlatform.isx86_64;
|
||||
nixpaks.google-chrome
|
||||
nixpaks.chromium
|
||||
nixpaks.brave-origin
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [ nixpaks.microsoft-edge ];
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
]
|
||||
# Zoom: Settings > Share Screen > Advanced > Screen Capture Mode on Wayland > PipeWire Mode.
|
||||
# zoom-us ships only an x86_64 Linux client, so the aarch64 host skips it.
|
||||
++ lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [ pkgs.zoom-us ];
|
||||
++ lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [ pkgs.nixpaks.zoom ];
|
||||
|
||||
programs.mpv = {
|
||||
enable = true;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# my custom hardened packages
|
||||
nixpaks.qq
|
||||
nixpaks.telegram-desktop
|
||||
bwraps.wechat
|
||||
nixpaks.wechat
|
||||
# discord # update too frequently, use the web version instead
|
||||
];
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
# nixpaks
|
||||
"${pkgs.nixpaks.firefox}/share/applications/org.mozilla.firefox.desktop"
|
||||
"${pkgs.nixpaks.telegram-desktop}/share/applications/org.telegram.desktop.desktop"
|
||||
"${pkgs.google-chrome}/share/applications/google-chrome.desktop"
|
||||
"${pkgs.nixpaks.google-chrome}/share/applications/com.google.Chrome.desktop"
|
||||
];
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
defaultApplications =
|
||||
let
|
||||
browser = [
|
||||
"google-chrome.desktop"
|
||||
"firefox.desktop"
|
||||
"com.google.Chrome.desktop"
|
||||
"org.mozilla.firefox.desktop"
|
||||
];
|
||||
editor = [
|
||||
"dev.zed.Zed.desktop"
|
||||
@@ -48,11 +48,14 @@
|
||||
"x-scheme-handler/http" = browser;
|
||||
"x-scheme-handler/https" = browser;
|
||||
"x-scheme-handler/zoommtg" = [ "Zoom.desktop" ];
|
||||
"x-scheme-handler/zoomus" = [ "Zoom.desktop" ];
|
||||
"application/x-zoom" = [ "Zoom.desktop" ];
|
||||
|
||||
# all other unknown schemes will be opened by this default application.
|
||||
# "x-scheme-handler/unknown" = editor;
|
||||
|
||||
"x-scheme-handler/tg" = [ "org.telegram.desktop.desktop" ];
|
||||
"x-scheme-handler/weixin" = [ "wechat.desktop" ];
|
||||
|
||||
"audio/*" = [ "mpv.desktop" ];
|
||||
"video/*" = [ "mpv.desktop" ];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ ... }: true
|
||||
@@ -0,0 +1,11 @@
|
||||
{ inputs, outputs, ... }:
|
||||
let
|
||||
pkgs = outputs.nixosConfigurations.ai-niri.pkgs;
|
||||
in
|
||||
import ../../../../hardening/tests/policy.nix {
|
||||
inherit pkgs;
|
||||
mkNixPak = inputs.nixpak.lib.nixpak {
|
||||
inherit pkgs;
|
||||
inherit (pkgs) lib;
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user