diff --git a/doc/manual/rl-next/supplementary-groups-support.md b/doc/manual/rl-next/supplementary-groups-support.md new file mode 100644 index 000000000..65f5ed008 --- /dev/null +++ b/doc/manual/rl-next/supplementary-groups-support.md @@ -0,0 +1,11 @@ +--- +synopsis: "Supplementary groups are now supported for daemon authentication" +cls: [5021] +issues: [fj#968] +category: "Improvements" +credits: [raito, thubrecht, alois31, horrors] +--- + +Linux now support receiving supplementary groups during UNIX domain authentication to a Lix daemon. + +This change is particularly beneficial for systemd units with `DynamicUser=true` that need to connect to a Lix daemon, using a `SupplementaryGroups=` allocated by systemd in the context of the process. This is desirable if you wish to harden Lix clients. diff --git a/lix/nix/daemon-settings/allowed-users.md b/lix/nix/daemon-settings/allowed-users.md index aada459c5..76e8bd131 100644 --- a/lix/nix/daemon-settings/allowed-users.md +++ b/lix/nix/daemon-settings/allowed-users.md @@ -7,9 +7,13 @@ default: ['*'] A list user names, separated by whitespace. These users are allowed to connect to the Nix daemon. -You can specify groups by prefixing names with `@`. -For instance, `@wheel` means all users in the `wheel` group. -Also, you can allow all users by specifying `*`. +You can specify groups by prefixing group names with `@`, like `@wheel` for all +users in the `wheel` group. To allow all users, use `*`. + +Both primary and supplementary groups (when the platform supports it) are +considered when determining membership. Additionally, groups a user belongs to +in the user database (e.g. LDAP if configured) are also taken into account for +access control. > **Note** > diff --git a/lix/nix/daemon-settings/trusted-users.md b/lix/nix/daemon-settings/trusted-users.md index 11e684215..074edbd70 100644 --- a/lix/nix/daemon-settings/trusted-users.md +++ b/lix/nix/daemon-settings/trusted-users.md @@ -7,8 +7,13 @@ default: [root] A list of user names, separated by whitespace. These users will have additional rights when connecting to the Nix daemon, such as the ability to specify additional [substituters](#conf-substituters), or to import unsigned [NARs](@docroot@/glossary.md#gloss-nar). -You can also specify groups by prefixing names with `@`. -For instance, `@wheel` means all users in the `wheel` group. +You can specify groups by prefixing group names with `@`, like `@wheel` for all +users in the `wheel` group. + +Both primary and supplementary groups (when the platform supports it) are +considered when determining membership. Additionally, groups a user belongs to +in the user database (e.g. LDAP if configured) are also taken into account for +access control. > **Warning** > diff --git a/lix/nix/daemon.cc b/lix/nix/daemon.cc index 332f83e60..31b90e09c 100644 --- a/lix/nix/daemon.cc +++ b/lix/nix/daemon.cc @@ -130,22 +130,21 @@ static bool matchUser(const std::string & user, const struct group & gr) return false; } - /** - * Does the given user (specified by user name and primary group name) + * Does the given user (specified by user name, primary group name and supplementary group names) * match the given user/group whitelist? * * If the list allows all users: Yes. * * If the username is in the set: Yes. * - * If the groupname is in the set: Yes. - * * If the user is in another group which is in the set: yes. + * If the groups intersects in the set: Yes. * * Otherwise: No. */ -static bool matchUser(const std::string & user, const std::string & group, const Strings & users) +static bool +matchUser(const std::string & user, const std::unordered_set & groups, const Strings & users) { if (find(users.begin(), users.end(), "*") != users.end()) return true; @@ -156,7 +155,7 @@ static bool matchUser(const std::string & user, const std::string & group, const for (auto & i : users) if (i.substr(0, 1) == "@") { auto rest = i.substr(1); - if (group == rest) { + if (groups.contains(rest)) { return true; } struct group * gr = sys::getgrnam(rest); @@ -292,26 +291,41 @@ static std::pair authPeer(const PeerInfo & peer) struct passwd * pw = getpwuid(peer.uid); std::string user = pw ? pw->pw_name : std::to_string(peer.uid); - struct group * gr = getgrgid(peer.gid); - std::string group = gr ? gr->gr_name : std::to_string(peer.gid); + auto assertHealthyGroup = [&](const std::string & group) { + if (group == settings.buildUsersGroup) { + throw Error( + "the user '%1%' is not allowed to connect to the Nix daemon as its group is '%2%', " + "which is the group of users running the sandboxed builds.", + user, + group + ); + } + }; - if (group == settings.buildUsersGroup) { - throw Error( - "the user '%1%' is not allowed to connect to the Nix daemon as its group is '%2%', " - "which is the group of users running the sandboxed builds.", - user, - group - ); + std::unordered_set groups; + + auto insertGroup = [&](gid_t gid) { + auto gr = getgrgid(gid); + std::string group = gr ? gr->gr_name : std::to_string(gid); + assertHealthyGroup(group); + groups.insert(group); + }; + + // This ensures that the `groups` set is always non-empty with the primary group name. + insertGroup(peer.gid); + + for (const gid_t suppGid : peer.supplementaryGids) { + insertGroup(suppGid); } const Strings & trustedUsers = authorizationSettings.trustedUsers; const Strings & allowedUsers = authorizationSettings.allowedUsers; - if (matchUser(user, group, trustedUsers)) { + if (matchUser(user, groups, trustedUsers)) { trusted = Trusted; } - if (!trusted && !matchUser(user, group, allowedUsers)) { + if (!trusted && !matchUser(user, groups, allowedUsers)) { throw Error("user '%1%' is not allowed to connect to the Nix daemon", user); } diff --git a/tests/nixos/authorization.nix b/tests/nixos/authorization.nix index fdeae06ed..28aff791f 100644 --- a/tests/nixos/authorization.nix +++ b/tests/nixos/authorization.nix @@ -1,15 +1,28 @@ { name = "authorization"; - nodes.machine = { + nodes.machine = { config, ... }: { virtualisation.writableStore = true; # TODO add a test without allowed-users setting. allowed-users is uncommon among NixOS users. - nix.settings.allowed-users = ["alice" "bob"]; + nix.settings.allowed-users = ["alice" "bob" "@special-group"]; nix.settings.trusted-users = ["alice"]; users.users.alice.isNormalUser = true; users.users.bob.isNormalUser = true; users.users.mallory.isNormalUser = true; + users.groups.special-group = {}; + + systemd.services.nix-connect-test = { + description = "Test Nix daemon connection as a DynamicUser with allowed suppgp"; + serviceConfig = { + DynamicUser = true; + SupplementaryGroups = "special-group"; + ExecStart = "${config.nix.package}/bin/nix store ping"; + Restart = "on-failure"; + RemainAfterExit = true; + }; + wantedBy = [ "multi-user.target" ]; + }; nix.settings.experimental-features = "nix-command"; }; @@ -20,6 +33,7 @@ in '' machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("nix-connect-test.service") machine.succeed(""" exec 1>&2 echo kSELDhobKaF8/VdxIxdP7EQe+Q > one