mirror of
https://github.com/NixOS/nix
synced 2025-06-24 22:11:15 +02:00
linux: support supplementary groups with auto-allocate-uids
Add a new option "supplementary-groups" that allows specifying additional groups to be mapped into the build sandbox and assigned as supplementary groups for the build user. This makes it possible to, for example, use the "kvm" group to provide access to /dev/kvm even when auto-allocate-uids is enabled—something that was not previously possible. It also enables use of supplementary groups to grant sandboxed builds read-only access to secrets or other shared resources. Closes: https://github.com/NixOS/nix/issues/9276 Signed-off-by: Samuli Thomasson <samuli.thomasson@pm.me>
This commit is contained in:
parent
587b5f5361
commit
62cc737d19
2 changed files with 161 additions and 9 deletions
|
@ -484,6 +484,35 @@ public:
|
|||
"id-count",
|
||||
"The number of UIDs/GIDs to use for dynamic ID allocation."};
|
||||
|
||||
Setting<StringSet> supplementaryGroups{
|
||||
this, {"kvm"}, "supplementary-groups",
|
||||
R"(
|
||||
A list of supplementary groups to be added to build users when dynamic
|
||||
UID/GID allocation [`auto-allocate-uids`](#conf-auto-allocate-uids) is
|
||||
enabled.
|
||||
|
||||
By default, each listed group is mapped into the build sandbox using its
|
||||
host system GID. You can override the mapped GID by appending `:GID` to
|
||||
the group name. If a group does not exist on the host (i.e., has no
|
||||
assigned GID), it is silently ignored.
|
||||
|
||||
Example:
|
||||
|
||||
```nix
|
||||
extra-supplementary-groups = nixbld users:10100
|
||||
```
|
||||
|
||||
This maps the `nixbld` group using its assigned GID (usually 30000).
|
||||
And the `users` group gets assigned GID 10100.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> When using [`build-users-group`](#conf-build-users-group),
|
||||
> supplementary groups should instead be specified directly in the
|
||||
> user definitions.
|
||||
)",
|
||||
{"build-supplementary-groups"}};
|
||||
|
||||
#ifdef __linux__
|
||||
Setting<bool> useCgroups{
|
||||
this, false, "use-cgroups",
|
||||
|
|
|
@ -221,6 +221,105 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
|
|||
return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID();
|
||||
}
|
||||
|
||||
bool setSupplementaryGroups;
|
||||
|
||||
/**
|
||||
* Return parent_gid -> (mapped_gid, mapped_name)
|
||||
*
|
||||
* Keys are host/parent gid's (can't have duplicates obviously). Values
|
||||
* are the mapped/target (gid, name) pairs. Each mapped gid (name) may
|
||||
* only appear once in the result.
|
||||
*/
|
||||
std::map<gid_t, std::tuple<gid_t, std::string>> getSupplementaryGIDMap()
|
||||
{
|
||||
// Primary GID (sandboxGid) is always mapped and it would make no
|
||||
// sense to assign as a supplementary group as well.
|
||||
//
|
||||
// It would be technically harmless to allow mapping and assigning 0,
|
||||
// aside from some potential confusion. It's only disallowed here
|
||||
// because the root group is declared always anyway. Allowing it here
|
||||
// would result in a duplicate /etc/group entry.
|
||||
//
|
||||
// Allowing assigning nogroup 65534 would be very bad, especially if
|
||||
// root 0 wasn't mapped, as it's the fallback group to which all
|
||||
// unmapped GIDs get mapped to (including root).
|
||||
std::vector<gid_t> reserved_gids = {sandboxGid(), 0, 65534};
|
||||
std::vector<std::string> reserved_names = {"root", "nixbld", "nogroup"};
|
||||
|
||||
std::map<gid_t, std::tuple<gid_t, std::string>> gid_map;
|
||||
|
||||
struct group grp;
|
||||
struct group * gr = nullptr;
|
||||
long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX);
|
||||
if (bufsize == -1) bufsize = 16384;
|
||||
std::vector<char> buffer(bufsize);
|
||||
|
||||
for (const auto & group_entry : settings.supplementaryGroups.get()) {
|
||||
std::string group_name = group_entry;
|
||||
std::optional<gid_t> mapped_gid;
|
||||
|
||||
// parse "name[:gid]"
|
||||
auto pos = group_entry.find(":");
|
||||
if (pos != std::string::npos) {
|
||||
std::string gid_str = group_entry.substr(pos + 1);
|
||||
group_name = group_entry.substr(0, pos);
|
||||
try {
|
||||
mapped_gid = static_cast<gid_t>(std::stoul(gid_str));
|
||||
} catch (...) {
|
||||
throw Error("Invalid GID number in '%s'", gid_str);
|
||||
}
|
||||
}
|
||||
|
||||
int ret = getgrnam_r(group_name.c_str(), &grp, buffer.data(), buffer.size(), &gr);
|
||||
if (ret != 0)
|
||||
throw Error("getgrnam_r failed for group '%s': %s", group_name, strerror(ret));
|
||||
if (!gr) {
|
||||
debug("Supplementary group '%s' not found", group_name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// host GID sanity checks
|
||||
gid_t parent_gid = gr->gr_gid;
|
||||
if (parent_gid == 0)
|
||||
throw Error("Group '%s': mapping the root group (GID 0) is not a good idea", group_name);
|
||||
if (gid_map.contains(parent_gid))
|
||||
throw Error("Group '%s': parent GID %d is already mapped", group_name, parent_gid);
|
||||
|
||||
/* Mapped GID sanity checks
|
||||
|
||||
65535 is the special invalid/overflow GID distinct from
|
||||
nogroup. We don't want to allow that.
|
||||
|
||||
2^16 and above are not allowed because it would seem impossible
|
||||
to assign them. In a quick test higher GIDs got truncated to
|
||||
65534. Might have something to do with how we're forced to call
|
||||
setgroups before setting up the namespace. */
|
||||
if (!mapped_gid)
|
||||
mapped_gid = parent_gid;
|
||||
if (mapped_gid > 65534)
|
||||
throw Error("Group '%s': mapped GID %d is too large (>65534)", group_name, mapped_gid.value());
|
||||
if (std::find(reserved_gids.begin(), reserved_gids.end(), mapped_gid.value()) != reserved_gids.end())
|
||||
throw Error("Group '%s': mapped GID %d conflicts with reserved GID", group_name, mapped_gid.value());
|
||||
|
||||
// mapped name sanity checks
|
||||
std::string mapped_name = gr->gr_name;
|
||||
if (std::find(reserved_names.begin(), reserved_names.end(), mapped_name) != reserved_names.end()) {
|
||||
mapped_name += "-host";
|
||||
debug("Group '%s': name conflicts with reserved name; renaming to '%s'", group_name, mapped_name);
|
||||
if (std::find(reserved_names.begin(), reserved_names.end(), mapped_name) != reserved_names.end()) {
|
||||
warn("Group '%s': original and alternative name '%s' both conflict with reserved names; skipping this group", group_name, mapped_name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
gid_map[parent_gid] = std::make_tuple(mapped_gid.value(), std::move(mapped_name));
|
||||
reserved_gids.push_back(mapped_gid.value());
|
||||
reserved_names.push_back(std::get<1>(gid_map[parent_gid]));
|
||||
}
|
||||
|
||||
return gid_map;
|
||||
}
|
||||
|
||||
bool needsHashRewrite() override
|
||||
{
|
||||
return false;
|
||||
|
@ -292,6 +391,8 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
|
|||
}
|
||||
}
|
||||
|
||||
setSupplementaryGroups = settings.autoAllocateUids && getuid() == 0;
|
||||
|
||||
// Kill any processes left in the cgroup or build user.
|
||||
DerivationBuilderImpl::prepareUser();
|
||||
}
|
||||
|
@ -343,12 +444,17 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
|
|||
|
||||
/* Declare the build user's group so that programs get a consistent
|
||||
view of the system (e.g., "id -gn"). */
|
||||
writeFile(
|
||||
chrootRootDir + "/etc/group",
|
||||
fmt("root:x:0:\n"
|
||||
"nixbld:!:%1%:\n"
|
||||
"nogroup:x:65534:\n",
|
||||
sandboxGid()));
|
||||
std::ostringstream oss;
|
||||
oss << fmt(
|
||||
"root:x:0:\n"
|
||||
"nogroup:x:65534:\n"
|
||||
"nixbld:!:%d:\n", sandboxGid());
|
||||
|
||||
if (setSupplementaryGroups)
|
||||
for (const auto & [parent_gid, mapped] : getSupplementaryGIDMap())
|
||||
oss << fmt("%s:x:%d:nixbld\n", std::get<1>(mapped), std::get<0>(mapped));
|
||||
|
||||
writeFile(chrootRootDir + "/etc/group", oss.str());
|
||||
|
||||
/* Create /etc/hosts with localhost entry. */
|
||||
if (derivationType.isSandboxed())
|
||||
|
@ -452,6 +558,13 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
|
|||
|
||||
usingUserNamespace = userNamespacesSupported();
|
||||
|
||||
// NOTE: setting supplementary groups like this only only works in
|
||||
// certain conditions (root permissions).
|
||||
std::vector<gid_t> supplementaryGroups;
|
||||
if (setSupplementaryGroups)
|
||||
for (const auto & [parent_gid, mapped] : getSupplementaryGIDMap())
|
||||
supplementaryGroups.push_back(std::get<0>(mapped));
|
||||
|
||||
Pipe sendPid;
|
||||
sendPid.create();
|
||||
|
||||
|
@ -464,9 +577,9 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
|
|||
openSlave();
|
||||
|
||||
try {
|
||||
/* Drop additional groups here because we can't do it
|
||||
/* Drop and/or set additional groups here because we can't do it
|
||||
after we've created the new user namespace. */
|
||||
if (setgroups(0, 0) == -1) {
|
||||
if (setgroups(supplementaryGroups.size(), supplementaryGroups.data()) == -1) {
|
||||
if (errno != EPERM)
|
||||
throw SysError("setgroups failed");
|
||||
if (settings.requireDropSupplementaryGroups)
|
||||
|
@ -522,7 +635,17 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
|
|||
if (!buildUser || buildUser->getUIDCount() == 1)
|
||||
writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny");
|
||||
|
||||
writeFile("/proc/" + std::to_string(pid) + "/gid_map", fmt("%d %d %d", sandboxGid(), hostGid, nrIds));
|
||||
std::ostringstream oss;
|
||||
|
||||
// Primary GID mapping
|
||||
oss << fmt("%d %d %d", sandboxGid(), hostGid, nrIds);
|
||||
|
||||
if (setSupplementaryGroups)
|
||||
// Supplementary GIDs one by one
|
||||
for (const auto & [parent_gid, mapped] : getSupplementaryGIDMap())
|
||||
oss << fmt("\n%d %d 1", std::get<0>(mapped), parent_gid);
|
||||
|
||||
writeFile("/proc/" + std::to_string(pid) + "/gid_map", oss.str());
|
||||
} else {
|
||||
debug("note: not using a user namespace");
|
||||
if (!buildUser)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue