]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: Add --bind-user= and --bind-user-shell= 38410/head
authorDaanDeMeyer <daan.j.demeyer@gmail.com>
Mon, 14 Jul 2025 08:24:53 +0000 (10:24 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 22 Oct 2025 14:42:50 +0000 (16:42 +0200)
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
src/vmspawn/vmspawn-mount.c
src/vmspawn/vmspawn-mount.h
src/vmspawn/vmspawn.c

index e52903ae651e8475a256f211b757fa47d40efbbd..6d677cf10695f60046bbe1f588642e09ba1baa9e 100644 (file)
           <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>
 
index 9f03a216554af439921dea67ce05aa17a0db6273..feff5c8f4a8363d0333489253fbfd6966c8a14a4 100644 (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;
 
index 9221519018e73834fa0d73de791152c80a11b256..4647affb9975fc4ec1d378fe6bea81fe348bf2aa 100644 (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);
index 735d733f2459996397bf5d6b0ed1cd79f4537bbb..3a2cd919b6a14fc9972de5bd8481df8ff6f00d08 100644 (file)
 #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,