diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml
index d11f7ed934..ed0acc6937 100644
--- a/man/systemd-nspawn.xml
+++ b/man/systemd-nspawn.xml
@@ -1601,7 +1601,7 @@ After=sys-subsystem-net-devices-ens1.device
Binds the home directory of the specified user on the host into the container. Takes
the name of an existing user on the host as argument. May be used multiple times to bind multiple
- users into the container. This does three things:
+ users into the container. This does two things:
The user's home directory is bind mounted from the host into
@@ -1616,7 +1616,7 @@ After=sys-subsystem-net-devices-ens1.device
user/group databases.
- The combination of the three operations above ensures that it is possible to log into the
+ The combination of the two operations above ensures that it is possible to log into the
container using the same account information as on the host. The user is only mapped transiently,
while the container is running, and the mapping itself does not result in persistent changes to the
container (except maybe for log messages generated at login time, and similar). Note that in
@@ -1667,6 +1667,19 @@ After=sys-subsystem-net-devices-ens1.device
+
+
+
+ When used with , includes the specified group as an
+ auxiliary group in the user records of users bound into the container. Takes a group name.
+
+ Note: This will not check whether the specified groups exist in the container.
+
+ This operation is only supported in combination with .
+
+
+
+
diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml
index 6d677cf106..7b26bec067 100644
--- a/man/systemd-vmspawn.xml
+++ b/man/systemd-vmspawn.xml
@@ -526,6 +526,20 @@
+
+
+
+
+ When used with , includes the specified group as an
+ auxiliary group in the user records of users bound into the virtual machine. Takes a group name.
+
+ Note: This will not check whether the specified groups exist in the virtual machine.
+
+ This operation is only supported in combination with .
+
+
+
+
diff --git a/mkosi/mkosi.credentials/userdb.user.testuser b/mkosi/mkosi.credentials/userdb.user.testuser
index 6a2ce88324..69a1bfd21d 100644
--- a/mkosi/mkosi.credentials/userdb.user.testuser
+++ b/mkosi/mkosi.credentials/userdb.user.testuser
@@ -3,10 +3,6 @@
"uid": 4711,
"disposition": "regular",
"enforcePasswordPolicy": false,
- "memberOf": [
- "wheel",
- "systemd-journal"
- ],
"shell": "/bin/bash",
"privileged": {
"hashedPassword": ["$1$kqp7NF1f$tNnQcshPX53CSfRKTQD0R1"]
diff --git a/src/nspawn/nspawn-bind-user.c b/src/nspawn/nspawn-bind-user.c
index d9a06e2c03..0211cfe23c 100644
--- a/src/nspawn/nspawn-bind-user.c
+++ b/src/nspawn/nspawn-bind-user.c
@@ -5,12 +5,16 @@
#include "sd-json.h"
#include "alloc-util.h"
+#include "chase.h"
+#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
+#include "io-util.h"
#include "log.h"
#include "nspawn.h"
#include "machine-bind-user.h"
#include "nspawn-bind-user.h"
+#include "strv.h"
#include "user-record.h"
#include "group-record.h"
#include "path-util.h"
@@ -67,6 +71,40 @@ static int write_and_symlink(
return 0;
}
+static int write_membership(const char *root, const char *user, const char *group) {
+ int r;
+
+ assert(user);
+ assert(group);
+
+ _cleanup_free_ char *membership = strjoin(user, ":", group, ".membership");
+ if (!membership)
+ return log_oom();
+
+ _cleanup_free_ char *p = path_join("/run/host/userdb/", membership);
+ if (!p)
+ return log_oom();
+
+ _cleanup_close_ int fd = chase_and_open(
+ p,
+ root,
+ CHASE_PREFIX_ROOT|CHASE_NO_AUTOFS,
+ O_WRONLY|O_CREAT|O_CLOEXEC,
+ /* ret_path= */ NULL);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to create %s: %m", p);
+
+ r = userns_chown_at(fd, /* fname= */ NULL, /* uid= */ 0, /* gid= */ 0, /* flags= */ 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
+
+ r = loop_write(fd, "{}\n", SIZE_MAX);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write empty JSON object into %s: %m", p);
+
+ return 0;
+}
+
int bind_user_setup(const MachineBindUserContext *c, const char *root) {
static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
USER_RECORD_LOAD_MASK_PRIVILEGED|
@@ -130,6 +168,12 @@ int bind_user_setup(const MachineBindUserContext *c, const char *root) {
if (r < 0)
return r;
+ STRV_FOREACH(u, stripped_group->members) {
+ r = write_membership(root, *u, stripped_group->group_name);
+ if (r < 0)
+ return r;
+ }
+
/* Third, write out user shadow data. i.e. extract privileged info from user record */
r = user_record_clone(d->payload_user, shadow_flags, &shadow_user);
if (r < 0)
@@ -161,6 +205,12 @@ int bind_user_setup(const MachineBindUserContext *c, const char *root) {
0);
if (r < 0)
return r;
+
+ STRV_FOREACH(g, stripped_user->member_of) {
+ r = write_membership(root, stripped_user->user_name, *g);
+ if (r < 0)
+ return r;
+ }
}
return 1;
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 88d2346d00..4cd16d9bc0 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -240,6 +240,7 @@ static MachineCredentialContext arg_credentials = {};
static char **arg_bind_user = NULL;
static char *arg_bind_user_shell = NULL;
static bool arg_bind_user_shell_copy = false;
+static char **arg_bind_user_groups = NULL;
static bool arg_suppress_sync = false;
static char *arg_settings_filename = NULL;
static Architecture arg_architecture = _ARCHITECTURE_INVALID;
@@ -283,6 +284,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_cpu_set, cpu_set_done);
STATIC_DESTRUCTOR_REGISTER(arg_sysctl, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_bind_user_groups, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_settings_filename, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
@@ -429,6 +431,8 @@ static int help(void) {
" --bind-user=NAME Bind user from host to container\n"
" --bind-user-shell=BOOL|PATH\n"
" Configure the shell to use for --bind-user= users\n"
+ " --bind-user-group=GROUP\n"
+ " Add an auxiliary group to --bind-user= users\n"
"\n%3$sInput/Output:%4$s\n"
" --console=MODE Select how stdin/stdout/stderr and /dev/console are\n"
" set up for the container.\n"
@@ -660,6 +664,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_LOAD_CREDENTIAL,
ARG_BIND_USER,
ARG_BIND_USER_SHELL,
+ ARG_BIND_USER_GROUP,
ARG_SUPPRESS_SYNC,
ARG_IMAGE_POLICY,
ARG_BACKGROUND,
@@ -738,6 +743,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL },
{ "bind-user", required_argument, NULL, ARG_BIND_USER },
{ "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL },
+ { "bind-user-group", required_argument, NULL, ARG_BIND_USER_GROUP },
{ "suppress-sync", required_argument, NULL, ARG_SUPPRESS_SYNC },
{ "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
{ "background", required_argument, NULL, ARG_BACKGROUND },
@@ -1514,6 +1520,15 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ case ARG_BIND_USER_GROUP:
+ if (!valid_user_group_name(optarg, /* flags= */ 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", optarg);
+
+ if (strv_extend(&arg_bind_user_groups, optarg) < 0)
+ return log_oom();
+
+ break;
+
case ARG_SUPPRESS_SYNC:
r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync);
if (r < 0)
@@ -1689,12 +1704,16 @@ static int verify_arguments(void) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "AmbientCapability= setting is not useful for boot mode.");
}
- /* Drop duplicate --bind-user= entries */
+ /* Drop duplicate --bind-user= and --bind-user-group= entries */
strv_uniq(arg_bind_user);
+ strv_uniq(arg_bind_user_groups);
if (arg_bind_user_shell && strv_isempty(arg_bind_user))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user=");
+ if (!strv_isempty(arg_bind_user_groups) && strv_isempty(arg_bind_user))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-group= without --bind-user=");
+
r = custom_mount_check_all();
if (r < 0)
return r;
@@ -1733,7 +1752,7 @@ static int in_child_chown(void) {
return IN_SET(arg_userns_mode, USER_NAMESPACE_PICK, USER_NAMESPACE_FIXED);
}
-static int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags) {
+int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags) {
assert(fd >= 0 || fd == AT_FDCWD);
if (!in_child_chown())
@@ -1756,6 +1775,9 @@ static int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int
return -EOVERFLOW;
}
+ if (isempty(fname))
+ flags |= AT_EMPTY_PATH;
+
return RET_NERRNO(fchownat(fd, strempty(fname), uid, gid, flags));
}
@@ -4020,6 +4042,7 @@ static int outer_child(
arg_bind_user_shell,
arg_bind_user_shell_copy,
"/run/host/home",
+ arg_bind_user_groups,
&bind_user_context);
if (r < 0)
return r;
diff --git a/src/nspawn/nspawn.h b/src/nspawn/nspawn.h
index 943b22db2d..192110c4d4 100644
--- a/src/nspawn/nspawn.h
+++ b/src/nspawn/nspawn.h
@@ -3,6 +3,7 @@
#include "shared-forward.h"
+int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags);
int userns_lchown(const char *p, uid_t uid, gid_t gid);
int userns_mkdir(const char *root, const char *path, mode_t mode, uid_t uid, gid_t gid);
int make_run_host(const char *root);
diff --git a/src/shared/machine-bind-user.c b/src/shared/machine-bind-user.c
index 476536ea28..c0fd1a96d0 100644
--- a/src/shared/machine-bind-user.c
+++ b/src/shared/machine-bind-user.c
@@ -94,6 +94,7 @@ static int convert_user(
const char *shell,
bool shell_copy,
const char *home_mount_directory,
+ char **groups,
UserRecord **ret_converted_user,
GroupRecord **ret_converted_group) {
@@ -145,6 +146,7 @@ static int convert_user(
SD_JSON_BUILD_PAIR("homeDirectory", SD_JSON_BUILD_STRING(h)),
SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
JSON_BUILD_PAIR_STRING_NON_EMPTY("shell", shell),
+ SD_JSON_BUILD_PAIR_STRV("memberOf", groups),
SD_JSON_BUILD_PAIR("privileged", SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "hashedPassword", SD_JSON_BUILD_VARIANT(hp)),
SD_JSON_BUILD_PAIR_CONDITION(!!ssh, "sshAuthorizedKeys", SD_JSON_BUILD_VARIANT(ssh))))));
@@ -212,6 +214,7 @@ int machine_bind_user_prepare(
const char *bind_user_shell,
bool bind_user_shell_copy,
const char *bind_user_home_mount_directory,
+ char **bind_user_groups,
MachineBindUserContext **ret) {
_cleanup_(machine_bind_user_context_freep) MachineBindUserContext *c = NULL;
@@ -288,6 +291,7 @@ int machine_bind_user_prepare(
bind_user_shell,
bind_user_shell_copy,
bind_user_home_mount_directory,
+ bind_user_groups,
&cu, &cg);
if (r < 0)
return r;
diff --git a/src/shared/machine-bind-user.h b/src/shared/machine-bind-user.h
index c5537d34d2..265fe13727 100644
--- a/src/shared/machine-bind-user.h
+++ b/src/shared/machine-bind-user.h
@@ -28,4 +28,5 @@ int machine_bind_user_prepare(
const char *bind_user_shell,
bool bind_user_shell_copy,
const char *bind_user_home_mount_directory,
+ char **bind_user_groups,
MachineBindUserContext **ret);
diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c
index 8efdc7c133..823b68e7d9 100644
--- a/src/userdb/userdbctl.c
+++ b/src/userdb/userdbctl.c
@@ -18,6 +18,7 @@
#include "format-table.h"
#include "format-util.h"
#include "fs-util.h"
+#include "io-util.h"
#include "log.h"
#include "main-func.h"
#include "mkdir.h"
@@ -1196,6 +1197,31 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
return r;
}
+static int write_membership(int dir_fd, const char *dir, const char *user, const char *group) {
+ int r;
+
+ assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
+ assert(dir);
+ assert(user);
+ assert(group);
+
+ _cleanup_free_ char *membership = strjoin(user, ":", group, ".membership");
+ if (!membership)
+ return log_oom();
+
+ _cleanup_close_ int fd = openat(dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to create %s/%s: %m", dir, membership);
+
+ r = loop_write(fd, "{}\n", SIZE_MAX);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write empty JSON object into %s/%s: %m", dir, membership);
+
+ log_info("Installed %s/%s from credential.", dir, membership);
+
+ return 0;
+}
+
static int load_credential_one(
int credential_dir_fd,
const char *name,
@@ -1430,27 +1456,15 @@ static int load_credential_one(
if (ur)
STRV_FOREACH(g, ur->member_of) {
- _cleanup_free_ char *membership = strjoin(ur->user_name, ":", *g);
- if (!membership)
- return log_oom();
-
- _cleanup_close_ int fd = openat(*userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
- if (fd < 0)
- return log_error_errno(errno, "Failed to create %s: %m", membership);
-
- log_info("Installed %s/%s from credential.", userdb_dir, membership);
+ r = write_membership(*userdb_dir_fd, userdb_dir, ur->user_name, *g);
+ if (r < 0)
+ return r;
}
else
STRV_FOREACH(u, gr->members) {
- _cleanup_free_ char *membership = strjoin(*u, ":", gr->group_name);
- if (!membership)
- return log_oom();
-
- _cleanup_close_ int fd = openat(*userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
- if (fd < 0)
- return log_error_errno(errno, "Failed to create %s: %m", membership);
-
- log_info("Installed %s/%s from credential.", userdb_dir, membership);
+ r = write_membership(*userdb_dir_fd, userdb_dir, *u, gr->group_name);
+ if (r < 0)
+ return r;
}
if (ur && user_record_disposition(ur) == USER_REGULAR) {
diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c
index f3ee2c28e7..14ae20fbe3 100644
--- a/src/vmspawn/vmspawn.c
+++ b/src/vmspawn/vmspawn.c
@@ -143,6 +143,7 @@ static bool arg_notify_ready = true;
static char **arg_bind_user = NULL;
static char *arg_bind_user_shell = NULL;
static bool arg_bind_user_shell_copy = false;
+static char **arg_bind_user_groups = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_directory, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
@@ -164,6 +165,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm_state_path, freep);
STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_bind_user_groups, strv_freep);
static int help(void) {
_cleanup_free_ char *link = NULL;
@@ -227,6 +229,8 @@ static int help(void) {
" --bind-user=NAME Bind user from host to virtual machine\n"
" --bind-user-shell=BOOL|PATH\n"
" Configure the shell to use for --bind-user= users\n"
+ " --bind-user-group=GROUP\n"
+ " Add an auxiliary group to --bind-user= users\n"
"\n%3$sIntegration:%4$s\n"
" --forward-journal=FILE|DIR\n"
" Forward the VM's journal to the host\n"
@@ -303,6 +307,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_NOTIFY_READY,
ARG_BIND_USER,
ARG_BIND_USER_SHELL,
+ ARG_BIND_USER_GROUP,
};
static const struct option options[] = {
@@ -354,6 +359,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "notify-ready", required_argument, NULL, ARG_NOTIFY_READY },
{ "bind-user", required_argument, NULL, ARG_BIND_USER },
{ "bind-user-shell", required_argument, NULL, ARG_BIND_USER_SHELL },
+ { "bind-user-group", required_argument, NULL, ARG_BIND_USER_GROUP },
{}
};
@@ -715,6 +721,15 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ case ARG_BIND_USER_GROUP:
+ if (!valid_user_group_name(optarg, /* flags= */ 0))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind user auxiliary group name: %s", optarg);
+
+ if (strv_extend(&arg_bind_user_groups, optarg) < 0)
+ return log_oom();
+
+ break;
+
case '?':
return -EINVAL;
@@ -722,12 +737,16 @@ static int parse_argv(int argc, char *argv[]) {
assert_not_reached();
}
- /* Drop duplicate --bind-user= entries */
+ /* Drop duplicate --bind-user= and --bind-user-group= entries */
strv_uniq(arg_bind_user);
+ strv_uniq(arg_bind_user_groups);
if (arg_bind_user_shell && strv_isempty(arg_bind_user))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user=");
+ if (!strv_isempty(arg_bind_user_groups) && strv_isempty(arg_bind_user))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-group= without --bind-user=");
+
if (argc > optind) {
arg_kernel_cmdline_extra = strv_copy(argv + optind);
if (!arg_kernel_cmdline_extra)
@@ -1840,6 +1859,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
arg_bind_user_shell,
arg_bind_user_shell_copy,
"/run/vmhost/home",
+ arg_bind_user_groups,
&bind_user_context);
if (r < 0)
return r;
diff --git a/test/units/TEST-87-AUX-UTILS-VM.coredump.sh b/test/units/TEST-87-AUX-UTILS-VM.coredump.sh
index 4f2bbf40c8..19f0fa280e 100755
--- a/test/units/TEST-87-AUX-UTILS-VM.coredump.sh
+++ b/test/units/TEST-87-AUX-UTILS-VM.coredump.sh
@@ -162,7 +162,7 @@ rm -f /tmp/core.{output,redirected}
# Unprivileged stuff
# Related issue: https://github.com/systemd/systemd/issues/26912
-UNPRIV_CMD=(systemd-run --user --wait --pipe -M "testuser@.host" --)
+UNPRIV_CMD=(systemd-run --user --wait --pipe -M "testuser@.host" -E SYSTEMD_PAGER --)
# Trigger a couple of coredumps as an unprivileged user
"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGTRAP"
"${UNPRIV_CMD[@]}" "$MAKE_DUMP_SCRIPT" "$CORE_TEST_UNPRIV_BIN" "SIGABRT"