<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>
#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"
#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"
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);
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;
" --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"
ARG_NO_ASK_PASSWORD,
ARG_PROPERTY,
ARG_NOTIFY_READY,
+ ARG_BIND_USER,
+ ARG_BIND_USER_SHELL,
};
static const struct option options[] = {
{ "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 },
{}
};
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;
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)
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,
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();
}
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=")
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, "
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,
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,