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.
This commit is contained in:
Nick Labich
2025-06-27 11:39:46 -04:00
committed by Lennart Poettering
parent 381304a209
commit a9e860f22e
9 changed files with 300 additions and 6 deletions

View File

@@ -1648,6 +1648,28 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
<xi:include href="version-info.xml" xpointer="v249"/></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 container. 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 container. This causes bound users to the use the container'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 container.</para></listitem>
<listitem><para>If passed an absolute path, sets that path as the shell for user records of all users bound into the container.</para></listitem>
</itemizedlist>
<para>Note: This will not check whether the specified shells exist in the container.</para>
<para>This operation is only supported in combination with <option>--bind-user=</option>.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--inaccessible=</option></term>

View File

@@ -495,6 +495,18 @@
<xi:include href="version-info.xml" xpointer="v249"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>BindUserShell=</varname></term>
<listitem><para>When used with <varname>BindUser</varname>, 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 <option>--bind-user-shell=</option>, see
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>
for details about the specific options supported. This setting is privileged (see above).</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>TemporaryFileSystem=</varname></term>

View File

@@ -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;

View File

@@ -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);

View File

@@ -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)

View File

@@ -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, &copy);
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;
}

View File

@@ -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_;

View File

@@ -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, &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;
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;

View File

@@ -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