diff options
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2405.section.md | 2 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/security/isolate.nix | 133 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/isolate.nix | 38 | ||||
-rw-r--r-- | pkgs/tools/security/isolate/default.nix | 16 | ||||
-rw-r--r-- | pkgs/tools/security/isolate/take-config-file-from-env.patch | 19 |
7 files changed, 207 insertions, 3 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 17df5d71f972..bd3ae81cc2ec 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -191,6 +191,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [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. --> diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 50157f62ed32..78107e33b9e9 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -325,6 +325,7 @@ ./security/duosec.nix ./security/google_oslogin.nix ./security/ipa.nix + ./security/isolate.nix ./security/krb5 ./security/lock-kernel-modules.nix ./security/misc.nix diff --git a/nixos/modules/security/isolate.nix b/nixos/modules/security/isolate.nix new file mode 100644 index 000000000000..3cc0176f3db3 --- /dev/null +++ b/nixos/modules/security/isolate.nix @@ -0,0 +1,133 @@ +{ 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 ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 7b47fbf5662e..2c9d1aa568bf 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -399,6 +399,7 @@ in { 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 {}; diff --git a/nixos/tests/isolate.nix b/nixos/tests/isolate.nix new file mode 100644 index 000000000000..327231be1cd4 --- /dev/null +++ b/nixos/tests/isolate.nix @@ -0,0 +1,38 @@ +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') + ''; +}) diff --git a/pkgs/tools/security/isolate/default.nix b/pkgs/tools/security/isolate/default.nix index b745af75d8b7..a1d67c49d531 100644 --- a/pkgs/tools/security/isolate/default.nix +++ b/pkgs/tools/security/isolate/default.nix @@ -3,7 +3,10 @@ , fetchFromGitHub , asciidoc , libcap +, pkg-config +, systemdLibs , installShellFiles +, nixosTests }: stdenv.mkDerivation rec { @@ -20,26 +23,33 @@ 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"; mainProgram = "isolate"; diff --git a/pkgs/tools/security/isolate/take-config-file-from-env.patch b/pkgs/tools/security/isolate/take-config-file-from-env.patch new file mode 100644 index 000000000000..c42d9a6e45b8 --- /dev/null +++ b/pkgs/tools/security/isolate/take-config-file-from-env.patch @@ -0,0 +1,19 @@ +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)) |