diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml
index 794d6e8f29..b05744dc08 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 4870d6b7ab..1db41a1b72 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 da927347c6..e7d976d199 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 b6d4e1e4ad..cb4d246bec 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 b735093691..7a8a8d8b3d 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 035796994c..f16a3b61b3 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 607db87feb..56a65d5a21 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 92e2c5ec20..27d26f9809 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 ffe5e8507d..a0fbb3637d 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