nixos/nix: Add support for running an unprivileged daemon (#491809)

This commit is contained in:
John Ericson
2026-02-23 16:17:38 +00:00
committed by GitHub
3 changed files with 157 additions and 2 deletions
+118 -2
View File
@@ -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
+1
View File
@@ -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;
+38
View File
@@ -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")
'';
}