From: Nick Labich Date: Fri, 27 Jun 2025 15:39:46 +0000 (-0400) Subject: nspawn: Add --bind-user-shell= to control shells for --bind-user X-Git-Tag: v258-rc1~218 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a9e860f22eee540a0a6819034e110572c9c8b9fd;p=thirdparty%2Fsystemd.git nspawn: Add --bind-user-shell= to control shells for --bind-user Prior to this change, no user shell can be specified in the user records passed into a container via --bind-user=. This new option allows users to: 1. When false (the default), continue to specify no user shell for each bound user record, resulting in the use of the container's default shell for bound users. 2. When true, include each host user's shell in the corresponding user record passed into a container (via --bind-user=). 3. When an absolute path, set that path as the user shell for each user record passed into a container (via --bind-user=). This does not change the existing behavior, but allows users to opt-in to either copy the shells specified by the host user records or override the shell explicitly by path. --- diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index 794d6e8f29a..b05744dc08b 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -1648,6 +1648,28 @@ After=sys-subsystem-net-devices-ens1.device + + + + When used with , includes the specified shell in the + user records of users bound into the container. 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 container. This causes bound users to the use the container'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 container. + + If passed an absolute path, sets that path as the shell for user records of all users bound into the container. + + + Note: This will not check whether the specified shells exist in the container. + + This operation is only supported in combination with . + + + + diff --git a/man/systemd.nspawn.xml b/man/systemd.nspawn.xml index 4870d6b7abf..1db41a1b72c 100644 --- a/man/systemd.nspawn.xml +++ b/man/systemd.nspawn.xml @@ -495,6 +495,18 @@ + + BindUserShell= + + When used with BindUser, specifies the shell that is included in + the user record of users bound from the host into the container. This option is equivalent to the + command line switch , see + systemd-nspawn1 + for details about the specific options supported. This setting is privileged (see above). + + + + TemporaryFileSystem= diff --git a/src/nspawn/nspawn-bind-user.c b/src/nspawn/nspawn-bind-user.c index da927347c6a..e7d976d1991 100644 --- a/src/nspawn/nspawn-bind-user.c +++ b/src/nspawn/nspawn-bind-user.c @@ -91,6 +91,8 @@ static int convert_user( UserRecord *u, GroupRecord *g, uid_t allocate_uid, + const char *shell, + bool shell_copy, UserRecord **ret_converted_user, GroupRecord **ret_converted_group) { @@ -104,6 +106,9 @@ static int convert_user( assert(g); assert(user_record_gid(u) == g->gid); + if (shell_copy) + shell = u->shell; + r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID); if (r < 0) return r; @@ -138,6 +143,7 @@ static int convert_user( SD_JSON_BUILD_PAIR_CONDITION(u->disposition >= 0, "disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(u->disposition))), 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("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)))))); @@ -203,6 +209,8 @@ BindUserContext* bind_user_context_free(BindUserContext *c) { int bind_user_prepare( const char *directory, char **bind_user, + const char *bind_user_shell, + bool bind_user_shell_copy, uid_t uid_shift, uid_t uid_range, CustomMount **custom_mounts, @@ -285,7 +293,7 @@ int bind_user_prepare( if (r < 0) return r; - r = convert_user(directory, u, g, current_uid, &cu, &cg); + r = convert_user(directory, u, g, current_uid, bind_user_shell, bind_user_shell_copy, &cu, &cg); if (r < 0) return r; diff --git a/src/nspawn/nspawn-bind-user.h b/src/nspawn/nspawn-bind-user.h index b6d4e1e4ad0..cb4d246bece 100644 --- a/src/nspawn/nspawn-bind-user.h +++ b/src/nspawn/nspawn-bind-user.h @@ -24,6 +24,6 @@ BindUserContext* bind_user_context_free(BindUserContext *c); DEFINE_TRIVIAL_CLEANUP_FUNC(BindUserContext*, bind_user_context_free); -int bind_user_prepare(const char *directory, char **bind_user, uid_t uid_shift, uid_t uid_range, CustomMount **custom_mounts, size_t *n_custom_mounts, BindUserContext **ret); +int bind_user_prepare(const char *directory, char **bind_user, const char *bind_user_shell, bool bind_user_shell_copy, uid_t uid_shift, uid_t uid_range, CustomMount **custom_mounts, size_t *n_custom_mounts, BindUserContext **ret); int bind_user_setup(const BindUserContext *c, const char *root); diff --git a/src/nspawn/nspawn-gperf.gperf b/src/nspawn/nspawn-gperf.gperf index b735093691c..7a8a8d8b3d0 100644 --- a/src/nspawn/nspawn-gperf.gperf +++ b/src/nspawn/nspawn-gperf.gperf @@ -71,6 +71,7 @@ Files.OverlayReadOnly, config_parse_overlay, 1, Files.PrivateUsersChown, config_parse_userns_chown, 0, offsetof(Settings, userns_ownership) Files.PrivateUsersOwnership, config_parse_userns_ownership, 0, offsetof(Settings, userns_ownership) Files.BindUser, config_parse_bind_user, 0, offsetof(Settings, bind_user) +Files.BindUserShell, config_parse_bind_user_shell, 0, 0 Network.Private, config_parse_tristate, 0, offsetof(Settings, private_network) Network.Interface, config_parse_network_iface_pair, 0, offsetof(Settings, network_interfaces) Network.MACVLAN, config_parse_macvlan_iface_pair, 0, offsetof(Settings, network_macvlan) diff --git a/src/nspawn/nspawn-settings.c b/src/nspawn/nspawn-settings.c index 035796994c2..f16a3b61b32 100644 --- a/src/nspawn/nspawn-settings.c +++ b/src/nspawn/nspawn-settings.c @@ -13,6 +13,7 @@ #include "nspawn-network.h" #include "nspawn-settings.h" #include "parse-util.h" +#include "path-util.h" #include "process-util.h" #include "rlimit-util.h" #include "socket-util.h" @@ -138,6 +139,7 @@ Settings* settings_free(Settings *s) { free(s->hostname); cpu_set_done(&s->cpu_set); strv_free(s->bind_user); + free(s->bind_user_shell); strv_free(s->network_interfaces); strv_free(s->network_macvlan); @@ -1000,3 +1002,68 @@ int config_parse_bind_user( return 0; } + +int parse_bind_user_shell(const char *s, char **ret_sh, bool *ret_copy) { + char *sh; + int r; + + if (path_is_absolute(s) && path_is_normalized(s)) { + sh = strdup(s); + if (!sh) + return -ENOMEM; + + *ret_sh = sh; + *ret_copy = false; + } else { + r = parse_boolean(s); + if (r < 0) + return r; + + *ret_sh = NULL; + *ret_copy = r; + } + + return 0; +} + +int config_parse_bind_user_shell( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Settings *settings = ASSERT_PTR(data); + char *sh = NULL; + bool copy = false; + int r; + + assert(rvalue); + + if (isempty(rvalue)) { + settings->bind_user_shell = mfree(settings->bind_user_shell); + settings->bind_user_shell_copy = false; + settings->bind_user_shell_set = false; + + return 0; + } + + r = parse_bind_user_shell(rvalue, &sh, ©); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse BindUserShell= value, ignoring: %s", rvalue); + return 0; + } + + free_and_replace(settings->bind_user_shell, sh); + settings->bind_user_shell_copy = copy; + settings->bind_user_shell_set = true; + + return 0; +} diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 607db87feb3..56a65d5a21a 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -123,10 +123,11 @@ typedef enum SettingsMask { SETTING_CONSOLE_MODE = UINT64_C(1) << 29, SETTING_CREDENTIALS = UINT64_C(1) << 30, SETTING_BIND_USER = UINT64_C(1) << 31, - SETTING_SUPPRESS_SYNC = UINT64_C(1) << 32, - SETTING_RLIMIT_FIRST = UINT64_C(1) << 33, /* we define one bit per resource limit here */ - SETTING_RLIMIT_LAST = UINT64_C(1) << (33 + _RLIMIT_MAX - 1), - _SETTINGS_MASK_ALL = (UINT64_C(1) << (33 + _RLIMIT_MAX)) -1, + SETTING_BIND_USER_SHELL = UINT64_C(1) << 32, + SETTING_SUPPRESS_SYNC = UINT64_C(1) << 33, + SETTING_RLIMIT_FIRST = UINT64_C(1) << 34, /* we define one bit per resource limit here */ + SETTING_RLIMIT_LAST = UINT64_C(1) << (34 + _RLIMIT_MAX - 1), + _SETTINGS_MASK_ALL = (UINT64_C(1) << (34 + _RLIMIT_MAX)) -1, _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; @@ -195,6 +196,9 @@ typedef struct Settings { size_t n_custom_mounts; UserNamespaceOwnership userns_ownership; char **bind_user; + char *bind_user_shell; + bool bind_user_shell_copy; + bool bind_user_shell_set; /* [Network] */ int private_network; @@ -270,6 +274,9 @@ CONFIG_PARSER_PROTOTYPE(config_parse_timezone_mode); CONFIG_PARSER_PROTOTYPE(config_parse_userns_chown); CONFIG_PARSER_PROTOTYPE(config_parse_userns_ownership); CONFIG_PARSER_PROTOTYPE(config_parse_bind_user); +CONFIG_PARSER_PROTOTYPE(config_parse_bind_user_shell); + +int parse_bind_user_shell(const char *s, char **ret_sh, bool *ret_copy); const char* resolv_conf_mode_to_string(ResolvConfMode a) _const_; ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 92e2c5ec20c..27d26f98095 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -240,6 +240,8 @@ static char **arg_sysctl = NULL; static ConsoleMode arg_console_mode = _CONSOLE_MODE_INVALID; 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 bool arg_suppress_sync = false; static char *arg_settings_filename = NULL; static Architecture arg_architecture = _ARCHITECTURE_INVALID; @@ -282,6 +284,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done); 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_settings_filename, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_background, freep); @@ -692,6 +695,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_SET_CREDENTIAL, ARG_LOAD_CREDENTIAL, ARG_BIND_USER, + ARG_BIND_USER_SHELL, ARG_SUPPRESS_SYNC, ARG_IMAGE_POLICY, ARG_BACKGROUND, @@ -769,6 +773,7 @@ static int parse_argv(int argc, char *argv[]) { { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, { "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 }, { "suppress-sync", required_argument, NULL, ARG_SUPPRESS_SYNC }, { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, { "background", required_argument, NULL, ARG_BACKGROUND }, @@ -1536,6 +1541,22 @@ static int parse_argv(int argc, char *argv[]) { arg_settings_mask |= SETTING_BIND_USER; break; + case ARG_BIND_USER_SHELL: { + bool copy = false; + char *sh = NULL; + r = parse_bind_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; + + arg_settings_mask |= SETTING_BIND_USER_SHELL; + break; + } + case ARG_SUPPRESS_SYNC: r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync); if (r < 0) @@ -1722,6 +1743,9 @@ static int verify_arguments(void) { /* 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="); + r = custom_mount_check_all(); if (r < 0) return r; @@ -4022,6 +4046,8 @@ static int outer_child( r = bind_user_prepare( directory, arg_bind_user, + arg_bind_user_shell, + arg_bind_user_shell_copy, chown_uid, chown_range, &arg_custom_mounts, &arg_n_custom_mounts, @@ -4841,6 +4867,12 @@ static int merge_settings(Settings *settings, const char *path) { !strv_isempty(settings->bind_user)) strv_free_and_replace(arg_bind_user, settings->bind_user); + if (!FLAGS_SET(arg_settings_mask, SETTING_BIND_USER_SHELL) && + settings->bind_user_shell_set) { + free_and_replace(arg_bind_user_shell, settings->bind_user_shell); + arg_bind_user_shell_copy = settings->bind_user_shell_copy; + } + if ((arg_settings_mask & SETTING_NOTIFY_READY) == 0 && settings->notify_ready >= 0) arg_notify_ready = settings->notify_ready; diff --git a/test/units/TEST-13-NSPAWN.nspawn.sh b/test/units/TEST-13-NSPAWN.nspawn.sh index ffe5e8507dd..a0fbb3637d6 100755 --- a/test/units/TEST-13-NSPAWN.nspawn.sh +++ b/test/units/TEST-13-NSPAWN.nspawn.sh @@ -580,6 +580,151 @@ testcase_bind_user() { rm -fr "$root" } +testcase_bind_user_shell() { + local root + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.bind-user.XXX)" + create_dummy_container "$root" + useradd --create-home --user-group --shell=/usr/bin/bash nspawn-bind-user-1 + useradd --create-home --user-group --shell=/usr/bin/sh nspawn-bind-user-2 + trap bind_user_cleanup RETURN + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-1.user' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user-shell=no \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-1.user' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user-shell=yes \ + bash -xec 'grep -q "\"shell\":\"/usr/bin/bash\"" /run/host/userdb/nspawn-bind-user-1.user' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user-shell=/bin/bash \ + bash -xec 'grep -q "\"shell\":\"/bin/bash\"" /run/host/userdb/nspawn-bind-user-1.user' + + (! systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user-shell=bad-argument \ + bash -xec 'true') + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-1.user && grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + --bind-user-shell=yes \ + bash -xec 'grep -q "\"shell\":\"/usr/bin/bash\"" /run/host/userdb/nspawn-bind-user-1.user && grep -q "\"shell\":\"/usr/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user' + + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-1 \ + --bind-user=nspawn-bind-user-2 \ + --bind-user-shell=/bin/sh \ + bash -xec 'grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-1.user && grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user' + + mach=$(basename "$root") + mkdir -p /run/systemd/nspawn + conf=/run/systemd/nspawn/"$mach".nspawn + + cat <<'EOF' >"$conf" +# [Files] +# BindUserShell=no by default +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +[Files] +BindUserShell=no +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +[Files] +BindUserShell=yes +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'grep -q "\"shell\":\"/usr/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +[Files] +BindUserShell=/bin/sh +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --bind-user=nspawn-bind-user-2 \ + bash -xec 'grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +# [Files] +# BindUserShell=no default doesn't override +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --settings=override \ + --bind-user=nspawn-bind-user-2 \ + --bind-user-shell=yes \ + bash -xec 'grep -q "\"shell\":\"/usr/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +[Files] +BindUserShell=no +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --settings=override \ + --bind-user=nspawn-bind-user-2 \ + --bind-user-shell=yes \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +[Files] +BindUserShell=no +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --settings=override \ + --bind-user=nspawn-bind-user-2 \ + --bind-user-shell=/foo \ + bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user' + + cat <<'EOF' >"$conf" +[Files] +BindUserShell=/bin/sh +EOF + systemd-nspawn --directory="$root" \ + --private-users=pick \ + --settings=override \ + --bind-user=nspawn-bind-user-2 \ + --bind-user-shell=yes \ + bash -xec 'grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user' + + rm -fr "$root" +} + testcase_bind_tmp_path() { # https://github.com/systemd/systemd/issues/4789 local root