diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 8f2f649747..c0c98a0e88 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -1975,6 +1975,11 @@
Cache directory root
This is either /var/cache (for the system manager) or the path $XDG_CACHE_HOME resolves to (for user managers).
+
+ %d
+ Credentials directory
+ This is the value of the $CREDENTIALS_DIRECTORY environment variable if available. See section "Credentials" in systemd.exec5 for more information.
+
%E
Configuration directory root
diff --git a/src/core/unit-printf.c b/src/core/unit-printf.c
index 7b23c6662f..4a86a3c0d6 100644
--- a/src/core/unit-printf.c
+++ b/src/core/unit-printf.c
@@ -148,6 +148,20 @@ static int specifier_special_directory(char specifier, const void *data, const c
return 0;
}
+static int specifier_credentials_dir(char specifier, const void *data, const char *root, const void *userdata, char **ret) {
+ const Unit *u = ASSERT_PTR(userdata);
+ char *d;
+
+ assert(ret);
+
+ d = strjoin(u->manager->prefix[EXEC_DIRECTORY_RUNTIME], "/credentials/", u->id);
+ if (!d)
+ return -ENOMEM;
+
+ *ret = d;
+ return 0;
+}
+
int unit_name_printf(const Unit *u, const char* format, char **ret) {
/*
* This will use the passed string as format string and replace the following specifiers (which should all be
@@ -191,6 +205,7 @@ int unit_full_printf_full(const Unit *u, const char *format, size_t max_length,
* %R: the root of this systemd's instance tree (deprecated)
*
* %C: the cache directory root (e.g. /var/cache or $XDG_CACHE_HOME)
+ * %d: the credentials directory ($CREDENTIALS_DIRECTORY)
* %E: the configuration directory root (e.g. /etc or $XDG_CONFIG_HOME)
* %L: the log directory root (e.g. /var/log or $XDG_CONFIG_HOME/log)
* %S: the state directory root (e.g. /var/lib or $XDG_CONFIG_HOME)
@@ -227,6 +242,7 @@ int unit_full_printf_full(const Unit *u, const char *format, size_t max_length,
{ 'R', specifier_cgroup_root, NULL },
{ 'C', specifier_special_directory, UINT_TO_PTR(EXEC_DIRECTORY_CACHE) },
+ { 'd', specifier_credentials_dir, NULL },
{ 'E', specifier_special_directory, UINT_TO_PTR(EXEC_DIRECTORY_CONFIGURATION) },
{ 'L', specifier_special_directory, UINT_TO_PTR(EXEC_DIRECTORY_LOGS) },
{ 'S', specifier_special_directory, UINT_TO_PTR(EXEC_DIRECTORY_STATE) },
diff --git a/src/test/test-execute.c b/src/test/test-execute.c
index 49629c6bc2..8e18bbdb01 100644
--- a/src/test/test-execute.c
+++ b/src/test/test-execute.c
@@ -1086,6 +1086,7 @@ static void test_exec_specifier(Manager *m) {
test(m, "exec-specifier.service", 0, CLD_EXITED);
test(m, "exec-specifier@foo-bar.service", 0, CLD_EXITED);
test(m, "exec-specifier-interpolation.service", 0, CLD_EXITED);
+ test(m, "exec-specifier-credentials-dir.service", 0, CLD_EXITED);
}
static void test_exec_standardinput(Manager *m) {
diff --git a/test/test-execute/exec-specifier-credentials-dir.service b/test/test-execute/exec-specifier-credentials-dir.service
new file mode 100644
index 0000000000..818619acaa
--- /dev/null
+++ b/test/test-execute/exec-specifier-credentials-dir.service
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Unit]
+Description=Test for specifiers
+
+[Service]
+Type=oneshot
+Environment=TOP_SECRET=%d/very_top_secret
+# Test if the specifier is resolved correctly both before and after LoadCredential=
+ExecStart=test %d/very_top_secret = "${CREDENTIALS_DIRECTORY}/very_top_secret"
+LoadCredential=very_top_secret
+ExecStart=test %d/very_top_secret = "${CREDENTIALS_DIRECTORY}/very_top_secret"
+ExecStart=sh -c 'test %d/very_top_secret = "$TOP_SECRET"'
diff --git a/test/test-execute/exec-specifier.service b/test/test-execute/exec-specifier.service
index 190ebe93e6..ae8b2428bc 100644
--- a/test/test-execute/exec-specifier.service
+++ b/test/test-execute/exec-specifier.service
@@ -20,6 +20,7 @@ ExecStart=test %L = /var/log
ExecStart=test %E = /etc
ExecStart=test %T = /tmp
ExecStart=test %V = /var/tmp
+ExecStart=test %d = %t/credentials/%n
ExecStart=sh -c 'test %u = $$(id -un)'
ExecStart=sh -c 'test %U = $$(id -u)'
ExecStart=sh -c 'test %g = $$(id -gn)'