diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml
index c75afe7cae4..0ce2293b640 100644
--- a/man/systemd-nspawn.xml
+++ b/man/systemd-nspawn.xml
@@ -1973,10 +1973,16 @@ After=sys-subsystem-net-devices-ens1.device
Note: when systemd-nspawn runs as systemd system service it can propagate
the credentials it received via LoadCredential=/SetCredential=
- to the container payload. A systemd service manager running as PID 1 in the container can further
- propagate them to the services it itself starts. It is thus possible to easily propagate credentials
- from a parent service manager to a container manager service and from there into its payload. This
- can even be done recursively.
+ to the container payload, if they are passed on with
+ ,
+ i.e. if PATH is the name of a credential rather than a path. A systemd
+ service manager running as PID 1 in the container can further propagate them to the services it
+ itself starts. It is thus possible to easily propagate credentials from a parent service manager to a
+ container manager service and from there into its payload. This can even be done recursively. For
+ systemd-nspawn@.service, credentials may also be passed with the
+ SetCredential= and LoadCredential= settings in
+ systemd.nspawn5
+ files.
In order to embed binary data into the credential data for ,
use C-style escaping (i.e. \n to embed a newline, or \x00 to
diff --git a/man/systemd.nspawn.xml b/man/systemd.nspawn.xml
index 97e854d23a5..202afe7ef4c 100644
--- a/man/systemd.nspawn.xml
+++ b/man/systemd.nspawn.xml
@@ -352,6 +352,43 @@
+
+ SetCredential=
+ LoadCredential=
+
+ Pass a credential to the container. These settings are equivalent to the
+ and command line switches,
+ and take ID:VALUE and
+ ID:PATH respectively, where
+ PATH is either an absolute path or the name of a credential passed to
+ systemd-nspawn itself. Each may be specified more than once to pass multiple
+ credentials. If the same credential ID is assigned more than once by the same setting, the last
+ assignment takes effect. If the empty string is assigned, the list of credentials set by the
+ respective setting so far is reset. Credentials from the settings file and from the command line are
+ not combined: if any credential is passed on the command line, these settings are ignored, unless
+ is used, in which case the credentials from the settings file
+ replace those from the command line. See
+ systemd-nspawn1 for
+ details.
+
+ As with the settings of the same names in unit files (see
+ systemd.exec5),
+ if the same credential ID is specified with both settings, LoadCredential= takes
+ priority, and SetCredential= acts as a fallback if the file does not exist. A
+ credential that is to be inherited from systemd-nspawn itself, i.e. if
+ PATH is a credential name, is skipped if it was not passed. In both of
+ these cases, a missing credential is not considered fatal.
+
+ LoadCredential= is privileged (see above), as it reads files on the host.
+ For example, the credentials of systemd-nspawn@.service may be propagated to
+ the container with LoadCredential=ID:ID
+ in a settings file in /etc/systemd/nspawn/. Do not use
+ SetCredential= for secret data, as settings files are usually readable by
+ unprivileged users. Use LoadCredential= instead.
+
+
+
+
LimitCPU=LimitFSIZE=
diff --git a/src/nspawn/nspawn-gperf.gperf b/src/nspawn/nspawn-gperf.gperf
index 439e176e458..c3be58c3037 100644
--- a/src/nspawn/nspawn-gperf.gperf
+++ b/src/nspawn/nspawn-gperf.gperf
@@ -62,6 +62,8 @@ Exec.LinkJournal, config_parse_link_journal, 0,
Exec.Timezone, config_parse_timezone_mode, 0, offsetof(Settings, timezone)
Exec.SuppressSync, config_parse_tristate, 0, offsetof(Settings, suppress_sync)
Exec.RestrictAddressFamilies, config_parse_restrict_address_families, 0, 0
+Exec.SetCredential, config_parse_set_credential, 0, 0
+Exec.LoadCredential, config_parse_load_credential, 0, 0
Files.ReadOnly, config_parse_tristate, 0, offsetof(Settings, read_only)
Files.Volatile, config_parse_volatile_mode, 0, offsetof(Settings, volatile_mode)
Files.Bind, config_parse_bind, 0, 0
diff --git a/src/nspawn/nspawn-settings.c b/src/nspawn/nspawn-settings.c
index 969516037e0..63d879ccf32 100644
--- a/src/nspawn/nspawn-settings.c
+++ b/src/nspawn/nspawn-settings.c
@@ -6,7 +6,10 @@
#include "capability-list.h"
#include "conf-parser.h"
#include "cpu-set-util.h"
+#include "creds-util.h"
+#include "escape.h"
#include "extract-word.h"
+#include "memory-util.h"
#include "namespace-util.h"
#include "nspawn-expose-ports.h"
#include "nspawn-mount.h"
@@ -14,6 +17,7 @@
#include "nspawn-settings.h"
#include "parse-helpers.h"
#include "parse-util.h"
+#include "path-util.h"
#include "process-util.h"
#include "rlimit-util.h"
#include "set.h"
@@ -142,6 +146,8 @@ Settings* settings_free(Settings *s) {
free(s->hostname);
cpu_set_done(&s->cpu_set);
set_free(s->restrict_address_families);
+ machine_credential_context_done(&s->credentials);
+ strv_free(s->load_credentials);
strv_free(s->bind_user);
free(s->bind_user_shell);
@@ -1093,3 +1099,132 @@ int config_parse_restrict_address_families(
return 0;
}
+
+int config_parse_set_credential(
+ 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);
+ _cleanup_(erase_and_freep) char *value = NULL;
+ _cleanup_free_ char *id = NULL;
+ const char *p;
+ ssize_t l;
+ int r;
+
+ assert(rvalue);
+
+ /* Don't log the assignment or the data on failure, they might be secret. */
+
+ if (isempty(rvalue)) {
+ machine_credential_context_done(&settings->credentials);
+ settings->credentials = (MachineCredentialContext) {};
+ return 0;
+ }
+
+ r = machine_credential_split(rvalue, &id, &p);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid credential assignment, ignoring.");
+ return 0;
+ }
+
+ if (!credential_name_valid(id)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name is not valid, ignoring: %s", id);
+ return 0;
+ }
+
+ l = cunescape(p, UNESCAPE_ACCEPT_NUL, &value);
+ if (l == -ENOMEM)
+ return log_oom();
+ if (l < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, l, "Failed to unescape data of credential '%s', ignoring: %m", id);
+ return 0;
+ }
+
+ /* As in unit files, a later assignment overrides an earlier one for the same credential. */
+ MachineCredential *cred = machine_credential_find(&settings->credentials, id);
+ if (cred) {
+ erase_and_free(cred->data);
+ cred->data = TAKE_PTR(value);
+ cred->size = l;
+ return 0;
+ }
+
+ r = machine_credential_add(&settings->credentials, id, value, l);
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+}
+
+int config_parse_load_credential(
+ 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);
+ _cleanup_free_ char *id = NULL, *source = NULL;
+ const char *p;
+ int r;
+
+ assert(rvalue);
+
+ /* The credentials are only loaded once we know that the settings file is trusted, hence only
+ * validate the syntax for now. */
+
+ if (isempty(rvalue)) {
+ settings->load_credentials = strv_free(settings->load_credentials);
+ return 0;
+ }
+
+ r = machine_credential_split(rvalue, &id, &p);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid credential assignment, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ if (!credential_name_valid(id)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name is not valid, ignoring: %s", id);
+ return 0;
+ }
+
+ if (path_is_absolute(p) ? !path_is_normalized(p) : !credential_name_valid(p)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Credential source is neither a normalized absolute path nor a credential name, ignoring: %s", p);
+ return 0;
+ }
+
+ source = strdup(p);
+ if (!source)
+ return log_oom();
+
+ /* As in unit files, a later assignment overrides an earlier one for the same credential. */
+ STRV_FOREACH_PAIR(i, s, settings->load_credentials)
+ if (streq(*i, id)) {
+ free_and_replace(*s, source);
+ return 0;
+ }
+
+ if (strv_consume_pair(&settings->load_credentials, TAKE_PTR(id), TAKE_PTR(source)) < 0)
+ return log_oom();
+
+ return 0;
+}
diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h
index f157121de94..8ea26a3127f 100644
--- a/src/nspawn/nspawn-settings.h
+++ b/src/nspawn/nspawn-settings.h
@@ -7,6 +7,7 @@
#include "conf-parser-forward.h"
#include "cpu-set-util.h"
#include "forward.h"
+#include "machine-credential.h"
#include "rlimit-util.h"
#include "seccomp-util.h"
#include "volatile-util.h"
@@ -195,6 +196,8 @@ typedef struct Settings {
int suppress_sync;
Set *restrict_address_families;
bool restrict_address_families_is_allowlist;
+ MachineCredentialContext credentials;
+ char **load_credentials; /* pairs of credential ID and source */
/* [Files] */
int read_only;
@@ -283,6 +286,8 @@ CONFIG_PARSER_PROTOTYPE(config_parse_userns_ownership);
CONFIG_PARSER_PROTOTYPE(config_parse_bind_user);
CONFIG_PARSER_PROTOTYPE(config_parse_bind_user_shell);
CONFIG_PARSER_PROTOTYPE(config_parse_restrict_address_families);
+CONFIG_PARSER_PROTOTYPE(config_parse_set_credential);
+CONFIG_PARSER_PROTOTYPE(config_parse_load_credential);
DECLARE_STRING_TABLE_LOOKUP(resolv_conf_mode, ResolvConfMode);
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 4b52c799ed1..2613008cfe2 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -65,6 +65,7 @@
#include "image-policy.h"
#include "in-addr-util.h"
#include "io-util.h"
+#include "iovec-util.h"
#include "libmount-util.h"
#include "log.h"
#include "loop-util.h"
@@ -4754,6 +4755,73 @@ static int ptyfwd_hotkey(PTYForward *f, char c, void *userdata) {
return 0;
}
+static int merge_settings_credentials(Settings *settings, const char *path) {
+ _cleanup_(machine_credential_context_done) MachineCredentialContext credentials = {};
+ int r;
+
+ assert(settings);
+ assert(path);
+
+ if (settings->credentials.n_credentials == 0 && strv_isempty(settings->load_credentials))
+ return 0;
+
+ /* Only allow LoadCredential= in trusted files */
+ char **load_credentials = settings->load_credentials;
+ if (!arg_settings_trusted && !strv_isempty(load_credentials)) {
+ log_warning("Ignoring LoadCredential= settings, file %s is not trusted.", path);
+ load_credentials = NULL;
+ }
+
+ STRV_FOREACH_PAIR(id, source, load_credentials) {
+ _cleanup_(iovec_done_erase) struct iovec iov = {};
+
+ r = machine_credential_read(*source, &iov);
+ if (r < 0) {
+ /* As in unit files, a missing credential is not fatal if it would have been
+ * inherited from our own credentials, or if SetCredential= provides a fallback for
+ * it. The source is either an absolute path or a credential name. */
+ bool inherited = !path_is_absolute(*source);
+ bool fallback = machine_credential_find(&settings->credentials, *id) != NULL;
+ bool missing = r == -ENOENT || (inherited && r == -ENXIO);
+
+ if (missing && inherited) {
+ log_full(fallback ? LOG_DEBUG : LOG_INFO,
+ "Credential '%s' was not passed to systemd-nspawn, %s credential '%s'.",
+ *source, fallback ? "using SetCredential= fallback for" : "skipping", *id);
+ continue;
+ }
+ if (missing && fallback) {
+ log_debug_errno(r, "Couldn't read credential '%s' from '%s', using SetCredential= fallback: %m",
+ *id, *source);
+ continue;
+ }
+
+ return log_error_errno(r, "Failed to read credential '%s' from '%s': %m", *id, *source);
+ }
+
+ r = machine_credential_add(&credentials, *id, iov.iov_base, iov.iov_len);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add credential '%s': %m", *id);
+ }
+
+ /* LoadCredential= takes precedence, SetCredential= only acts as fallback. */
+ FOREACH_ARRAY(cred, settings->credentials.credentials, settings->credentials.n_credentials) {
+ if (machine_credential_find(&credentials, cred->id))
+ continue;
+
+ r = machine_credential_add(&credentials, cred->id, cred->data, cred->size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add credential '%s': %m", cred->id);
+ }
+
+ if (credentials.n_credentials > 0 || !strv_isempty(load_credentials)) {
+ machine_credential_context_done(&arg_credentials);
+ arg_credentials = TAKE_STRUCT(credentials);
+ }
+
+ return 0;
+}
+
static int merge_settings(Settings *settings, const char *path) {
int rl;
@@ -5099,6 +5167,14 @@ static int merge_settings(Settings *settings, const char *path) {
arg_restrict_address_families_is_allowlist = settings->restrict_address_families_is_allowlist;
}
+ if (!FLAGS_SET(arg_settings_mask, SETTING_CREDENTIALS)) {
+ int r;
+
+ r = merge_settings_credentials(settings, path);
+ if (r < 0)
+ return r;
+ }
+
/* The following properties can only be set through the OCI settings logic, not from the command line, hence we
* don't consult arg_settings_mask for them. */
@@ -6735,7 +6811,7 @@ static int run(int argc, char *argv[]) {
r = machine_credential_add(&arg_credentials, "journal.forward_to_socket", NSPAWN_JOURNAL_SOCKET_PATH, SIZE_MAX);
if (r == -EEXIST) {
- log_error_errno(r, "Credential 'journal.forward_to_socket' already set via --set-credential=, refusing --forward-journal=.");
+ log_error_errno(r, "Credential 'journal.forward_to_socket' already set via --set-credential= or SetCredential=, refusing --forward-journal=.");
goto finish;
}
if (r < 0) {
diff --git a/src/shared/machine-credential.c b/src/shared/machine-credential.c
index ace10b43dba..37880cc1f30 100644
--- a/src/shared/machine-credential.c
+++ b/src/shared/machine-credential.c
@@ -102,71 +102,119 @@ static int machine_credential_add_and_log(
return 0;
}
+int machine_credential_split(const char *s, char **ret_id, const char **ret_value) {
+ _cleanup_free_ char *id = NULL;
+ const char *p = ASSERT_PTR(s);
+ int r;
+
+ assert(ret_id);
+ assert(ret_value);
+
+ /* Splits "ID:VALUE" at the first ':'. Doesn't validate the ID, so that callers can log it. Returns
+ * -EBADMSG if there's no separator. */
+
+ r = extract_first_word(&p, &id, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0 || !p)
+ return -EBADMSG;
+
+ *ret_id = TAKE_PTR(id);
+ *ret_value = p;
+ return 0;
+}
+
+static bool machine_credential_source_valid(const char *source) {
+ assert(source);
+
+ return (is_path(source) && path_is_valid(source)) || credential_name_valid(source);
+}
+
+int machine_credential_read(const char *source, struct iovec *ret) {
+ ReadFullFileFlags flags = READ_FULL_FILE_SECURE;
+ _cleanup_free_ char *path_alloc = NULL;
+ int r;
+
+ assert(source);
+ assert(ret);
+
+ /* Reads a credential from a path (which may refer to an AF_UNIX socket), or from a credential passed
+ * to us, if the source is a credential name. Returns -ENXIO in the latter case if we didn't receive
+ * any credentials. Doesn't log. */
+
+ if (is_path(source) && path_is_valid(source))
+ flags |= READ_FULL_FILE_CONNECT_SOCKET;
+ else if (credential_name_valid(source)) {
+ const char *e;
+
+ r = get_credentials_dir(&e);
+ if (r < 0)
+ return r;
+
+ path_alloc = path_join(e, source);
+ if (!path_alloc)
+ return -ENOMEM;
+
+ source = path_alloc;
+ } else
+ return -EINVAL;
+
+ return read_full_file_full(
+ AT_FDCWD, source,
+ /* offset= */ UINT64_MAX,
+ /* size= */ SIZE_MAX,
+ flags,
+ /* bind_name= */ NULL,
+ (char**) &ret->iov_base, &ret->iov_len);
+}
int machine_credential_set(MachineCredentialContext *ctx, const char *cred_str) {
+ _cleanup_free_ char *id = NULL;
+ const char *p;
int r;
assert(ctx);
+ assert(cred_str);
- const char *p = ASSERT_PTR(cred_str);
- _cleanup_free_ char *id = NULL;
- r = extract_first_word(&p, &id, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ /* Don't log the argument or the data on failure, they might be secret. */
+
+ r = machine_credential_split(cred_str, &id, &p);
+ if (r == -EBADMSG)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing ':' separator in --set-credential= argument.");
if (r < 0)
return log_error_errno(r, "Failed to parse --set-credential= parameter: %m");
- if (r == 0 || !p)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Missing value for --set-credential=: %s", cred_str);
- _cleanup_free_ char *data = NULL;
+ _cleanup_(erase_and_freep) char *data = NULL;
ssize_t l;
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &data);
if (l < 0)
- return log_error_errno(l, "Failed to unescape credential data: %s", p);
+ return log_error_errno(l, "Failed to unescape data of credential '%s': %m", id);
return machine_credential_add_and_log(ctx, id, data, l);
}
int machine_credential_load(MachineCredentialContext *ctx, const char *cred_path) {
+ _cleanup_free_ char *id = NULL;
+ const char *p;
int r;
assert(ctx);
+ assert(cred_path);
- const char *p = ASSERT_PTR(cred_path);
- _cleanup_free_ char *id = NULL;
- r = extract_first_word(&p, &id, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ r = machine_credential_split(cred_path, &id, &p);
+ if (r == -EBADMSG)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing value for --load-credential=: %s", cred_path);
if (r < 0)
return log_error_errno(r, "Failed to parse --load-credential= parameter: %m");
- if (r == 0 || !p)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing value for --load-credential=: %s", cred_path);
- ReadFullFileFlags flags = READ_FULL_FILE_SECURE;
- _cleanup_free_ char *path_alloc = NULL;
- if (is_path(p) && path_is_valid(p))
- flags |= READ_FULL_FILE_CONNECT_SOCKET;
- else if (credential_name_valid(p)) {
- const char *e;
-
- r = get_credentials_dir(&e);
- if (r < 0)
- return log_error_errno(r, "Credential not available (no credentials passed at all): %s", p);
-
- path_alloc = path_join(e, p);
- if (!path_alloc)
- return log_oom();
-
- p = path_alloc;
- } else
+ if (!machine_credential_source_valid(p))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Credential source appears to be neither a valid path nor a credential name: %s", p);
_cleanup_(iovec_done_erase) struct iovec iov = {};
- r = read_full_file_full(
- AT_FDCWD, p,
- /* offset= */ UINT64_MAX,
- /* size= */ SIZE_MAX,
- flags,
- /* bind_name= */ NULL,
- (char**) &iov.iov_base, &iov.iov_len);
+ r = machine_credential_read(p, &iov);
+ if (r == -ENXIO && !is_path(p))
+ return log_error_errno(r, "Credential not available (no credentials passed at all): %s", p);
if (r < 0)
return log_error_errno(r, "Failed to read credential '%s': %m", p);
diff --git a/src/shared/machine-credential.h b/src/shared/machine-credential.h
index 244ebd6192a..32cd5db7edb 100644
--- a/src/shared/machine-credential.h
+++ b/src/shared/machine-credential.h
@@ -19,5 +19,9 @@ void machine_credential_context_done(MachineCredentialContext *ctx);
MachineCredential* machine_credential_find(MachineCredentialContext *ctx, const char *id);
int machine_credential_add(MachineCredentialContext *ctx, const char *id, const char *value, size_t size);
+
+int machine_credential_split(const char *s, char **ret_id, const char **ret_value);
+int machine_credential_read(const char *source, struct iovec *ret);
+
int machine_credential_set(MachineCredentialContext *ctx, const char *cred_str);
int machine_credential_load(MachineCredentialContext *ctx, const char *cred_path);
diff --git a/test/fuzz/fuzz-nspawn-settings/basic-config b/test/fuzz/fuzz-nspawn-settings/basic-config
index abc8b20d3dc..2b1103f8afb 100644
--- a/test/fuzz/fuzz-nspawn-settings/basic-config
+++ b/test/fuzz/fuzz-nspawn-settings/basic-config
@@ -4,6 +4,12 @@ ProcessTwo=off
Ephemeral=yes
Parameters=/sbin/init -x=1
Environment=THIS=that
+SetCredential=firstboot.locale:C.UTF-8
+SetCredential=binary:a\x00b
+LoadCredential=passwd.hashed-password.root:/etc/credstore/root-password
+LoadCredential=from-service:from-service
+SetCredential=firstboot.locale:de_DE.UTF-8
+LoadCredential=from-service:/etc/credstore/from-service
User=user
WorkingDirectory=/cwd
PivotRoot=/newroot
diff --git a/test/units/TEST-13-NSPAWN.nspawn.sh b/test/units/TEST-13-NSPAWN.nspawn.sh
index 6e6b8028b71..36c1b6c70d8 100755
--- a/test/units/TEST-13-NSPAWN.nspawn.sh
+++ b/test/units/TEST-13-NSPAWN.nspawn.sh
@@ -640,6 +640,119 @@ EOF
rm -fr "$root" "/run/systemd/nspawn/$container.nspawn"
}
+testcase_nspawn_settings_credentials() {
+ local root container
+
+ mkdir -p /run/systemd/nspawn
+ root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.nspawn-settings-credentials.XXX)"
+ container="$(basename "$root")"
+ create_dummy_container "$root"
+ rm -f "/etc/systemd/nspawn/$container.nspawn"
+ echo "foo bar" >/tmp/cred.path
+ rm -f /tmp/cred.missing
+
+ # Settings files in /run/systemd/nspawn/ are trusted, hence may load credentials from the host
+ cat >"/run/systemd/nspawn/$container.nspawn" <"$root/creds.sh" <<\EOF
+#!/usr/bin/env bash
+set -ex
+
+[[ "$CREDENTIALS_DIRECTORY" == /run/host/credentials ]]
+[[ "$("/run/systemd/nspawn/$container.nspawn" <