firefox: sandbox with firejail

TODO: get it so open-in-mpv launches an mpv that has access to ~/.config/mpv

i guess this is the 'firejail url problem'
This commit is contained in:
Colin 2024-01-21 23:59:15 +00:00
parent ad92a2e158
commit 9ecd0adcbe
4 changed files with 89 additions and 21 deletions

View File

@ -113,6 +113,8 @@ let
rm $out/lib/${cfg.browser.libName}/browser/omni.ja rm $out/lib/${cfg.browser.libName}/browser/omni.ja
${pkgs.buildPackages.gnused}/bin/sed -i s'/devtools-commandkey-inspector = C/devtools-commandkey-inspector = VK_F12/' omni/localization/en-US/devtools/startup/key-shortcuts.ftl ${pkgs.buildPackages.gnused}/bin/sed -i s'/devtools-commandkey-inspector = C/devtools-commandkey-inspector = VK_F12/' omni/localization/en-US/devtools/startup/key-shortcuts.ftl
pushd omni; ${pkgs.buildPackages.zip}/bin/zip $out/lib/${cfg.browser.libName}/browser/omni.ja -r ./*; popd pushd omni; ${pkgs.buildPackages.zip}/bin/zip $out/lib/${cfg.browser.libName}/browser/omni.ja -r ./*; popd
runHook postFixup
''; '';
}); });
@ -189,6 +191,7 @@ in
enable = lib.mkDefault config.services.i2p.enable; enable = lib.mkDefault config.services.i2p.enable;
}; };
open-in-mpv = { open-in-mpv = {
# test: `open-in-mpv 'mpv:///open?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ'`
package = pkgs.firefox-extensions.open-in-mpv; package = pkgs.firefox-extensions.open-in-mpv;
enable = lib.mkDefault config.sane.programs.open-in-mpv.enabled; enable = lib.mkDefault config.sane.programs.open-in-mpv.enabled;
}; };
@ -213,6 +216,7 @@ in
({ ({
sane.programs.firefox = { sane.programs.firefox = {
inherit packageUnwrapped; inherit packageUnwrapped;
sandbox.method = "firejail";
suggestedPrograms = [ suggestedPrograms = [
"open-in-mpv" "open-in-mpv"
@ -288,6 +292,21 @@ in
[General] [General]
StartWithLastProfile=1 StartWithLastProfile=1
''; '';
env.PASSWORD_STORE_DIR = "/home/colin/private/knowledge/secrets/accounts";
# alternative to PASSWORD_STORE_DIR, but firejail doesn't handle this symlink well
# fs.".password-store".symlink.target = lib.mkIf cfg.addons.browserpass-extension.enable "private/knowledge/secrets/accounts";
# browserpass needs these paths:
# - .ssh: to unlock the sops key, if not unlocked (`sane-secrets-unlock`(
# - .config/sops: where the key to decrypt account secrets
# - private/knowledge/secrets/accounts: where the encrypted account secrets live
# TODO: find a way to not expose ~/.ssh to firefox
# - unlock sops at login?
fs.".ssh" = lib.mkIf cfg.addons.browserpass-extension.enable {};
fs.".ssh/id_ed25519" = lib.mkIf cfg.addons.browserpass-extension.enable {};
fs.".config/sops" = lib.mkIf cfg.addons.browserpass-extension.enable {};
fs."private/knowledge/secrets/accounts" = lib.mkIf cfg.addons.browserpass-extension.enable {};
}; };
}) })
(mkIf config.sane.programs.firefox.enabled { (mkIf config.sane.programs.firefox.enabled {

View File

@ -10,6 +10,7 @@
''; '';
}); });
net = "vpn"; net = "vpn";
sandbox.method = "firejail";
# ".config/nicotine": contains the config file, with plaintext creds. # ".config/nicotine": contains the config file, with plaintext creds.
# TODO: define this as a secret instead of persisting it. # TODO: define this as a secret instead of persisting it.
persist.byStore.private = [ ".config/nicotine" ]; persist.byStore.private = [ ".config/nicotine" ];

View File

@ -89,9 +89,5 @@
fs."Videos/servo".symlink.target = "/mnt/servo-media/Videos"; fs."Videos/servo".symlink.target = "/mnt/servo-media/Videos";
# fs."Music/servo".symlink.target = "/mnt/servo-media/Music"; # fs."Music/servo".symlink.target = "/mnt/servo-media/Music";
fs."Pictures/servo-macros".symlink.target = "/mnt/servo-media/Pictures/macros"; fs."Pictures/servo-macros".symlink.target = "/mnt/servo-media/Pictures/macros";
# used by password managers, e.g. unix `pass`
# TODO: move this to the specific programs which need it
fs.".password-store".symlink.target = "knowledge/secrets/accounts";
}; };
} }

View File

@ -33,18 +33,36 @@ let
defaultEnables = solveDefaultEnableFor cfg; defaultEnables = solveDefaultEnableFor cfg;
# wrap a package so that its binaries (maybe) run in a sandbox # wrap a package so that its binaries (maybe) run in a sandbox
wrapPkg = { net }: package: ( wrapPkg = { sandbox, fs, net, ... }: package: (
if net == "clearnet" then if sandbox.method == null then
package package
else if net == "vpn" then else if sandbox.method == "firejail" then
let let
vpn = lib.findSingle (v: v.default) null null (builtins.attrValues config.sane.vpn);
# XXX: firejail needs suid bit for some (not all) of its sandboxing methods. hence, rely on the user installing it system-wide and call it by suid path. # XXX: firejail needs suid bit for some (not all) of its sandboxing methods. hence, rely on the user installing it system-wide and call it by suid path.
firejailBin = "/run/wrappers/bin/firejail"; firejailBin = "/run/wrappers/bin/firejail";
firejailFlags = [ vpn = lib.findSingle (v: v.default) null null (builtins.attrValues config.sane.vpn);
vpnFlags = [
"--net=${vpn.bridgeDevice}" "--net=${vpn.bridgeDevice}"
] ++ (builtins.map (addr: "--dns=${addr}") vpn.dns); ] ++ (builtins.map (addr: "--dns=${addr}") vpn.dns);
in allowPath = p: [
"--noblacklist=${p}"
"--whitelist=${p}"
];
fsFlags = lib.flatten (builtins.map
(p: allowPath ''''${HOME}/${p}'')
(builtins.attrNames fs)
);
firejailFlags = [
# "--quiet" #< TODO: enable
# "--tracelog" # logs blacklist violations to syslog (but default firejail disallows this)
] ++ allowPath "/run/current-system" #< for basics like `ls`, and all this program's `suggestedPrograms`
++ fsFlags
++ lib.optionals (net == "vpn") vpnFlags;
firejailBase = pkgs.writeShellScript
"firejail-${package.pname}-base"
''exec ${firejailBin} ${lib.escapeShellArgs firejailFlags} \'';
# two ways i could wrap a package in a sandbox: # two ways i could wrap a package in a sandbox:
# 1. package.overrideAttrs, with `postFixup`. # 1. package.overrideAttrs, with `postFixup`.
# 2. pkgs.symlinkJoin, or pkgs.runCommand, creating an entirely new package which calls into the inner binaries. # 2. pkgs.symlinkJoin, or pkgs.runCommand, creating an entirely new package which calls into the inner binaries.
@ -55,9 +73,9 @@ let
# no.1 may bloat rebuild times. # no.1 may bloat rebuild times.
# #
# ultimately, no.1 is probably more reliable, but i expect i'll factor out a switch to allow either approach -- particularly when debugging package buld failures. # ultimately, no.1 is probably more reliable, but i expect i'll factor out a switch to allow either approach -- particularly when debugging package buld failures.
package.overrideAttrs (unwrapped: { packageWrapped = package.overrideAttrs (unwrapped: {
postFixup = (unwrapped.postFixup or "") + '' postFixup = (unwrapped.postFixup or "") + ''
getFirejailProfile() { getFirejailProfile() {
_maybeProfile="${pkgs.firejail}/etc/firejail/$1.profile" _maybeProfile="${pkgs.firejail}/etc/firejail/$1.profile"
if [ -e "$_maybeProfile" ]; then if [ -e "$_maybeProfile" ]; then
firejailProfileFlags="--profile=$_maybeProfile" firejailProfileFlags="--profile=$_maybeProfile"
@ -70,22 +88,39 @@ let
name="$1" name="$1"
getFirejailProfile "$name" getFirejailProfile "$name"
mv "$out/bin/$name" "$out/bin/.$name-firejailed" mv "$out/bin/$name" "$out/bin/.$name-firejailed"
cat <<EOF >> "$out/bin/$name" cat <<EOF >> "tmp-firejail-$name"
#!/bin/sh $firejailProfileFlags \
exec ${firejailBin} ${lib.concatStringsSep " " firejailFlags} $firejailProfileFlags "$out/bin/.$name-firejailed" "\$@" --join-or-start="${package.name}-$name" \
-- "$out/bin/.$name-firejailed" "\$@"
EOF EOF
chmod +x "$out/bin/$p" cat ${firejailBase} "tmp-firejail-$name" > "$out/bin/$name"
chmod +x "$out/bin/$name"
} }
for p in $(ls "$out/bin/"); do for _p in $(ls "$out/bin/"); do
firejailWrap "$p" firejailWrap "$_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-firejail
''; '';
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 {}) // {
checkSandboxed = pkgs.runCommand "${package.name}-check-sandboxed" {} ''
# this pseudo-package gets "built" as part of toplevel system build.
# if the build is failing here, that means the program isn't properly sandboxed:
# make sure that "postFixup" gets called as part of the package's build script
test -f "${packageWrapped}/nix-support/sandboxed-${sandbox.method}" \
&& touch "$out"
'';
};
});
in
packageWrapped
else else
throw "unknown net type '${net}'" throw "unknown net type '${net}'"
); );
@ -235,6 +270,13 @@ let
- "vpn" to route all traffic over the default VPN. - "vpn" to route all traffic over the default VPN.
''; '';
}; };
sandbox.method = mkOption {
type = types.nullOr (types.enum [ "firejail" ]);
default = null; #< TODO: default to firejail
description = ''
how/whether to sandbox all binaries in the package.
'';
};
configOption = mkOption { configOption = mkOption {
type = types.raw; type = types.raw;
default = mkOption { default = mkOption {
@ -258,18 +300,27 @@ let
package = if config.packageUnwrapped == null then package = if config.packageUnwrapped == null then
null null
else else
wrapPkg { inherit (config) net; } config.packageUnwrapped wrapPkg config config.packageUnwrapped
; ;
}; };
}); });
toPkgSpec = with lib; types.coercedTo types.package (p: { package = p; }) pkgSpec; toPkgSpec = with lib; types.coercedTo types.package (p: { package = p; }) pkgSpec;
configs = lib.mapAttrsToList (name: p: { configs = lib.mapAttrsToList (name: p: {
assertions = builtins.map (sug: { assertions = [
{
assertion = (p.net == "clearnet") || p.sandbox.method != null;
message = ''program "${name}" requests net "${p.net}", which requires sandboxing, but sandboxing was disabled'';
}
] ++ builtins.map (sug: {
assertion = cfg ? "${sug}"; assertion = cfg ? "${sug}";
message = ''program "${sug}" referenced by "${name}", but not defined''; message = ''program "${sug}" referenced by "${name}", but not defined'';
}) p.suggestedPrograms; }) p.suggestedPrograms;
system.checks = lib.optionals (p.enabled && p.sandbox.method != null && p.package != null) [
p.package.passthru.checkSandboxed
];
# conditionally add to system PATH and env # conditionally add to system PATH and env
environment = lib.optionalAttrs (p.enabled && p.enableFor.system) { environment = lib.optionalAttrs (p.enabled && p.enableFor.system) {
systemPackages = lib.optional (p.package != null) p.package; systemPackages = lib.optional (p.package != null) p.package;
@ -343,6 +394,7 @@ in
users.users = f.users.users; users.users = f.users.users;
sane.users = f.sane.users; sane.users = f.sane.users;
sops.secrets = f.sops.secrets; sops.secrets = f.sops.secrets;
system.checks = f.system.checks;
}; };
in lib.mkMerge [ in lib.mkMerge [
(take (sane-lib.mkTypedMerge take configs)) (take (sane-lib.mkTypedMerge take configs))