nixos-config/modules/wrapper-manager/sandboxing/bubblewrap/filesystem.nix

249 lines
8.2 KiB
Nix

# Essentially a poor man's version of NixOS filesystem module except that is
# made for Bubblewrap environment. Everything here should only make use of
# Bubblewrap's filesystem options from the command-line application.
{ config, lib, pkgs, ... }:
let
cfg = config.sandboxing.bubblewrap;
fileOperationsWithPerms = [
"file" "dir"
"bind-data" "ro-bind-data"
];
fileOperationsWithoutPerms = [
"symlink"
"bind" "bind-try"
"dev-bind" "dev-bind-try"
"ro-bind" "ro-bind-try"
];
bubblewrapModuleFactory = { isGlobal ? false }: let
filesystemSubmodule = { config, lib, name, ... }: {
options = {
source = lib.mkOption {
type = lib.types.path;
description = ''
The source of the path to be copied from.
'';
example = lib.literalExpression "./files/example.file";
};
permissions = lib.mkOption {
type = with lib.types; nullOr (strMatch "[0-7]{0,4}");
description = ''
The permissions of the node in octal. If the value is `null`, it
will be handled by Bubblewrap executable. For more details for each
operation, see {manpage}`bwrap(1)`.
'';
default = null;
example = "0755";
};
operation = lib.mkOption {
type = lib.types.enum (fileOperationsWithPerms ++ fileOperationsWithoutPerms);
description = ''
Specify what filesystem-related operations to be done for the given
filesystem object. Only certain operations accept permissions given
from {option}`sandboxing.bubblewrap.filesystem.<name>.permissions`.
'';
default = "ro-bind-try";
example = "bind";
};
lock = lib.mkEnableOption "locking the file";
};
};
in {
enableSharedNixStore = lib.mkEnableOption null // {
default = if isGlobal then false else cfg.enableSharedNixStore;
description = ''
Whether to share the entire Nix store directory.
::: {.caution}
Typically, this is not recommended especially for Bubblewrap
environments. If you want to bind some of the items from the Nix store,
it is recommended to use {option}`sandboxing.bubblewrap.sharedNixPaths` instead.
:::
'';
};
sharedNixPaths = lib.mkOption {
type = with lib.types; listOf package;
default = [ ];
description = if isGlobal then ''
A global list of store paths (including its dependencies) to be shared
per-Bubblewrap-enabled-wrappers.
'' else ''
A list of store paths to be mounted (as read-only bind-mounts). Note
that this also includes the listed store objects' dependencies.
'';
example = lib.literalExpression ''
with pkgs; [
gtk3
]
'';
};
binds = {
ro = lib.mkOption {
type = with lib.types; listOf path;
default = [ ];
description =
if isGlobal
then ''
Global list of read-only mounts to be given to all
Bubblewrap-enabled wrappers.
''
else ''
List of read-only mounts to the Bubblewrap environment.
'';
example = [
"/etc/resolv.conf"
"/etc/ssh"
];
};
rw = lib.mkOption {
type = with lib.types; listOf path;
default = [ ];
description =
if isGlobal
then ''
Global list of read-write mounts to be given to all
Bubblewrap-enabled wrappers.
''
else ''
List of read-write mounts to the Bubblewrap environment.
'';
};
dev = lib.mkOption {
type = with lib.types; listOf path;
default = [ ];
description =
if isGlobal
then ''
Global list of devices to be mounted to all Bubblewrap-enabled
wrappers.
''
else ''
List of devices to be mounted inside of the Bubblewrap environment.
'';
};
};
filesystem = lib.mkOption {
type = with lib.types; attrsOf (submodule filesystemSubmodule);
description =
if isGlobal then ''
Set of filesystem configurations to be copied to per-wrapper.
'' else ''
Set of wrapper-specific filesystem configurations in the Bubblewrap
environment.
'';
default = { };
example = lib.literalExpression ''
{
"/etc/hello" = {
source = ./files/hello;
permissions = "0700";
operation = "file";
};
"/etc/xdg" = {
permissions = "0700";
operation = "dir";
};
"/srv/data" = {
source = "/srv/data";
operation = "symlink";
};
"/srv/logs".operation = "dir";
}
'';
};
};
# TODO: There has to be a better way to get this info without relying on
# pkgs.closureInfo builder, right?
getClosurePaths = rootpaths:
let
sharedNixPathsClosureInfo = pkgs.closureInfo { inherit rootpaths; };
closurePaths = lib.readFile "${sharedNixPathsClosureInfo}/store-paths";
in
lib.lists.filter (p: p != "") (lib.splitStrings "\n" closurePaths);
in
{
options.sandboxing.bubblewrap = bubblewrapModuleFactory { isGlobal = true; };
config.sandboxing.bubblewrap.binds.ro = getClosurePaths cfg.sharedNixPaths;
config.sandboxing.bubblewrap.filesystem =
let
makeFilesystemMapping = operation: bind:
lib.nameValuePair bind { inherit operation; source = bind; };
filesystemMappings =
lib.lists.map (makeFilesystemMapping "ro-bind-try") cfg.binds.ro
++ lib.lists.map (makeFilesystemMapping "bind") cfg.binds.rw
++ lib.lists.map (makeFilesystemMapping "dev-bind-try") cfg.binds.dev;
in
builtins.listToAttrs filesystemMappings;
options.wrappers =
let
bubblewrapModule = { config, lib, name, ... }: let
submoduleCfg = config.sandboxing.bubblewrap;
in {
options.sandboxing.bubblewrap = bubblewrapModuleFactory { isGlobal = false; };
config = lib.mkIf (config.sandboxing.variant == "bubblewrap") (lib.mkMerge [
{
sandboxing.bubblewrap.binds.ro = getClosurePaths submoduleCfg.sharedNixPaths;
sandboxing.bubblewrap.filesystem =
let
makeFilesystemMapping = operation: bind:
lib.nameValuePair bind { inherit operation; source = bind; };
filesystemMappings =
lib.lists.map (makeFilesystemMapping "ro-bind-try") submoduleCfg.binds.ro
++ lib.lists.map (makeFilesystemMapping "bind") submoduleCfg.binds.rw
++ lib.lists.map (makeFilesystemMapping "dev-bind-try") submoduleCfg.binds.dev;
in
builtins.listToAttrs filesystemMappings;
sandboxing.bubblewrap.extraArgs =
let
makeFilesystemArgs = dst: metadata:
let
src = metadata.source;
hasPermissions = metadata.permissions != null;
isValidOperationWithPerms = lib.elem metadata.operation fileOperationsWithPerms;
in
lib.optionals (hasPermissions && isValidOperationWithPerms) [ "--perms ${metadata.permissions}" ]
++ (
if metadata.operation == "dir"
then [ "--${metadata.operation} ${dst}" ]
else [ "--${metadata.operation} ${src} ${dst}" ]
)
++ lib.optionals metadata.lock [ "--lock-file ${dst}" ];
in
lib.lists.flatten
(lib.mapAttrsToList makeFilesystemArgs submoduleCfg.filesystem);
}
{
sandboxing.bubblewrap.binds = cfg.binds;
sandboxing.bubblewrap.filesystem = cfg.filesystem;
}
(lib.mkIf submoduleCfg.enableSharedNixStore {
sandboxing.bubblewrap.binds.ro = [ builtins.storeDir ] ++ lib.optionals (builtins.storeDir != "/nix/store") [ "/nix/store" ];
})
]);
};
in
lib.mkOption {
type = with lib.types; attrsOf (submodule bubblewrapModule);
};
}