about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGuillaume Girol <symphorien@users.noreply.github.com>2024-04-27 22:48:20 +0200
committerGitHub <noreply@github.com>2024-04-27 22:48:20 +0200
commit3ed7049cddddc675ea43a2c8b1c925e69230aa43 (patch)
treeb5b2efd20e5bf46e275c8c60d28c0e80068ccfdd
parent33812a15b4dc2b35d31fea4a425839bcc970d542 (diff)
parent4a0a12efc2433642f6fa28d7837983a3c83796aa (diff)
Merge pull request #305853 from virchau13s-forks/isolate-module
isolate: add module and module tests
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/security/isolate.nix133
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/isolate.nix38
-rw-r--r--pkgs/tools/security/isolate/default.nix16
-rw-r--r--pkgs/tools/security/isolate/take-config-file-from-env.patch19
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 17df5d71f9724..bd3ae81cc2ec8 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 50157f62ed320..78107e33b9e9a 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 0000000000000..3cc0176f3db30
--- /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 7b47fbf5662e3..2c9d1aa568bf2 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 0000000000000..327231be1cd4a
--- /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 b745af75d8b7f..a1d67c49d531d 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 0000000000000..c42d9a6e45b83
--- /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))