condition: make ConditionCredential= also search the credential stores

Credentials can reach a system through the credential stores. E.g.
`systemd-imds --import` writes cloud provided credentials to
`/run/credstore/` in the initrd. `ImportCredential=` already finds them
there, but `ConditionCredential=` and `AssertCredential=` only checked
the credentials passed into the service manager itself. Hence, a
credential in the store could configure a service, but not decide
whether a unit runs.

Make the credential condition search the same credential store
directories as `ImportCredential=`. Only the existence of a credential
is checked, encrypted ones are not decrypted.

This is a behaviour change, see NEWS. As before, non-root callers which
can't access a credential store treat these as absent, otherwise this
would break running e.g. `systemd-analyze` or `sysetmd-sysupdate` as
user.

Signed-off-by: Martin Pitt <martin@amutable.com>
This commit is contained in:
Martin Pitt committed 2026-10-20 06:55:47 +02:00
1 parent 284ee7ea66
commit 1592d554e5
6 files changed
+203 -11

No files matched your search

+13
View File
@@ -85,6 +85,19 @@ CHANGES WITH 263 in spe:
images, or set $SYSTEMD_DISSECT_VALIDATEFS=0 when using them to
restore the previous behaviour.
* ConditionCredential= and AssertCredential= now also search the
credential store directories (/etc/credstore/, /run/credstore/,
/usr/lib/credstore/, their .encrypted counterparts, and the XDG
counterparts for the per-user service manager), the same way
ImportCredential= does. Previously they only checked the credentials
passed into the service manager itself. This makes credentials
imported by e.g. "systemd-imds --import" usable for conditionalizing
units. Units with such conditions may now start (or, with "!", be
skipped) because of credential store entries that were not intended
as per-boot switches. The same applies to SuggestOnCredential= in
systemd-sysupdate. The Credential= match in .network, .netdev, and
.link files is unchanged.
Changes in systemd-udevd:
* systemd-udevd now uses fanotify rather than inotify to watch device
+2
View File
@@ -588,3 +588,5 @@ These directories are hence a great place to store credentials to load on the sy
Sometimes it makes sense to conditionalize system services and invoke them only
if the right system credential is passed to the system.
Use the `ConditionCredential=` and `AssertCredential=` unit file settings for that.
Like `ImportCredential=`, they also search the credential store directories described above,
so a credential placed in e.g. `/run/credstore/` by an initrd or cloud provisioning tool also counts.
+12
View File
@@ -1580,6 +1580,18 @@
<filename>unit@.service</filename> service instance belonging to the user. The argument must be a
valid credential name.</para>
<para>If no such credential was passed into the service manager, the credential stores are searched
too, i.e. the same directories that <varname>ImportCredential=</varname> searches (see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>),
such as <filename>/etc/credstore/</filename>, <filename>/run/credstore/</filename>,
<filename>/usr/lib/credstore/</filename> and their <filename>.encrypted</filename> counterparts for
the system service manager, and <filename>$XDG_CONFIG_HOME/credstore/</filename>,
<filename>$XDG_RUNTIME_DIR/credstore/</filename>, <filename>$HOME/.local/lib/credstore/</filename>
and their <filename>.encrypted</filename> counterparts for the per-user service manager. Hence any
credential that <varname>ImportCredential=</varname> would pass to a service also satisfies this
condition. For encrypted credentials only their existence is checked, they are not decrypted. Before
version 263, only the credentials passed into the service manager were checked.</para>
<xi:include href="version-info.xml" xpointer="v252"/></listitem>
</varlistentry>
+46 -6
View File
@@ -48,6 +48,7 @@
#include "nulstr-util.h"
#include "os-util.h"
#include "parse-util.h"
#include "path-lookup.h"
#include "path-util.h"
#include "percent-util.h"
#include "pidref.h"
@@ -146,6 +147,7 @@ static int condition_test_kernel_command_line(Condition *c, char **env, RuntimeS
}
static int condition_test_credential(Condition *c, char **env, RuntimeScope scope) {
_cleanup_strv_free_ char **dirs = NULL;
int r;
assert(c);
@@ -161,7 +163,6 @@ static int condition_test_credential(Condition *c, char **env, RuntimeScope scop
int (*gd)(const char **ret);
FOREACH_ARGUMENT(gd, get_credentials_dir, get_encrypted_credentials_dir) {
_cleanup_free_ char *j = NULL;
const char *cd;
r = gd(&cd);
@@ -170,20 +171,59 @@ static int condition_test_credential(Condition *c, char **env, RuntimeScope scop
if (r < 0)
return r;
j = path_join(cd, c->parameter);
r = strv_extend(&dirs, cd);
if (r < 0)
return r;
}
size_t n_own_dirs = strv_length(dirs);
/* Service managers also search the credential stores, the same as ImportCredential= does */
if (IN_SET(scope, RUNTIME_SCOPE_SYSTEM, RUNTIME_SCOPE_USER)) {
_cleanup_strv_free_ char **plain = NULL, **encrypted = NULL;
r = credential_store_path(scope, &plain);
if (r < 0)
return r;
r = credential_store_path_encrypted(scope, &encrypted);
if (r < 0)
return r;
r = strv_extend_strv_consume(&dirs, TAKE_PTR(plain), /* filter_duplicates= */ false);
if (r < 0)
return r;
r = strv_extend_strv_consume(&dirs, TAKE_PTR(encrypted), /* filter_duplicates= */ false);
if (r < 0)
return r;
}
/* A broken directory must not hide a match in a later one, so only report errors if the credential
* was found nowhere. */
int ret = 0;
STRV_FOREACH(d, dirs) {
_cleanup_free_ char *j = NULL;
j = path_join(*d, c->parameter);
if (!j)
return -ENOMEM;
r = access_nofollow(j, F_OK);
if (r >= 0)
return true; /* yay! */
if (r != -ENOENT)
return r;
if (r == -ENOENT)
continue; /* not found in this dir */
/* not found in this dir */
/* Credential stores may be inaccessible to the caller, in particular the system ones are only
* accessible to root. Unprivileged callers (e.g. systemd-sysupdate or systemd-analyze run as
* user) just cannot see credentials in there, which is not an error. */
if ((size_t) (d - dirs) >= n_own_dirs && ERRNO_IS_NEG_PRIVILEGE(r))
continue;
RET_GATHER(ret, r);
}
return false;
return ret; /* error, or 0 for "not found" */
}
static int condition_test_version_cmp(const char *condition, const char *ver) {
+124 -5
View File
@@ -3,6 +3,7 @@
#include <gnu/libc-version.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <unistd.h>
@@ -27,6 +28,7 @@
#include "libaudit-util.h"
#include "limits-util.h"
#include "log.h"
#include "mkdir.h"
#include "nulstr-util.h"
#include "os-util.h"
#include "path-util.h"
@@ -883,13 +885,16 @@ TEST(condition_test_credential) {
ASSERT_OK_ERRNO(unsetenv("CREDENTIALS_DIRECTORY"));
ASSERT_OK_ERRNO(unsetenv("ENCRYPTED_CREDENTIALS_DIRECTORY"));
/* Without a scope, only the credential directories from the environment are checked, which keeps
* this independent of the host's credential stores. Those are covered by the next test. */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "definitelymissing", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ, RUNTIME_SCOPE_SYSTEM));
ASSERT_OK_ZERO(condition_test(condition, environ, _RUNTIME_SCOPE_INVALID));
condition_free(condition);
/* invalid */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "..", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ, RUNTIME_SCOPE_SYSTEM));
ASSERT_OK_ZERO(condition_test(condition, environ, _RUNTIME_SCOPE_INVALID));
condition_free(condition);
ASSERT_OK(mkdtemp_malloc(NULL, &n1));
@@ -899,26 +904,140 @@ TEST(condition_test_credential) {
ASSERT_OK_ERRNO(setenv("ENCRYPTED_CREDENTIALS_DIRECTORY", n2, /* overwrite= */ true));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "stillmissing", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ, RUNTIME_SCOPE_SYSTEM));
ASSERT_OK_ZERO(condition_test(condition, environ, _RUNTIME_SCOPE_INVALID));
condition_free(condition);
ASSERT_NOT_NULL((j = path_join(n1, "existing")));
ASSERT_OK(touch(j));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "existing", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ, RUNTIME_SCOPE_SYSTEM));
ASSERT_OK_POSITIVE(condition_test(condition, environ, _RUNTIME_SCOPE_INVALID));
condition_free(condition);
free(j);
ASSERT_NOT_NULL((j = path_join(n2, "existing-encrypted")));
ASSERT_OK(touch(j));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "existing-encrypted", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ, RUNTIME_SCOPE_SYSTEM));
ASSERT_OK_POSITIVE(condition_test(condition, environ, _RUNTIME_SCOPE_INVALID));
condition_free(condition);
ASSERT_OK(set_unset_env("CREDENTIALS_DIRECTORY", d1, /* overwrite= */ true));
ASSERT_OK(set_unset_env("ENCRYPTED_CREDENTIALS_DIRECTORY", d2, /* overwrite= */ true));
}
TEST(condition_test_credential_store) {
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_free_ char *saved = NULL, *j = NULL;
Condition *condition;
/* The user scope credential stores are below $XDG_CONFIG_HOME, which we can redirect */
ASSERT_OK(free_and_strdup(&saved, getenv("XDG_CONFIG_HOME")));
ASSERT_OK(mkdtemp_malloc(NULL, &t));
ASSERT_OK_ERRNO(setenv("XDG_CONFIG_HOME", t, /* overwrite= */ true));
ASSERT_NOT_NULL((j = path_join(t, "credstore", "instore")));
ASSERT_OK(mkdir_parents(j, 0700));
ASSERT_OK(touch(j));
j = mfree(j);
ASSERT_NOT_NULL((j = path_join(t, "credstore.encrypted", "instore-encrypted")));
ASSERT_OK(mkdir_parents(j, 0700));
ASSERT_OK(touch(j));
FOREACH_STRING(name, "instore", "instore-encrypted") {
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, name, /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ, RUNTIME_SCOPE_USER));
/* without a service manager scope, the credential stores are not searched */
ASSERT_OK_ZERO(condition_test(condition, environ, _RUNTIME_SCOPE_INVALID));
condition_free(condition);
}
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "notinstore", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ, RUNTIME_SCOPE_USER));
condition_free(condition);
ASSERT_OK(set_unset_env("XDG_CONFIG_HOME", saved, /* overwrite= */ true));
}
TEST(condition_test_credential_store_error) {
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_free_ char *saved_config = NULL, *saved_runtime = NULL, *config = NULL, *j = NULL;
Condition *condition;
/* A broken credential store must not hide a match in a later one. $XDG_CONFIG_HOME/credstore is
* searched before $XDG_RUNTIME_DIR/credstore; make the former a file, so that it fails with ENOTDIR
* (unlike EACCES, this also works as root). */
ASSERT_OK(free_and_strdup(&saved_config, getenv("XDG_CONFIG_HOME")));
ASSERT_OK(free_and_strdup(&saved_runtime, getenv("XDG_RUNTIME_DIR")));
ASSERT_OK(mkdtemp_malloc(NULL, &t));
ASSERT_NOT_NULL((config = path_join(t, "config")));
ASSERT_OK_ERRNO(setenv("XDG_CONFIG_HOME", config, /* overwrite= */ true));
ASSERT_OK_ERRNO(setenv("XDG_RUNTIME_DIR", t, /* overwrite= */ true));
ASSERT_NOT_NULL((j = path_join(config, "credstore")));
ASSERT_OK(mkdir_parents(j, 0700));
ASSERT_OK(touch(j));
j = mfree(j);
ASSERT_NOT_NULL((j = path_join(t, "credstore", "inruntime")));
ASSERT_OK(mkdir_parents(j, 0700));
ASSERT_OK(touch(j));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "inruntime", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ, RUNTIME_SCOPE_USER));
condition_free(condition);
/* if it is not found anywhere, the error is reported */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "nowhere", /* trigger= */ false, /* negate= */ false)));
ASSERT_ERROR(condition_test(condition, environ, RUNTIME_SCOPE_USER), ENOTDIR);
condition_free(condition);
ASSERT_OK(set_unset_env("XDG_CONFIG_HOME", saved_config, /* overwrite= */ true));
ASSERT_OK(set_unset_env("XDG_RUNTIME_DIR", saved_runtime, /* overwrite= */ true));
}
TEST(condition_test_credential_store_inaccessible) {
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_free_ char *saved_config = NULL, *saved_creds = NULL, *store = NULL, *own = NULL, *j = NULL;
Condition *condition;
if (geteuid() == 0)
return (void) log_tests_skipped("root can access any directory");
/* Credentials in a credential store that the caller cannot access are just invisible, while an
* inaccessible own credentials directory is still an error. */
ASSERT_OK(free_and_strdup(&saved_config, getenv("XDG_CONFIG_HOME")));
ASSERT_OK(free_and_strdup(&saved_creds, getenv("CREDENTIALS_DIRECTORY")));
ASSERT_OK(mkdtemp_malloc(NULL, &t));
ASSERT_OK_ERRNO(setenv("XDG_CONFIG_HOME", t, /* overwrite= */ true));
ASSERT_OK_ERRNO(unsetenv("CREDENTIALS_DIRECTORY"));
ASSERT_NOT_NULL((store = path_join(t, "credstore")));
ASSERT_NOT_NULL((j = path_join(store, "hidden")));
ASSERT_OK(mkdir_parents(j, 0700));
ASSERT_OK(touch(j));
ASSERT_OK_ERRNO(chmod(store, 0000));
FOREACH_STRING(name, "hidden", "nowhere") {
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, name, /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ, RUNTIME_SCOPE_USER));
condition_free(condition);
}
ASSERT_NOT_NULL((own = path_join(t, "own")));
ASSERT_OK_ERRNO(mkdir(own, 0000));
ASSERT_OK_ERRNO(setenv("CREDENTIALS_DIRECTORY", own, /* overwrite= */ true));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_CREDENTIAL, "nowhere", /* trigger= */ false, /* negate= */ false)));
ASSERT_ERROR(condition_test(condition, environ, RUNTIME_SCOPE_USER), EACCES);
condition_free(condition);
/* let the cleanup remove everything */
ASSERT_OK_ERRNO(chmod(store, 0700));
ASSERT_OK_ERRNO(chmod(own, 0700));
ASSERT_OK(set_unset_env("XDG_CONFIG_HOME", saved_config, /* overwrite= */ true));
ASSERT_OK(set_unset_env("CREDENTIALS_DIRECTORY", saved_creds, /* overwrite= */ true));
}
TEST(condition_test_cpufeature) {
Condition *condition;
+6
View File
@@ -369,6 +369,12 @@ echo -n c >/etc/credstore/test.creds.third
# Credential name cannot contain ':'
echo -n hoge >/etc/credstore/test.creds.hoge:invalid
# Conditions and asserts consider the credential stores, too
systemd-run -p AssertCredential=test.creds.first -p Type=oneshot true
systemd-run -p AssertCredential=test.creds.third -p Type=oneshot true
(! systemd-run -p AssertCredential=!test.creds.first -p Type=oneshot true)
(! systemd-run -p AssertCredential=test.creds.undefined -p Type=oneshot true)
# Check if credentials with invalid names are not imported.
systemd-run -p "ImportCredential=test.creds.*" \
--unit=test-54-ImportCredential.service \