diff --git a/SECURITY.md b/SECURITY.md index 75e29648..e9597e65 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -323,8 +323,9 @@ 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 deliberate host networking, GUI/audio/portal/GPU access and selected writable - document directories. Firefox additionally retains browserpass/GnuPG integration. These are +- 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 diff --git a/hardening/nixpaks/firefox.nix b/hardening/nixpaks/firefox.nix index 266e4d90..5f733b6e 100644 --- a/hardening/nixpaks/firefox.nix +++ b/hardening/nixpaks/firefox.nix @@ -21,6 +21,9 @@ let sloth, ... }: + let + passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store"; + in { app = { package = firefox; @@ -34,6 +37,11 @@ let ./modules/custom-policy.nix ]; + # 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. + pasta.enable = lib.mkForce false; + bubblewrap = { # To trace all the home files Firefox accesses, you can use the following nushell command: # just trace-access firefox @@ -68,7 +76,7 @@ let # ================ for browserpass extension =============================== "/etc/gnupg" (sloth.concat' sloth.homeDir "/.gnupg") # gpg's config - (sloth.concat' sloth.homeDir "/.local/share/password-store") # my secrets + passwordStore # my secrets (sloth.concat' sloth.runtimeDir "/gnupg") # for access gpg-agent socket # Match the host's default-app associations and XDG user dirs. @@ -81,6 +89,8 @@ let # 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; }; }; }; diff --git a/hardening/nixpaks/modules/custom-policy.nix b/hardening/nixpaks/modules/custom-policy.nix index 046878ae..809ce423 100644 --- a/hardening/nixpaks/modules/custom-policy.nix +++ b/hardening/nixpaks/modules/custom-policy.nix @@ -197,8 +197,8 @@ in # 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 (see the note on the PulseAudio runtime - # dir there). Only app-specific mounts belong in this file. + # from shadowing a writable one. Only app-specific mounts belong in this + # file. bind.rw = with sloth; [ [ (mkdir appDataDir) diff --git a/hardening/nixpaks/modules/gui-base.nix b/hardening/nixpaks/modules/gui-base.nix index 354fb7b3..2d0b96f6 100644 --- a/hardening/nixpaks/modules/gui-base.nix +++ b/hardening/nixpaks/modules/gui-base.nix @@ -14,6 +14,9 @@ }: 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; @@ -43,15 +46,20 @@ in # 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. PulseAudio is the exception (see bind.rw). - # Every app here is Wayland-only, so X11 stays disabled (the nixpak default, - # spelled out for intent) and PipeWire is shared for screensharing. + # 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; }; @@ -66,19 +74,11 @@ in # gvfsd is a directory that holds FUSE mounts, not a socket, so it must # stay writable. (envSuffix "XDG_RUNTIME_DIR" "/gvfsd") - - # libpulse prepares its runtime dir before connecting (mkdir + chmod 0700), - # so this directory must be writable. nixpak's built-in `sockets.pulse` - # binds it read-only instead, and libpulse then aborts with EROFS when the - # directory is not already 0700 (e.g. pipewire-pulse has not started yet). - # Keep this the only bind of $XDG_RUNTIME_DIR/pulse. - (envSuffix "XDG_RUNTIME_DIR" "/pulse") ]; 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") - "/run/dbus" (sloth.concat' sloth.xdgConfigHome "/gtk-2.0") (sloth.concat' sloth.xdgConfigHome "/gtk-3.0") @@ -112,7 +112,23 @@ in "/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 @@ -126,7 +142,32 @@ in "${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" + ]; }; }; } diff --git a/hardening/nixpaks/modules/network.nix b/hardening/nixpaks/modules/network.nix index 225adea2..7a1d9e8f 100644 --- a/hardening/nixpaks/modules/network.nix +++ b/hardening/nixpaks/modules/network.nix @@ -1,8 +1,9 @@ # https://github.com/nixpak/pkgs/blob/master/pkgs/modules/network.nix { etc.sslCertificates.enable = true; - bubblewrap = { - bind.ro = [ "/etc/resolv.conf" ]; - network = true; - }; + bubblewrap.network = true; + # pasta's default isolate mode gives the app its own loopback, so host + # services on 127.0.0.1 are out of reach. Proxying still works because + # mihomo runs in TUN mode, which sees pasta's outbound traffic. + pasta.enable = true; }