programs: allow programs to specify "sandbox.method = "bwrap"" for bubblewrap sandboxing

This commit is contained in:
2024-01-23 10:44:13 +00:00
parent 0ddcfcaa23
commit 6e9220d2bb
4 changed files with 99 additions and 33 deletions

View File

@@ -36,17 +36,19 @@ let
wrapPkg = pkgName: { fs, net, persist, sandbox, ... }: package: ( wrapPkg = pkgName: { fs, net, persist, sandbox, ... }: package: (
if sandbox.method == null then if sandbox.method == null then
package package
else if sandbox.method == "firejail" then else
let let
makeSandboxed = pkgs.callPackage ./make-sandboxed.nix { sane-sandboxed = config.sane.sandboxHelper; }; makeSandboxed = pkgs.callPackage ./make-sandboxed.nix { sane-sandboxed = config.sane.sandboxHelper; };
vpn = lib.findSingle (v: v.default) null null (builtins.attrValues config.sane.vpn); vpn = lib.findSingle (v: v.default) null null (builtins.attrValues config.sane.vpn);
in in
makeSandboxed { makeSandboxed {
inherit pkgName package; inherit pkgName package;
inherit (sandbox) binMap; inherit (sandbox) binMap method;
vpn = if net == "vpn" then vpn else null; vpn = if net == "vpn" then vpn else null;
allowedHomePaths = builtins.attrNames fs ++ builtins.attrNames persist.byPath; allowedHomePaths = builtins.attrNames fs ++ builtins.attrNames persist.byPath;
allowedRootPaths = [ allowedRootPaths = [
"/nix/store"
"/etc" #< especially for /etc/profiles/per-user/$USER/bin
"/run/current-system" #< for basics like `ls`, and all this program's `suggestedPrograms` (/run/current-system/sw/bin) "/run/current-system" #< for basics like `ls`, and all this program's `suggestedPrograms` (/run/current-system/sw/bin)
"/run/wrappers" #< SUID wrappers, in this case so that firejail can be re-entrant "/run/wrappers" #< SUID wrappers, in this case so that firejail can be re-entrant
# "/bin/sh" #< to allow `firejail --join=...` (doesn't work) # "/bin/sh" #< to allow `firejail --join=...` (doesn't work)
@@ -54,11 +56,10 @@ let
# /run/opengl-driver is a symlink into /nix/store; needed by e.g. mpv # /run/opengl-driver is a symlink into /nix/store; needed by e.g. mpv
"/run/opengl-driver" "/run/opengl-driver"
"/run/opengl-driver-32" "/run/opengl-driver-32"
"/run/user" #< particularly /run/user/$id/wayland-1, pulse, etc.
# "/dev/dri" #< fix non-fatal "libEGL warning: wayland-egl: could not open /dev/dri/renderD128" (geary) # "/dev/dri" #< fix non-fatal "libEGL warning: wayland-egl: could not open /dev/dri/renderD128" (geary)
]; ];
} }
else
throw "unknown sandbox type '${sandbox.method}'"
); );
pkgSpec = with lib; types.submodule ({ config, name, ... }: { pkgSpec = with lib; types.submodule ({ config, name, ... }: {
options = { options = {
@@ -207,8 +208,8 @@ let
''; '';
}; };
sandbox.method = mkOption { sandbox.method = mkOption {
type = types.nullOr (types.enum [ "firejail" ]); type = types.nullOr (types.enum [ "bwrap" "firejail" ]);
default = null; #< TODO: default to firejail default = null; #< TODO: default to bwrap
description = '' description = ''
how/whether to sandbox all binaries in the package. how/whether to sandbox all binaries in the package.
''; '';

View File

@@ -1,12 +1,10 @@
{ lib { lib
, buildPackages
, firejail
, runCommand , runCommand
, runtimeShell , runtimeShell
, sane-sandboxed , sane-sandboxed
, writeTextFile , writeTextFile
}: }:
{ pkgName, package, vpn ? null, allowedHomePaths ? [], allowedRootPaths ? [], binMap ? {} }: { pkgName, package, method, vpn ? null, allowedHomePaths ? [], allowedRootPaths ? [], binMap ? {} }:
let let
sane-sandboxed' = sane-sandboxed.meta.mainProgram; #< load by bin name to reduce rebuilds sane-sandboxed' = sane-sandboxed.meta.mainProgram; #< load by bin name to reduce rebuilds
@@ -29,7 +27,9 @@ let
addr addr
]) vpn.dns); ]) vpn.dns);
sandboxFlags = allowPaths allowedRootPaths sandboxFlags = [
"--sane-sandbox-method" method
] ++ allowPaths allowedRootPaths
++ allowHomePaths allowedHomePaths ++ allowHomePaths allowedHomePaths
++ lib.optionals (vpn != null) vpnItems; ++ lib.optionals (vpn != null) vpnItems;
@@ -96,23 +96,27 @@ let
for _p in $(ls "$out/bin/"); do for _p in $(ls "$out/bin/"); do
sandboxWrap "$_p" sandboxWrap "$_p"
done done
# stamp file which can be consumed to ensure this wrapping code was actually called.
mkdir -p $out/nix-support
touch $out/nix-support/sandboxed
''; '';
meta = (unwrapped.meta or {}) // { meta = (unwrapped.meta or {}) // {
# take precedence over non-sandboxed versions of the same binary. # take precedence over non-sandboxed versions of the same binary.
priority = ((unwrapped.meta or {}).priority or 0) - 1; priority = ((unwrapped.meta or {}).priority or 0) - 1;
}; };
passthru = (unwrapped.passthru or {}) // { passthru = (unwrapped.passthru or {}) // {
checkSandboxed = runCommand "${pkgName}-check-sandboxed" {} '' checkSandboxed = runCommand "${pkgName}-check-sandboxed" {} ''
# this pseudo-package gets "built" as part of toplevel system build. # invoke each binary in a way only the sandbox wrapper will recognize,
# if the build is failing here, that means the program isn't properly sandboxed: # ensuring that every binary has in fact been wrapped.
# make sure that "postFixup" gets called as part of the package's build script _numExec=0
test -f "${packageWrapped}/nix-support/sandboxed" \ for b in ${packageWrapped}/bin/*; do
&& touch "$out" PATH="$PATH:${packageWrapped}/bin:${sane-sandboxed}/bin" "$b" --sane-sandbox-method exit0fortest | grep "exiting 0 for test"
_numExec=$(( $_numExec + 1 ))
done
echo "successfully tested $_numExec binaries"
test "$_numExec" -ne 0 && touch "$out"
''; '';
sandboxProfiles = writeTextFile { sandboxProfiles = writeTextFile {
name = "${pkgName}-sandbox-profiles"; name = "${pkgName}-sandbox-profiles";
destination = "/share/sane-sandboxed/profiles/${pkgName}.profile"; destination = "/share/sane-sandboxed/profiles/${pkgName}.profile";

View File

@@ -9,7 +9,9 @@ rootPaths=()
homePaths=() homePaths=()
net= net=
dns=() dns=()
method=
firejailFlags=() firejailFlags=()
bwrapFlags=()
debug() { debug() {
[ -n "$SANE_SANDBOX_DEBUG" ] && printf "[debug] %s" "$1" >&2 [ -n "$SANE_SANDBOX_DEBUG" ] && printf "[debug] %s" "$1" >&2
@@ -67,6 +69,10 @@ parseArgs() {
(--sane-sandbox-disable) (--sane-sandbox-disable)
SANE_SANDBOX_DISABLE=1 SANE_SANDBOX_DISABLE=1
;; ;;
(--sane-sandbox-method)
method="$1"
shift
;;
(--sane-sandbox-dns) (--sane-sandbox-dns)
dns+=("$1") dns+=("$1")
shift shift
@@ -75,6 +81,10 @@ parseArgs() {
firejailFlags+=("$1") firejailFlags+=("$1")
shift shift
;; ;;
(--sane-sandbox-bwrap-arg)
bwrapFlags+=("$1")
shift
;;
(--sane-sandbox-net) (--sane-sandbox-net)
net="$1" net="$1"
shift shift
@@ -98,36 +108,87 @@ parseArgs() {
done done
} }
## FIREJAIL BACKEND
firejailIngestRootPath() {
firejailFlags+=("--noblacklist=$1" "--whitelist=$1")
}
firejailIngestHomePath() {
firejailFlags+=("--noblacklist="'${HOME}/'"$1" "--whitelist="'${HOME}/'"$1")
}
firejailIngestNet() {
firejailFlags+=("--net=$1")
}
firejailIngestDns() {
firejailFlags+=("--dns=$1")
}
firejailIngestName() {
firejailFlags+=("--join-or-start=$1")
}
firejailExec() {
if [ -n "$firejailProfile" ]; then
firejailFlags+=("--profile=$firejailProfile")
fi
PATH="$PATH:@firejail@/bin" exec firejail "${firejailFlags[@]}" -- "${cliArgs[@]}"
}
## BUBBLEWRAP BACKEND
bwrapIngestRootPath() {
bwrapFlags+=("--dev-bind" "$1" "$1")
}
bwrapIngestHomePath() {
bwrapFlags+=("--dev-bind" "$HOME/$1" "$HOME/$1")
}
bwrapIngestName() {
echo "bwrap naming/labeling not yet implemented"
}
# WIP
bwrapExec() {
PATH="$PATH:@bubblewrap@/bin" exec bwrap --dev /dev --proc /proc "${bwrapFlags[@]}" -- "${cliArgs[@]}"
}
## BACKEND HANDOFF
parseArgs "$@" parseArgs "$@"
cliArgs+="${parseArgsExtra[@]}" cliArgs+=("${parseArgsExtra[@]}")
test -n "$SANE_SANDBOX_DISABLE" && exec "${cliArgs[@]}" test -n "$SANE_SANDBOX_DISABLE" && exec "${cliArgs[@]}"
## construct firejail flags from sane-sandbox flags test "$method" = "exit0fortest" && echo "exiting 0 for test" && exit 0
# drop into an interactive shell to e.g. inspect the mount space
test "$method" = "debugshell" && exec sh
### convert generic args into sandbox-specific args
# order matters: for firejail, early args override the later --profile args
for _path in "${rootPaths[@]}"; do for _path in "${rootPaths[@]}"; do
firejailFlags+=("--noblacklist=$_path" "--whitelist=$_path") "$method"IngestRootPath "$_path"
done done
for _path in "${homePaths[@]}"; do for _path in "${homePaths[@]}"; do
firejailFlags+=("--noblacklist="'${HOME}/'"$_path" "--whitelist="'${HOME}/'"$_path") "$method"IngestHomePath "$_path"
done done
if [ -n "$net" ]; then if [ -n "$net" ]; then
firejailFlags+=("--net=$net") "$method"IngestNet "$net"
fi fi
for _addr in "${dns[@]}"; do for _addr in "${dns[@]}"; do
firejailFlags+=("--dns=$_addr") "$method"IngestDns "$_addr"
done done
if [ -n "$name" ]; then if [ -n "$name" ]; then
firejailFlags+=("--join-or-start=$name") "$method"IngestName "$name"
fi fi
# order matters: customizations (i.e. the above) must be before --profile "$method"Exec
if [ -n "$firejailProfile" ]; then
firejailFlags+=("--profile=$firejailProfile")
fi
PATH="$PATH:@firejail@" exec firejail "${firejailFlags[@]}" -- "${cliArgs[@]}" echo "sandbox glue failed for method='$method'"
exit 1

View File

@@ -1,4 +1,5 @@
{ lib, stdenv { lib, stdenv
, bubblewrap
, firejail , firejail
, runtimeShell , runtimeShell
, substituteAll , substituteAll
@@ -8,8 +9,7 @@
let let
sane-sandboxed = substituteAll { sane-sandboxed = substituteAll {
src = ./sane-sandboxed; src = ./sane-sandboxed;
inherit runtimeShell; inherit bubblewrap firejail runtimeShell;
firejail = "${firejail}/bin/firejail";
firejailProfileDirs = "/run/current-system/sw/etc/firejail /etc/firejail ${firejail}/etc/firejail"; firejailProfileDirs = "/run/current-system/sw/etc/firejail /etc/firejail ${firejail}/etc/firejail";
# /run might be unavailable inside a container, so to support nested containers # /run might be unavailable inside a container, so to support nested containers
# fallback to a profile dir adjacent to the sane-sandboxed binary # fallback to a profile dir adjacent to the sane-sandboxed binary