vmspawn: Add --bind-user= and --bind-user-shell=

We use virtiofsd ID translation to mimick idmapped mounts and the
transient userdb credentials to provision the mapped user in the VM.
This commit is contained in:
DaanDeMeyer
2025-07-14 10:24:53 +02:00
committed by Daan De Meyer
parent ac0a248ee6
commit b1681f981b
4 changed files with 210 additions and 10 deletions

View File

@@ -459,6 +459,73 @@
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind-user=</option></term>
<listitem><para>Binds the home directory of the specified user on the host into the virtual
machine. Takes the name of an existing user on the host as argument. May be used multiple times to
bind multiple users into the virtual machine. This does two things:</para>
<orderedlist>
<listitem><para>The user's home directory is made available from the host into
<filename>/run/vmhost/home/</filename> using virtiofs. virtiofsd id translation to map the host
user's UID/GID to its assigned UID/GID in the virtual machine.</para></listitem>
<listitem><para>JSON user and group records are generated in that describes the mapped user which
are passed into the virtual machine using <literal>userdb.transient.*</literal> credentials.
They contain a minimized representation of the host's user record, adjusted to the UID/GID and
home directory path assigned to the user in the virtual machine. The
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>
glibc NSS module will pick up these records from there and make them available in the virtual
machine's user/group databases.</para></listitem>
</orderedlist>
<para>The combination of the two operations above ensures that it is possible to log into the
virtual machine using the same account information as on the host. The user is only mapped
transiently, while the virtual machine is running, and the mapping itself does not result in
persistent changes to the virtual machine (except maybe for log messages generated at login time,
and similar). Note that in particular the UID/GID assignment in the virtual machine is not made
persistently. If the user is mapped transiently, it is best to not allow the user to make
persistent changes to the virtual machine. If the user leaves files or directories owned by the
user, and those UIDs/GIDs are reused during later virtual machine invocations (possibly with a
different <option>--bind-user=</option> mapping), those files and directories will be accessible to
the "new" user.</para>
<para>The user/group record mapping only works if the virtual machine contains systemd 258 or
newer, with <command>nss-systemd</command> properly configured in
<filename>nsswitch.conf</filename>. See
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry> for
details.</para>
<para>Note that the user record propagated from the host into the virtual machine will contain the
UNIX password hash of the user, so that seamless logins in the virtual machine are possible. If the
virtual machine is less trusted than the host it is hence important to use a strong UNIX password
hash function (e.g. yescrypt or similar, with the <literal>$y$</literal> hash prefix).</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--bind-user-shell=</option></term>
<listitem><para>When used with <option>--bind-user=</option>, includes the specified shell in the
user records of users bound into the virtual machine. Takes either a boolean or an absolute path.</para>
<itemizedlist>
<listitem><para>If false (the default), no shell is passed in the user records for users bound into
the virtual machine. This causes bound users to the use the virtual machine's default shell.</para></listitem>
<listitem><para>If true, the shells specified by the host user records are included in the user records of all users bound into the virtual machine.</para></listitem>
<listitem><para>If passed an absolute path, sets that path as the shell for user records of all users bound into the virtual machine.</para></listitem>
</itemizedlist>
<para>Note: This will not check whether the specified shells 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>

View File

@@ -7,7 +7,7 @@
#include "string-util.h"
#include "vmspawn-mount.h"
static void runtime_mount_done(RuntimeMount *mount) {
void runtime_mount_done(RuntimeMount *mount) {
assert(mount);
mount->source = mfree(mount->source);
@@ -24,7 +24,11 @@ void runtime_mount_context_done(RuntimeMountContext *ctx) {
}
int runtime_mount_parse(RuntimeMountContext *ctx, const char *s, bool read_only) {
_cleanup_(runtime_mount_done) RuntimeMount mount = { .read_only = read_only };
_cleanup_(runtime_mount_done) RuntimeMount mount = {
.read_only = read_only,
.source_uid = UID_INVALID,
.target_uid = UID_INVALID,
};
_cleanup_free_ char *source_rel = NULL;
int r;

View File

@@ -6,7 +6,9 @@
typedef struct RuntimeMount {
bool read_only;
char *source;
uid_t source_uid;
char *target;
uid_t target_uid;
} RuntimeMount;
typedef struct RuntimeMountContext {
@@ -14,5 +16,6 @@ typedef struct RuntimeMountContext {
size_t n_mounts;
} RuntimeMountContext;
void runtime_mount_done(RuntimeMount *mount);
void runtime_mount_context_done(RuntimeMountContext *ctx);
int runtime_mount_parse(RuntimeMountContext *ctx, const char *s, bool read_only);

View File

@@ -35,11 +35,13 @@
#include "format-util.h"
#include "fs-util.h"
#include "gpt.h"
#include "group-record.h"
#include "hexdecoct.h"
#include "hostname-setup.h"
#include "hostname-util.h"
#include "id128-util.h"
#include "log.h"
#include "machine-bind-user.h"
#include "machine-credential.h"
#include "main-func.h"
#include "mkdir.h"
@@ -68,6 +70,8 @@
#include "terminal-util.h"
#include "tmpfile-util.h"
#include "unit-name.h"
#include "user-record.h"
#include "user-util.h"
#include "utf8.h"
#include "vmspawn-mount.h"
#include "vmspawn-register.h"
@@ -136,6 +140,9 @@ static char *arg_tpm_state_path = NULL;
static TpmStateMode arg_tpm_state_mode = TPM_STATE_AUTO;
static bool arg_ask_password = true;
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_DESTRUCTOR_REGISTER(arg_directory, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image, freep);
@@ -155,6 +162,8 @@ STATIC_DESTRUCTOR_REGISTER(arg_ssh_key_type, freep);
STATIC_DESTRUCTOR_REGISTER(arg_smbios11, strv_freep);
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 int help(void) {
_cleanup_free_ char *link = NULL;
@@ -215,6 +224,9 @@ static int help(void) {
" --bind-ro=SOURCE[:TARGET]\n"
" Mount a file or directory, but read-only\n"
" --extra-drive=PATH Adds an additional disk to the virtual machine\n"
" --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"
"\n%3$sIntegration:%4$s\n"
" --forward-journal=FILE|DIR\n"
" Forward the VM's journal to the host\n"
@@ -289,6 +301,8 @@ static int parse_argv(int argc, char *argv[]) {
ARG_NO_ASK_PASSWORD,
ARG_PROPERTY,
ARG_NOTIFY_READY,
ARG_BIND_USER,
ARG_BIND_USER_SHELL,
};
static const struct option options[] = {
@@ -338,6 +352,8 @@ static int parse_argv(int argc, char *argv[]) {
{ "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
{ "property", required_argument, NULL, ARG_PROPERTY },
{ "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 },
{}
};
@@ -675,6 +691,30 @@ static int parse_argv(int argc, char *argv[]) {
break;
case ARG_BIND_USER:
if (!valid_user_group_name(optarg, /* flags= */ 0))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name to bind: %s", optarg);
if (strv_extend(&arg_bind_user, optarg) < 0)
return log_oom();
break;
case ARG_BIND_USER_SHELL: {
bool copy = false;
char *sh = NULL;
r = parse_user_shell(optarg, &sh, &copy);
if (r == -ENOMEM)
return log_oom();
if (r < 0)
return log_error_errno(r, "Invalid user shell to bind: %s", optarg);
free_and_replace(arg_bind_user_shell, sh);
arg_bind_user_shell_copy = copy;
break;
}
case '?':
return -EINVAL;
@@ -682,6 +722,12 @@ static int parse_argv(int argc, char *argv[]) {
assert_not_reached();
}
/* Drop duplicate --bind-user= entries */
strv_uniq(arg_bind_user);
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 (argc > optind) {
arg_kernel_cmdline_extra = strv_copy(argv + optind);
if (!arg_kernel_cmdline_extra)
@@ -1359,7 +1405,9 @@ static int find_virtiofsd(char **ret) {
static int start_virtiofsd(
const char *scope,
const char *directory,
bool uidmap,
uid_t source_uid,
uid_t target_uid,
uid_t uid_range,
const char *runtime_dir,
const char *sd_socket_activate,
char **ret_listen_address,
@@ -1397,20 +1445,20 @@ static int start_virtiofsd(
if (!argv)
return log_oom();
if (uidmap && arg_uid_shift != UID_INVALID) {
r = strv_extend(&argv, "--uid-map");
if (source_uid != UID_INVALID && target_uid != UID_INVALID && uid_range != UID_INVALID) {
r = strv_extend(&argv, "--translate-uid");
if (r < 0)
return log_oom();
r = strv_extendf(&argv, ":0:" UID_FMT ":" UID_FMT ":", arg_uid_shift, arg_uid_range);
r = strv_extendf(&argv, "map:" UID_FMT ":" UID_FMT ":" UID_FMT, target_uid, source_uid, uid_range);
if (r < 0)
return log_oom();
r = strv_extend(&argv, "--gid-map");
r = strv_extend(&argv, "--translate-gid");
if (r < 0)
return log_oom();
r = strv_extendf(&argv, ":0:" GID_FMT ":" GID_FMT ":", arg_uid_shift, arg_uid_range);
r = strv_extendf(&argv, "map:" GID_FMT ":" GID_FMT ":" GID_FMT, target_uid, source_uid, uid_range);
if (r < 0)
return log_oom();
}
@@ -1425,6 +1473,65 @@ static int start_virtiofsd(
return 0;
}
static int bind_user_setup(
const MachineBindUserContext *context,
MachineCredentialContext *credentials,
RuntimeMountContext *mounts) {
int r;
assert(credentials);
assert(mounts);
if (!context)
return 0;
FOREACH_ARRAY(bind_user, context->data, context->n_data) {
_cleanup_free_ char *formatted = NULL;
r = sd_json_variant_format(bind_user->payload_user->json, SD_JSON_FORMAT_NEWLINE, &formatted);
if (r < 0)
return log_error_errno(r, "Failed to format JSON user record: %m");
_cleanup_free_ char *cred = strjoin("userdb.transient.user.", bind_user->payload_user->user_name);
if (!cred)
return log_oom();
r = machine_credential_add(credentials, cred, formatted, SIZE_MAX);
if (r < 0)
return r;
formatted = mfree(formatted);
r = sd_json_variant_format(bind_user->payload_group->json, SD_JSON_FORMAT_NEWLINE, &formatted);
if (r < 0)
return log_error_errno(r, "Failed to format JSON group record: %m");
free(cred);
cred = strjoin("userdb.transient.group.", bind_user->payload_group->group_name);
if (!cred)
return log_oom();
r = machine_credential_add(credentials, cred, formatted, SIZE_MAX);
if (r < 0)
return r;
_cleanup_(runtime_mount_done) RuntimeMount mount = {
.source = strdup(user_record_home_directory(bind_user->host_user)),
.source_uid = bind_user->host_user->uid,
.target = strdup(user_record_home_directory(bind_user->payload_user)),
.target_uid = bind_user->payload_user->uid,
};
if (!mount.source || !mount.target)
return log_oom();
if (!GREEDY_REALLOC(mounts->mounts, mounts->n_mounts + 1))
return log_oom();
mounts->mounts[mounts->n_mounts++] = TAKE_STRUCT(mount);
}
return 0;
}
static int kernel_cmdline_maybe_append_root(void) {
int r;
bool cmdline_contains_root = strv_find_startswith(arg_kernel_cmdline_extra, "root=")
@@ -1726,6 +1833,21 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
if (r < 0)
return log_error_errno(r, "Failed to find OVMF config: %m");
_cleanup_(machine_bind_user_context_freep) MachineBindUserContext *bind_user_context = NULL;
r = machine_bind_user_prepare(
/* directory= */ NULL,
arg_bind_user,
arg_bind_user_shell,
arg_bind_user_shell_copy,
"/run/vmhost/home",
&bind_user_context);
if (r < 0)
return r;
r = bind_user_setup(bind_user_context, &arg_credentials, &arg_runtime_mounts);
if (r < 0)
return r;
/* only warn if the user hasn't disabled secureboot */
if (!ovmf_config->supports_sb && arg_secure_boot)
log_warning("Couldn't find OVMF firmware blob with Secure Boot support, "
@@ -2177,7 +2299,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
r = start_virtiofsd(
unit,
arg_directory,
/* uidmap= */ true,
/* source_uid= */ arg_uid_shift,
/* target_uid= */ 0,
/* uid_range= */ arg_uid_range,
runtime_dir,
sd_socket_activate,
&listen_address,
@@ -2267,7 +2391,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
r = start_virtiofsd(
unit,
mount->source,
/* uidmap= */ false,
/* source_uid= */ mount->source_uid,
/* target_uid= */ mount->target_uid,
/* uid_range= */ 1U,
runtime_dir,
sd_socket_activate,
&listen_address,