diff --git a/nixos/modules/services/system/nix-daemon.nix b/nixos/modules/services/system/nix-daemon.nix index ef740b9ba8d94..5798f30ca7b45 100644 --- a/nixos/modules/services/system/nix-daemon.nix +++ b/nixos/modules/services/system/nix-daemon.nix @@ -62,6 +62,101 @@ in ]; }) (lib.mkRemovedOptionModule [ "nix" "daemonNiceLevel" ] "Consider nix.daemonCPUSchedPolicy instead.") + { + # Unprivileged Nix daemon + config = lib.mkIf (cfg.daemonUser != "root") { + assertions = [ + { + message = '' + The Nix daemon cannot run as the root group when not running as the root user. + ''; + assertion = cfg.daemonGroup != "root"; + } + { + message = '' + Nix must have the `local-overlay-store` experimental feature when not running as the root user. + ''; + assertion = lib.elem "local-overlay-store" cfg.settings.experimental-features; + } + { + message = '' + Nix must have the `auto-allocate-uids` experimental feature when not running as the root user. + ''; + assertion = lib.elem "auto-allocate-uids" cfg.settings.experimental-features; + } + ]; + + nix.settings = { + sandbox = true; + + auto-allocate-uids = true; + + # No such group would exist within the sandbox, so chowning to it would fail + build-users-group = ""; + + # Default settings from Nix, we need to specify them here to use them in nix code though + start-id = lib.mkDefault (832 * 1024 * 1024); + id-count = lib.mkDefault (128 * 65536); + }; + + systemd.services.nix-daemon = { + # Nix assumes it should use `daemon` if it isn't root, so we have to set `NIX_REMOTE` anyway + environment.NIX_REMOTE = "local?use-roots-daemon=true"; + serviceConfig = { + User = cfg.daemonUser; + Group = cfg.daemonGroup; + + # Empty string needed to disable old Exec + ExecStart = [ + "" + "${nixPackage}/libexec/nix-nswrapper ${toString cfg.settings.start-id} ${toString cfg.settings.id-count} ${nixPackage}/bin/nix-daemon --daemon" + ]; + }; + }; + + # We can't remount rw while unprivileged + boot.nixStoreMountOpts = [ + "nodev" + "nosuid" + ]; + + users.users."${cfg.daemonUser}" = { + subUidRanges = [ + { + startUid = cfg.settings.start-id; + count = cfg.settings.id-count; + } + ]; + subGidRanges = [ + { + startGid = cfg.settings.start-id; + count = cfg.settings.id-count; + } + ]; + }; + + systemd.tmpfiles.rules = [ + "d /nix/store 0755 ${config.nix.daemonUser} ${config.nix.daemonGroup} - -" + "Z /nix/var 0755 ${config.nix.daemonUser} ${config.nix.daemonGroup} - -" + "d /nix/var/nix/builds 0755 ${config.nix.daemonUser} ${config.nix.daemonGroup} - 7d" + "d /nix/var/nix/daemon-socket 0755 ${config.nix.daemonUser} ${config.nix.daemonGroup} - -" + ]; + + systemd.services.nix-roots-daemon = { + serviceConfig.ExecStart = "${config.nix.package.out}/bin/nix --extra-experimental-features nix-command store roots-daemon"; + }; + systemd.sockets.nix-roots-daemon = { + wantedBy = [ + "nix-daemon.service" + ]; + listenStreams = [ "/nix/var/nix/gc-roots-socket/socket" ]; + unitConfig = { + ConditionPathIsReadWrite = "/nix/var/nix/gc-roots-socket"; + RequiresMountsFor = "/nix/store"; + }; + }; + }; + } ]; ###### interface @@ -88,6 +183,24 @@ in ''; }; + daemonUser = lib.mkOption { + type = lib.types.str; + default = "root"; + description = '' + User to use to run the Nix daemon. + If this is not "root" then the Nix daemon will set several settings to preserve functionality. + When setting this option, you must also set `nix.daemonGroup`. + ''; + }; + + daemonGroup = lib.mkOption { + type = lib.types.str; + default = "root"; + description = '' + Group to use to run the Nix daemon. + ''; + }; + daemonCPUSchedPolicy = lib.mkOption { type = lib.types.enum [ "other" @@ -192,7 +305,8 @@ in systemd.packages = [ nixPackage ]; - systemd.tmpfiles.packages = [ nixPackage ]; + # The upstream Nix tmpfiles.d file assumes the daemon runs as root + systemd.tmpfiles.packages = lib.mkIf (cfg.daemonUser == "root") [ nixPackage ]; systemd.sockets.nix-daemon.wantedBy = [ "sockets.target" ]; @@ -200,7 +314,9 @@ in path = [ nixPackage config.programs.ssh.package - ]; + ] + # For running "newuidmap" + ++ lib.optional (cfg.daemonUser != "root") "/run/wrappers"; environment = cfg.envVars diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a4c56e474fba1..bff977f16733b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1099,6 +1099,7 @@ in nix-channel = pkgs.callPackage ../modules/config/nix-channel/test.nix { }; nix-config = runTest ./nix-config.nix; nix-daemon-firewall = runTest ./nix-daemon-firewall.nix; + nix-daemon-unprivileged = runTest ./nix-daemon-unprivileged.nix; nix-ld = runTest ./nix-ld.nix; nix-misc = handleTest ./nix/misc.nix { }; nix-required-mounts = runTest ./nix-required-mounts; diff --git a/nixos/tests/nix-daemon-unprivileged.nix b/nixos/tests/nix-daemon-unprivileged.nix new file mode 100644 index 0000000000000..34aa3d8b83ce0 --- /dev/null +++ b/nixos/tests/nix-daemon-unprivileged.nix @@ -0,0 +1,38 @@ +{ lib, pkgs, ... }: +{ + name = "nix-daemon-unprivileged"; + meta.maintainers = with lib.maintainers; [ artemist ]; + + nodes.machine = { + users.groups.nix-daemon = { }; + users.users.nix-daemon = { + isSystemUser = true; + group = "nix-daemon"; + }; + + nix = { + package = pkgs.nixVersions.git; + daemonUser = "nix-daemon"; + daemonGroup = "nix-daemon"; + settings.experimental-features = [ + "local-overlay-store" + "auto-allocate-uids" + ]; + }; + + # Easiest way to get a file onto the machine + environment.etc."test.nix".text = '' + derivation { + name = "test"; + builder = "/bin/sh"; + args = [ "-c" "echo succeeded > $out" ]; + system = "${pkgs.stdenv.hostPlatform.system}"; + } + ''; + }; + testScript = '' + start_all() + machine.wait_for_unit("sockets.target") + machine.succeed("NIX_REMOTE=daemon nix-build /etc/test.nix") + ''; +}