diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index a230df1dd5f8d..d36173177251e 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -872,6 +872,8 @@ - `qgis` and `qgis-ltr` are now built without `grass` by default. `grass` support can be enabled with `qgis.override { withGrass = true; }`. +- `virtualisation.incus` module gained new `incus-user.service` and `incus-user.socket` systemd units. It is now possible to add a user to `incus` group instead of `incus-admin` for increased security. + ## Detailed Migration Information {#sec-release-24.11-migration} ### `sound` options removal {#sec-release-24.11-migration-sound} diff --git a/nixos/modules/virtualisation/incus.nix b/nixos/modules/virtualisation/incus.nix index 777e3b28f2007..aa2102f49f0d7 100644 --- a/nixos/modules/virtualisation/incus.nix +++ b/nixos/modules/virtualisation/incus.nix @@ -153,7 +153,10 @@ in Users in the "incus-admin" group can interact with the daemon (e.g. to start or stop containers) using the - {command}`incus` command line tool, among others + {command}`incus` command line tool, among others. + Users in the "incus" group can also interact with + the daemon, but with lower permissions + (i.e. administrative operations are forbidden). ''; package = lib.mkPackageOption pkgs "incus-lts" { }; @@ -359,6 +362,27 @@ in }; }; + systemd.services.incus-user = { + description = "Incus Container and Virtual Machine Management User Daemon"; + + inherit environment; + + after = [ + "incus.service" + "incus-user.socket" + ]; + + requires = [ + "incus-user.socket" + ]; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/incus-user --group incus"; + + Restart = "on-failure"; + }; + }; + systemd.services.incus-startup = lib.mkIf cfg.softDaemonRestart { description = "Incus Instances Startup/Shutdown"; @@ -391,6 +415,17 @@ in }; }; + systemd.sockets.incus-user = { + description = "Incus user UNIX socket"; + wantedBy = [ "sockets.target" ]; + + socketConfig = { + ListenStream = "/var/lib/incus/unix.socket.user"; + SocketMode = "0660"; + SocketGroup = "incus"; + }; + }; + systemd.services.incus-preseed = lib.mkIf (cfg.preseed != null) { description = "Incus initialization with preseed file"; @@ -409,6 +444,7 @@ in }; }; + users.groups.incus = { }; users.groups.incus-admin = { }; users.users.root = { diff --git a/nixos/tests/incus/incusd-options.nix b/nixos/tests/incus/incusd-options.nix index a223f1c8cb55b..634d236b05d9f 100644 --- a/nixos/tests/incus/incusd-options.nix +++ b/nixos/tests/incus/incusd-options.nix @@ -44,7 +44,7 @@ import ../make-test-python.nix ( preseed = { networks = [ { - name = "nixostestbr0"; + name = "incusbr0"; type = "bridge"; config = { "ipv4.address" = "10.0.100.1/24"; @@ -58,12 +58,12 @@ import ../make-test-python.nix ( devices = { eth0 = { name = "eth0"; - network = "nixostestbr0"; + network = "incusbr0"; type = "nic"; }; root = { path = "/"; - pool = "nixostest_pool"; + pool = "default"; size = "35GiB"; type = "disk"; }; @@ -72,43 +72,69 @@ import ../make-test-python.nix ( ]; storage_pools = [ { - name = "nixostest_pool"; + name = "default"; driver = "dir"; } ]; }; }; + }; + networking.nftables.enable = true; + + users.users.testuser = { + isNormalUser = true; + shell = pkgs.bashInteractive; + group = "incus"; + uid = 1000; + }; }; - testScript = '' - def instance_is_up(_) -> bool: - status, _ = machine.execute("incus exec container --disable-stdin --force-interactive /run/current-system/sw/bin/systemctl -- is-system-running") - return status == 0 - - machine.wait_for_unit("incus.service") - machine.wait_for_unit("incus-preseed.service") - - with subtest("Container image can be imported"): - machine.succeed("incus image import ${container-image-metadata} ${container-image-rootfs} --alias nixos") - - with subtest("Container can be launched and managed"): - machine.succeed("incus launch nixos container") - with machine.nested("Waiting for instance to start and be usable"): - retry(instance_is_up) - machine.succeed("echo true | incus exec container /run/current-system/sw/bin/bash -") - - with subtest("Verify preseed resources created"): - machine.succeed("incus profile show default") - machine.succeed("incus network info nixostestbr0") - machine.succeed("incus storage show nixostest_pool") - - with subtest("Instance is stopped when softDaemonRestart is disabled and services is stopped"): - pid = machine.succeed("incus info container | grep 'PID'").split(":")[1].strip() - machine.succeed(f"ps {pid}") - machine.succeed("systemctl stop incus") - machine.fail(f"ps {pid}") - ''; + testScript = # python + '' + def wait_for_instance(name: str, project: str = "default"): + def instance_is_up(_) -> bool: + status, _ = machine.execute(f"incus exec {name} --disable-stdin --force-interactive --project {project} -- /run/current-system/sw/bin/systemctl is-system-running") + return status == 0 + + with machine.nested(f"Waiting for instance {name} to start and be usable"): + retry(instance_is_up) + + machine.wait_for_unit("incus.service") + machine.wait_for_unit("incus-preseed.service") + + with subtest("Container image can be imported"): + machine.succeed("incus image import ${container-image-metadata} ${container-image-rootfs} --alias nixos") + + with subtest("Container can be launched and managed"): + machine.succeed("incus launch nixos instance1") + wait_for_instance("instance1") + machine.succeed("echo true | incus exec instance1 /run/current-system/sw/bin/bash -") + + with subtest("Verify preseed resources created"): + machine.succeed("incus profile show default") + machine.succeed("incus network info incusbr0") + machine.succeed("incus storage show default") + + with subtest("Instance is stopped when softDaemonRestart is disabled and services is stopped"): + pid = machine.succeed("incus info instance1 | grep 'PID'").split(":")[1].strip() + machine.succeed(f"ps {pid}") + machine.succeed("systemctl stop incus") + machine.fail(f"ps {pid}") + + with subtest("incus-user allows restricted access for users"): + machine.fail("incus project show user-1000") + machine.succeed("su - testuser bash -c 'incus list'") + # a project is created dynamically for the user + machine.succeed("incus project show user-1000") + # users shouldn't be able to list storage pools + machine.fail("su - testuser bash -c 'incus storage list'") + + with subtest("incus-user allows users to launch instances"): + machine.succeed("su - testuser bash -c 'incus image import ${container-image-metadata} ${container-image-rootfs} --alias nixos'") + machine.succeed("su - testuser bash -c 'incus launch nixos instance2'") + wait_for_instance("instance2", "user-1000") + ''; } )