nix/daemon: consider supplementary groups during authentication

Two things happens here:

- we consider supplementary groups that are known for authentication,
fixes #968.

- we check supplementary groups if they are our build users group and
throw an error if they are (new behavior).

Nonetheless, we did not remove the ability for `matchUser` to recurse
and check for groups that the user may have but the connection might not
have communicated as part of their groups, i.e. if a process reduces its
list of groups via a call to setgroups, it will still be authorized.
This will come in another commit.

The authorization NixOS test has been extended with a store ping test
running in systemd with DynamicUser=true *AND* a supplementary group in
allowed-users.

Co-authored-by: Tom Hubrecht <github@mail.hubrecht.ovh>
Change-Id: I25b2b8304d66a04651cea523b5585a5d15ceebe8
Signed-off-by: Raito Bezarius <raito@lix.systems>
This commit is contained in:
Raito Bezarius
2026-01-28 08:24:24 +00:00
co-authored by Tom Hubrecht
parent 9e55dd6b8f
commit c493fb668e
5 changed files with 72 additions and 24 deletions
@@ -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.
+7 -3
View File
@@ -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**
>
+7 -2
View File
@@ -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**
>
+31 -17
View File
@@ -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<std::string> & 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<TrustedFlag, std::string> 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<std::string> 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);
}
+16 -2
View File
@@ -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