From b1681f981b51bd684ec170e46bf0f94b9b550c17 Mon Sep 17 00:00:00 2001 From: DaanDeMeyer Date: Mon, 14 Jul 2025 10:24:53 +0200 Subject: [PATCH] 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. --- man/systemd-vmspawn.xml | 67 +++++++++++++++++ src/vmspawn/vmspawn-mount.c | 8 +- src/vmspawn/vmspawn-mount.h | 3 + src/vmspawn/vmspawn.c | 142 ++++++++++++++++++++++++++++++++++-- 4 files changed, 210 insertions(+), 10 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index e52903ae651..6d677cf1069 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -459,6 +459,73 @@ + + + + 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: + + + The user's home directory is made available from the host into + /run/vmhost/home/ using virtiofs. virtiofsd id translation to map the host + user's UID/GID to its assigned UID/GID in the virtual machine. + + JSON user and group records are generated in that describes the mapped user which + are passed into the virtual machine using userdb.transient.* 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 + nss-systemd8 + glibc NSS module will pick up these records from there and make them available in the virtual + machine's user/group databases. + + + 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 mapping), those files and directories will be accessible to + the "new" user. + + The user/group record mapping only works if the virtual machine contains systemd 258 or + newer, with nss-systemd properly configured in + nsswitch.conf. See + nss-systemd8 for + details. + + 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 $y$ hash prefix). + + + + + + + + When used with , includes the specified shell in the + user records of users bound into the virtual machine. Takes either a boolean or an absolute path. + + + 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. + + If true, the shells specified by the host user records are included in the user records of all users bound into the virtual machine. + + If passed an absolute path, sets that path as the shell for user records of all users bound into the virtual machine. + + + Note: This will not check whether the specified shells exist in the virtual machine. + + This operation is only supported in combination with . + + + diff --git a/src/vmspawn/vmspawn-mount.c b/src/vmspawn/vmspawn-mount.c index 9f03a216554..feff5c8f4a8 100644 --- a/src/vmspawn/vmspawn-mount.c +++ b/src/vmspawn/vmspawn-mount.c @@ -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; diff --git a/src/vmspawn/vmspawn-mount.h b/src/vmspawn/vmspawn-mount.h index 9221519018e..4647affb997 100644 --- a/src/vmspawn/vmspawn-mount.h +++ b/src/vmspawn/vmspawn-mount.h @@ -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); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 735d733f245..3a2cd919b6a 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -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, ©); + 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, -- 2.47.3