Merge pull request #305853 from virchau13s-forks/isolate-module

isolate: add module and module tests

Changed files
+207 -3
nixos
doc
manual
release-notes
modules
tests
pkgs
+2
nixos/doc/manual/release-notes/rl-2405.section.md
···
- [prometheus-nats-exporter](https://github.com/nats-io/prometheus-nats-exporter), a Prometheus exporter for NATS. Available as [services.prometheus.exporters.nats](#opt-services.prometheus.exporters.nats.enable).
+
- [isolate](https://github.com/ioi/isolate), a sandbox for securely executing untrusted programs. Available as [security.isolate](#opt-security.isolate.enable).
+
## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
+1
nixos/modules/module-list.nix
···
./security/duosec.nix
./security/google_oslogin.nix
./security/ipa.nix
+
./security/isolate.nix
./security/krb5
./security/lock-kernel-modules.nix
./security/misc.nix
+133
nixos/modules/security/isolate.nix
···
+
{ config, lib, pkgs, ... }:
+
+
let
+
inherit (lib) mkEnableOption mkPackageOption mkOption types mkIf maintainers;
+
+
cfg = config.security.isolate;
+
configFile = pkgs.writeText "isolate-config.cf" ''
+
box_root=${cfg.boxRoot}
+
lock_root=${cfg.lockRoot}
+
cg_root=${cfg.cgRoot}
+
first_uid=${toString cfg.firstUid}
+
first_gid=${toString cfg.firstGid}
+
num_boxes=${toString cfg.numBoxes}
+
restricted_init=${if cfg.restrictedInit then "1" else "0"}
+
${cfg.extraConfig}
+
'';
+
isolate = pkgs.symlinkJoin {
+
name = "isolate-wrapped-${pkgs.isolate.version}";
+
+
paths = [ pkgs.isolate ];
+
+
nativeBuildInputs = [ pkgs.makeWrapper ];
+
+
postBuild = ''
+
wrapProgram $out/bin/isolate \
+
--set ISOLATE_CONFIG_FILE ${configFile}
+
+
wrapProgram $out/bin/isolate-cg-keeper \
+
--set ISOLATE_CONFIG_FILE ${configFile}
+
'';
+
};
+
in
+
{
+
options.security.isolate = {
+
enable = mkEnableOption ''
+
Sandbox for securely executing untrusted programs
+
'';
+
+
package = mkPackageOption pkgs "isolate-unwrapped" { };
+
+
boxRoot = mkOption {
+
type = types.path;
+
default = "/var/lib/isolate/boxes";
+
description = ''
+
All sandboxes are created under this directory.
+
To avoid symlink attacks, this directory and all its ancestors
+
must be writeable only by root.
+
'';
+
};
+
+
lockRoot = mkOption {
+
type = types.path;
+
default = "/run/isolate/locks";
+
description = ''
+
Directory where lock files are created.
+
'';
+
};
+
+
cgRoot = mkOption {
+
type = types.str;
+
default = "auto:/run/isolate/cgroup";
+
description = ''
+
Control group which subgroups are placed under.
+
Either an explicit path to a subdirectory in cgroupfs, or "auto:file" to read
+
the path from "file", where it is put by `isolate-cg-helper`.
+
'';
+
};
+
+
firstUid = mkOption {
+
type = types.numbers.between 1000 65533;
+
default = 60000;
+
description = ''
+
Start of block of UIDs reserved for sandboxes.
+
'';
+
};
+
+
firstGid = mkOption {
+
type = types.numbers.between 1000 65533;
+
default = 60000;
+
description = ''
+
Start of block of GIDs reserved for sandboxes.
+
'';
+
};
+
+
numBoxes = mkOption {
+
type = types.numbers.between 1000 65533;
+
default = 1000;
+
description = ''
+
Number of UIDs and GIDs to reserve, starting from
+
{option}`firstUid` and {option}`firstGid`.
+
'';
+
};
+
+
restrictedInit = mkOption {
+
type = types.bool;
+
default = false;
+
description = ''
+
If true, only root can create sandboxes.
+
'';
+
};
+
+
extraConfig = mkOption {
+
type = types.str;
+
default = "";
+
description = ''
+
Extra configuration to append to the configuration file.
+
'';
+
};
+
};
+
+
config = mkIf cfg.enable {
+
environment.systemPackages = [
+
isolate
+
];
+
+
systemd.services.isolate = {
+
description = "Isolate control group hierarchy daemon";
+
wantedBy = [ "multi-user.target" ];
+
serviceConfig = {
+
Type = "notify";
+
ExecStart = "${isolate}/bin/isolate-cg-keeper";
+
Slice = "isolate.slice";
+
Delegate = true;
+
};
+
};
+
+
systemd.slices.isolate = {
+
description = "Isolate sandbox slice";
+
};
+
+
meta.maintainers = with maintainers; [ virchau13 ];
+
};
+
}
+1
nixos/tests/all-tests.nix
···
honk = runTest ./honk.nix;
installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
invidious = handleTest ./invidious.nix {};
+
isolate = handleTest ./isolate.nix {};
livebook-service = handleTest ./livebook-service.nix {};
pyload = handleTest ./pyload.nix {};
oci-containers = handleTestOn ["aarch64-linux" "x86_64-linux"] ./oci-containers.nix {};
+38
nixos/tests/isolate.nix
···
+
import ./make-test-python.nix ({ lib, ... }:
+
{
+
name = "isolate";
+
meta.maintainers = with lib.maintainers; [ virchau13 ];
+
+
nodes.machine =
+
{ ... }:
+
{
+
security.isolate = {
+
enable = true;
+
};
+
};
+
+
testScript = ''
+
bash_path = machine.succeed('realpath $(which bash)').strip()
+
sleep_path = machine.succeed('realpath $(which sleep)').strip()
+
def sleep_test(walltime, sleeptime):
+
return f'isolate --no-default-dirs --wall-time {walltime} ' + \
+
f'--dir=/box={box_path} --dir=/nix=/nix --run -- ' + \
+
f"{bash_path} -c 'exec -a sleep {sleep_path} {sleeptime}'"
+
+
def sleep_test_cg(walltime, sleeptime):
+
return f'isolate --cg --no-default-dirs --wall-time {walltime} ' + \
+
f'--dir=/box={box_path} --dir=/nix=/nix --processes=2 --run -- ' + \
+
f"{bash_path} -c '( exec -a sleep {sleep_path} {sleeptime} )'"
+
+
with subtest("without cgroups"):
+
box_path = machine.succeed('isolate --init').strip()
+
machine.succeed(sleep_test(1, 0.5))
+
machine.fail(sleep_test(0.5, 1))
+
machine.succeed('isolate --cleanup')
+
with subtest("with cgroups"):
+
box_path = machine.succeed('isolate --cg --init').strip()
+
machine.succeed(sleep_test_cg(1, 0.5))
+
machine.fail(sleep_test_cg(0.5, 1))
+
machine.succeed('isolate --cg --cleanup')
+
'';
+
})
+13 -3
pkgs/tools/security/isolate/default.nix
···
, fetchFromGitHub
, asciidoc
, libcap
+
, pkg-config
+
, systemdLibs
, installShellFiles
+
, nixosTests
}:
stdenv.mkDerivation rec {
···
nativeBuildInputs = [
asciidoc
installShellFiles
+
pkg-config
];
buildInputs = [
libcap.dev
+
systemdLibs.dev
];
-
buildFlags = [
-
"isolate"
-
"isolate.1"
+
patches = [
+
./take-config-file-from-env.patch
];
installPhase = ''
runHook preInstall
install -Dm755 ./isolate $out/bin/isolate
+
install -Dm755 ./isolate-cg-keeper $out/bin/isolate-cg-keeper
+
install -Dm755 ./isolate-check-environment $out/bin/isolate-check-environment
installManPage isolate.1
runHook postInstall
'';
+
+
passthru.tests = {
+
isolate = nixosTests.isolate;
+
};
meta = {
description = "Sandbox for securely executing untrusted programs";
+19
pkgs/tools/security/isolate/take-config-file-from-env.patch
···
+
diff --git a/config.c b/config.c
+
index 6477259..c462ed3 100644
+
--- a/config.c
+
+++ b/config.c
+
@@ -114,9 +114,12 @@ cf_check(void)
+
void
+
cf_parse(void)
+
{
+
- FILE *f = fopen(CONFIG_FILE, "r");
+
+ char *config_file = getenv("ISOLATE_CONFIG_FILE");
+
+ if(!config_file)
+
+ die("ISOLATE_CONFIG_FILE environment variable not set");
+
+ FILE *f = fopen(config_file, "r");
+
if (!f)
+
- die("Cannot open %s: %m", CONFIG_FILE);
+
+ die("Cannot open %s: %m", config_file);
+
+
char line[MAX_LINE_LEN];
+
while (fgets(line, sizeof(line), f))