Various --bind-user= fixes (#39498)

This commit is contained in:
Yu Watanabe
2025-10-31 20:43:54 +09:00
committed by GitHub
11 changed files with 164 additions and 28 deletions
+15 -2
View File
@@ -1601,7 +1601,7 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
<listitem><para>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:</para>
users into the container. This does two things:</para>
<orderedlist>
<listitem><para>The user's home directory is bind mounted from the host into
@@ -1616,7 +1616,7 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
user/group databases.</para></listitem>
</orderedlist>
<para>The combination of the three operations above ensures that it is possible to log into the
<para>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</programlisting>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind-user-group=<replaceable>NAME</replaceable></option></term>
<listitem><para>When used with <option>--bind-user=</option>, includes the specified group as an
auxiliary group in the user records of users bound into the container. Takes a group name.</para>
<para>Note: This will not check whether the specified groups exist in the container.</para>
<para>This operation is only supported in combination with <option>--bind-user=</option>.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--inaccessible=</option></term>
+14
View File
@@ -526,6 +526,20 @@
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind-user-group=<replaceable>NAME</replaceable></option></term>
<listitem><para>When used with <option>--bind-user=</option>, includes the specified group as an
auxiliary group in the user records of users bound into the virtual machine. Takes a group name.</para>
<para>Note: This will not check whether the specified groups exist in the virtual machine.</para>
<para>This operation is only supported in combination with <option>--bind-user=</option>.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
</variablelist>
</refsect2>
@@ -3,10 +3,6 @@
"uid": 4711,
"disposition": "regular",
"enforcePasswordPolicy": false,
"memberOf": [
"wheel",
"systemd-journal"
],
"shell": "/bin/bash",
"privileged": {
"hashedPassword": ["$1$kqp7NF1f$tNnQcshPX53CSfRKTQD0R1"]
+50
View File
@@ -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;
+25 -2
View File
@@ -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;
+1
View File
@@ -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);
+4
View File
@@ -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;
+1
View File
@@ -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);
+32 -18
View File
@@ -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) {
+21 -1
View File
@@ -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;
+1 -1
View File
@@ -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"