#include "alloc-util.h"
#include "extract-word.h"
+#include "fd-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "log.h"
return false;
}
+
+bool credential_name_valid(const char *s) {
+ /* We want that credential names are both valid in filenames (since that's our primary way to pass
+ * them around) and as fdnames (which is how we might want to pass them around eventually) */
+ return filename_is_valid(s) && fdname_is_valid(s);
+}
bool path_strv_contains(char **l, const char *path);
bool prefixed_path_strv_contains(char **l, const char *path);
+
+bool credential_name_valid(const char *s);
return sd_bus_message_close_container(reply);
}
+static int property_get_set_credential(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ ExecContext *c = userdata;
+ ExecSetCredential *sc;
+ Iterator iterator;
+ int r;
+
+ assert(bus);
+ assert(c);
+ assert(property);
+ assert(reply);
+
+ r = sd_bus_message_open_container(reply, 'a', "(say)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(sc, c->set_credentials, iterator) {
+
+ r = sd_bus_message_open_container(reply, 'r', "say");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "s", sc->id);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_array(reply, 'y', sc->data, sc->size);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_close_container(reply);
+}
+
+static int property_get_load_credential(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ ExecContext *c = userdata;
+ char **i, **j;
+ int r;
+
+ assert(bus);
+ assert(c);
+ assert(property);
+ assert(reply);
+
+ r = sd_bus_message_open_container(reply, 'a', "(ss)");
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH_PAIR(i, j, c->load_credentials) {
+ r = sd_bus_message_append(reply, "(ss)", *i, *j);
+ if (r < 0)
+ return r;
+ }
+
+ return sd_bus_message_close_container(reply);
+}
+
static int property_get_root_hash(
sd_bus *bus,
const char *path,
SD_BUS_PROPERTY("Group", "s", NULL, offsetof(ExecContext, group), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("DynamicUser", "b", bus_property_get_bool, offsetof(ExecContext, dynamic_user), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RemoveIPC", "b", bus_property_get_bool, offsetof(ExecContext, remove_ipc), SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("SetCredential", "a(say)", property_get_set_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
+ SD_BUS_PROPERTY("LoadCredential", "a(ss)", property_get_load_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("SupplementaryGroups", "as", NULL, offsetof(ExecContext, supplementary_groups), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PAMName", "s", NULL, offsetof(ExecContext, pam_name), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ReadWritePaths", "as", NULL, offsetof(ExecContext, read_write_paths), SD_BUS_VTABLE_PROPERTY_CONST),
return 1;
+ } else if (streq(name, "SetCredential")) {
+ bool isempty = true;
+
+ r = sd_bus_message_enter_container(message, 'a', "(say)");
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ const char *id;
+ const void *p;
+ size_t sz;
+
+ r = sd_bus_message_enter_container(message, 'r', "say");
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ r = sd_bus_message_read(message, "s", &id);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read_array(message, 'y', &p, &sz);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ if (!credential_name_valid(id))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential ID is invalid: %s", id);
+
+ isempty = false;
+
+ if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
+ _cleanup_free_ char *a = NULL, *b = NULL;
+ _cleanup_free_ void *copy = NULL;
+ ExecSetCredential *old;
+
+ copy = memdup(p, sz);
+ if (!copy)
+ return -ENOMEM;
+
+ old = hashmap_get(c->set_credentials, id);
+ if (old) {
+ free_and_replace(old->data, copy);
+ old->size = sz;
+ } else {
+ _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
+
+ sc = new0(ExecSetCredential, 1);
+ if (!sc)
+ return -ENOMEM;
+
+ sc->id = strdup(id);
+ if (!sc->id)
+ return -ENOMEM;
+
+ sc->data = TAKE_PTR(copy);
+ sc->size = sz;
+
+ r = hashmap_ensure_allocated(&c->set_credentials, &exec_set_credential_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(c->set_credentials, sc->id, sc);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(sc);
+ }
+
+ a = specifier_escape(id);
+ if (!a)
+ return -ENOMEM;
+
+ b = cescape_length(p, sz);
+ if (!b)
+ return -ENOMEM;
+
+ (void) unit_write_settingf(u, flags, name, "%s=%s:%s", name, a, b);
+ }
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) {
+ c->set_credentials = hashmap_free(c->set_credentials);
+ (void) unit_write_settingf(u, flags, name, "%s=", name);
+ }
+
+ return 1;
+
+ } else if (streq(name, "LoadCredential")) {
+ bool isempty = true;
+
+ r = sd_bus_message_enter_container(message, 'a', "(ss)");
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ const char *id, *source;
+
+ r = sd_bus_message_read(message, "(ss)", &id, &source);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ if (!credential_name_valid(id))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential ID is invalid: %s", id);
+
+ if (!(path_is_absolute(source) ? path_is_normalized(source) : credential_name_valid(source)))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential source is invalid: %s", source);
+
+ isempty = false;
+
+ if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
+ r = strv_extend_strv(&c->load_credentials, STRV_MAKE(id, source), /* filter_duplicates = */ false);
+ if (r < 0)
+ return r;
+
+ (void) unit_write_settingf(u, flags|UNIT_ESCAPE_SPECIFIERS, name, "%s=%s:%s", name, id, source);
+ }
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ return r;
+
+ if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) {
+ c->load_credentials = strv_free(c->load_credentials);
+ (void) unit_write_settingf(u, flags, name, "%s=", name);
+ }
+
+ return 1;
+
} else if (streq(name, "SyslogLevel")) {
int32_t level;
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
+#include <sys/mount.h>
#include <sys/personality.h>
#include <sys/prctl.h>
#include <sys/shm.h>
#include "sd-messages.h"
+#include "acl-util.h"
#include "af-list.h"
#include "alloc-util.h"
#if HAVE_APPARMOR
#include "barrier.h"
#include "cap-list.h"
#include "capability-util.h"
-#include "chown-recursive.h"
#include "cgroup-setup.h"
+#include "chown-recursive.h"
#include "cpu-set-util.h"
#include "def.h"
#include "env-file.h"
#include "execute.h"
#include "exit-status.h"
#include "fd-util.h"
+#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "memory-util.h"
#include "missing_fs.h"
#include "mkdir.h"
+#include "mountpoint-util.h"
#include "namespace.h"
#include "parse-util.h"
#include "path-util.h"
#include "strv.h"
#include "syslog-util.h"
#include "terminal-util.h"
+#include "tmpfile-util.h"
#include "umask-util.h"
#include "unit.h"
#include "user-util.h"
c->protect_hostname;
}
+static bool exec_context_has_credentials(const ExecContext *context) {
+
+ assert(context);
+
+ return !hashmap_isempty(context->set_credentials) ||
+ context->load_credentials;
+}
+
#if HAVE_SECCOMP
static bool skip_seccomp_unavailable(const Unit* u, const char* msg) {
assert(p);
assert(ret);
-#define N_ENV_VARS 15
+#define N_ENV_VARS 16
our_env = new0(char*, N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX);
if (!our_env)
return -ENOMEM;
our_env[n_env++] = x;
}
+ if (exec_context_has_credentials(c) && p->prefix[EXEC_DIRECTORY_RUNTIME]) {
+ x = strjoin("CREDENTIALS_DIRECTORY=", p->prefix[EXEC_DIRECTORY_RUNTIME], "/credentials/", u->id);
+ if (!x)
+ return -ENOMEM;
+
+ our_env[n_env++] = x;
+ }
+
our_env[n_env++] = NULL;
assert(n_env <= N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX);
#undef N_ENV_VARS
return r;
}
+static int write_credential(
+ int dfd,
+ const char *id,
+ const void *data,
+ size_t size,
+ uid_t uid,
+ bool ownership_ok) {
+
+ _cleanup_(unlink_and_freep) char *tmp = NULL;
+ _cleanup_close_ int fd = -1;
+ int r;
+
+ r = tempfn_random_child("", "cred", &tmp);
+ if (r < 0)
+ return r;
+
+ fd = openat(dfd, tmp, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL|O_NOFOLLOW|O_NOCTTY, 0600);
+ if (fd < 0) {
+ tmp = mfree(tmp);
+ return -errno;
+ }
+
+ r = loop_write(fd, data, size, /* do_pool = */ false);
+ if (r < 0)
+ return r;
+
+ if (fchmod(fd, 0400) < 0) /* Take away "w" bit */
+ return -errno;
+
+ if (uid_is_valid(uid) && uid != getuid()) {
+#if HAVE_ACL
+ r = fd_add_uid_acl_permission(fd, uid, /* read = */ true, /* write = */ false, /* execute = */ false);
+#else
+ r = -EOPNOTSUPP;
+#endif
+ if (r < 0) {
+ if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
+ return r;
+
+ if (!ownership_ok) /* Ideally we use ACLs, since we can neatly express what we want
+ * to express: that the user gets read access and nothing
+ * else. But if the backing fs can't support that (e.g. ramfs)
+ * then we can use file ownership instead. But that's only safe if
+ * we can then re-mount the whole thing read-only, so that the
+ * user can no longer chmod() the file to gain write access. */
+ return r;
+
+ if (fchown(fd, uid, (gid_t) -1) < 0)
+ return -errno;
+ }
+ }
+
+ if (renameat(dfd, tmp, dfd, id) < 0)
+ return -errno;
+
+ tmp = mfree(tmp);
+ return 0;
+}
+
+#define CREDENTIALS_BYTES_MAX (1024LU * 1024LU) /* Refuse to pass more than 1M, after all this is unswappable memory */
+
+static int acquire_credentials(
+ const ExecContext *context,
+ const ExecParameters *params,
+ const char *p,
+ uid_t uid,
+ bool ownership_ok) {
+
+ uint64_t left = CREDENTIALS_BYTES_MAX;
+ _cleanup_close_ int dfd = -1;
+ ExecSetCredential *sc;
+ char **id, **fn;
+ Iterator iterator;
+ int r;
+
+ assert(context);
+ assert(p);
+
+ dfd = open(p, O_DIRECTORY|O_CLOEXEC);
+ if (dfd < 0)
+ return -errno;
+
+ /* First we use the literally specified credentials. Note that they might be overriden again below,
+ * and thus act as a "default" if the same credential is specified multiple times */
+ HASHMAP_FOREACH(sc, context->set_credentials, iterator) {
+ size_t add;
+
+ add = strlen(sc->id) + sc->size;
+ if (add > left)
+ return -E2BIG;
+
+ r = write_credential(dfd, sc->id, sc->data, sc->size, uid, ownership_ok);
+ if (r < 0)
+ return r;
+
+ left -= add;
+ }
+
+ /* Then, load credential off disk (or acquire via AF_UNIX socket) */
+ STRV_FOREACH_PAIR(id, fn, context->load_credentials) {
+ ReadFullFileFlags flags = READ_FULL_FILE_SECURE;
+ _cleanup_(erase_and_freep) char *data = NULL;
+ _cleanup_free_ char *j = NULL;
+ const char *source;
+ size_t size, add;
+
+ if (path_is_absolute(*fn)) {
+ /* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */
+ source = *fn;
+ flags |= READ_FULL_FILE_CONNECT_SOCKET;
+ } else if (params->received_credentials) {
+ /* If this is a relative path, take it relative to the credentials we received
+ * ourselves. We don't support the AF_UNIX stuff in this mode, since we are operating
+ * on a credential store, i.e. this is guaranteed to be regular files. */
+ j = path_join(params->received_credentials, *fn);
+ if (!j)
+ return -ENOMEM;
+
+ source = j;
+ } else
+ source = NULL;
+
+ if (source)
+ r = read_full_file_full(AT_FDCWD, source, flags, &data, &size);
+ else
+ r = -ENOENT;
+ if (r == -ENOENT &&
+ faccessat(dfd, *id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) /* If the source file doesn't exist, but we already acquired the key otherwise, then don't fail */
+ continue;
+ if (r < 0)
+ return r;
+
+ add = strlen(*id) + size;
+ if (add > left)
+ return -E2BIG;
+
+ r = write_credential(dfd, *id, data, size, uid, ownership_ok);
+ if (r < 0)
+ return r;
+
+ left -= add;
+ }
+
+ if (fchmod(dfd, 0500) < 0) /* Now take away the "w" bit */
+ return -errno;
+
+ /* After we created all keys with the right perms, also make sure the credential store as a whole is
+ * accessible */
+
+ if (uid_is_valid(uid) && uid != getuid()) {
+#if HAVE_ACL
+ r = fd_add_uid_acl_permission(dfd, uid, /* read = */ true, /* write = */ false, /* execute = */ true);
+#else
+ r = -EOPNOTSUPP;
+#endif
+ if (r < 0) {
+ if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
+ return r;
+
+ if (!ownership_ok)
+ return r;
+
+ if (fchown(dfd, uid, (gid_t) -1) < 0)
+ return -errno;
+ }
+ }
+
+ return 0;
+}
+
+static int setup_credentials_internal(
+ const ExecContext *context,
+ const ExecParameters *params,
+ const char *final, /* This is where the credential store shall eventually end up at */
+ const char *workspace, /* This is where we can prepare it before moving it to the final place */
+ bool reuse_workspace, /* Whether to reuse any existing workspace mount if it already is a mount */
+ bool must_mount, /* Whether to require that we mount something, it's not OK to use the plain directory fall back */
+ uid_t uid) {
+
+ int r, workspace_mounted; /* negative if we don't know yet whether we have/can mount something; true
+ * if we mounted something; false if we definitely can't mount anything */
+ bool final_mounted;
+ const char *where;
+
+ assert(context);
+ assert(final);
+ assert(workspace);
+
+ if (reuse_workspace) {
+ r = path_is_mount_point(workspace, NULL, 0);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ workspace_mounted = true; /* If this is already a mount, and we are supposed to reuse it, let's keep this in mind */
+ else
+ workspace_mounted = -1; /* We need to figure out if we can mount something to the workspace */
+ } else
+ workspace_mounted = -1; /* ditto */
+
+ r = path_is_mount_point(final, NULL, 0);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ /* If the final place already has something mounted, we use that. If the workspace also has
+ * something mounted we assume it's actually the same mount (but with MS_RDONLY
+ * different). */
+ final_mounted = true;
+
+ if (workspace_mounted < 0) {
+ /* If the final place is mounted, but the workspace we isn't, then let's bind mount
+ * the final version to the workspace, and make it writable, so that we can make
+ * changes */
+
+ if (mount(final, workspace, NULL, MS_BIND|MS_REC, NULL) < 0)
+ return -errno;
+
+ if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0)
+ return -errno;
+
+ workspace_mounted = true;
+ }
+ } else
+ final_mounted = false;
+
+ if (workspace_mounted < 0) {
+ /* Nothing is mounted on the workspace yet, let's try to mount something now */
+ for (int try = 0;; try++) {
+
+ if (try == 0) {
+ /* Try "ramfs" first, since it's not swap backed */
+ if (mount("ramfs", workspace, "ramfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, "mode=0700") >= 0) {
+ workspace_mounted = true;
+ break;
+ }
+
+ } else if (try == 1) {
+ _cleanup_free_ char *opts = NULL;
+
+ if (asprintf(&opts, "mode=0700,nr_inodes=1024,size=%lu", CREDENTIALS_BYTES_MAX) < 0)
+ return -ENOMEM;
+
+ /* Fall back to "tmpfs" otherwise */
+ if (mount("tmpfs", workspace, "tmpfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, opts) >= 0) {
+ workspace_mounted = true;
+ break;
+ }
+
+ } else {
+ /* If that didn't work, try to make a bind mount from the final to the workspace, so that we can make it writable there. */
+ if (mount(final, workspace, NULL, MS_BIND|MS_REC, NULL) < 0) {
+ if (!ERRNO_IS_PRIVILEGE(errno)) /* Propagate anything that isn't a permission problem */
+ return -errno;
+
+ if (must_mount) /* If we it's not OK to use the plain directory
+ * fallback, propagate all errors too */
+ return -errno;
+
+ /* If we lack privileges to bind mount stuff, then let's gracefully
+ * proceed for compat with container envs, and just use the final dir
+ * as is. */
+
+ workspace_mounted = false;
+ break;
+ }
+
+ /* Make the new bind mount writable (i.e. drop MS_RDONLY) */
+ if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0)
+ return -errno;
+
+ workspace_mounted = true;
+ break;
+ }
+ }
+ }
+
+ assert(!must_mount || workspace_mounted > 0);
+ where = workspace_mounted ? workspace : final;
+
+ r = acquire_credentials(context, params, where, uid, workspace_mounted);
+ if (r < 0)
+ return r;
+
+ if (workspace_mounted) {
+ /* Make workspace read-only now, so that any bind mount we make from it defaults to read-only too */
+ if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0)
+ return -errno;
+
+ /* And mount it to the final place, read-only */
+ if (final_mounted) {
+ if (umount2(workspace, MNT_DETACH|UMOUNT_NOFOLLOW) < 0)
+ return -errno;
+ } else {
+ if (mount(workspace, final, NULL, MS_MOVE, NULL) < 0)
+ return -errno;
+ }
+ } else {
+ _cleanup_free_ char *parent = NULL;
+
+ /* If we do not have our own mount put used the plain directory fallback, then we need to
+ * open access to the top-level credential directory and the per-service directory now */
+
+ parent = dirname_malloc(final);
+ if (!parent)
+ return -ENOMEM;
+ if (chmod(parent, 0755) < 0)
+ return -errno;
+ }
+
+ return 0;
+}
+
+static int setup_credentials(
+ const ExecContext *context,
+ const ExecParameters *params,
+ const char *unit,
+ uid_t uid) {
+
+ _cleanup_free_ char *p = NULL, *q = NULL;
+ const char *i;
+ int r;
+
+ assert(context);
+ assert(params);
+
+ if (!exec_context_has_credentials(context))
+ return 0;
+
+ if (!params->prefix[EXEC_DIRECTORY_RUNTIME])
+ return -EINVAL;
+
+ /* This where we'll place stuff when we are done; this main credentials directory is world-readable,
+ * and the subdir we mount over with a read-only file system readable by the service's user */
+ q = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials");
+ if (!q)
+ return -ENOMEM;
+
+ r = mkdir_label(q, 0755); /* top-level dir: world readable/searchable */
+ if (r < 0 && r != -EEXIST)
+ return r;
+
+ p = path_join(q, unit);
+ if (!p)
+ return -ENOMEM;
+
+ r = mkdir_label(p, 0700); /* per-unit dir: private to user */
+ if (r < 0 && r != -EEXIST)
+ return r;
+
+ r = safe_fork("(sd-mkdcreds)", FORK_DEATHSIG|FORK_WAIT|FORK_NEW_MOUNTNS, NULL);
+ if (r < 0) {
+ _cleanup_free_ char *t = NULL, *u = NULL;
+
+ /* If this is not a privilege or support issue then propagate the error */
+ if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
+ return r;
+
+ /* Temporary workspace, that remains inaccessible all the time. We prepare stuff there before moving
+ * it into place, so that users can't access half-initialized credential stores. */
+ t = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "systemd/temporary-credentials");
+ if (!t)
+ return -ENOMEM;
+
+ /* We can't set up a mount namespace. In that case operate on a fixed, inaccessible per-unit
+ * directory outside of /run/credentials/ first, and then move it over to /run/credentials/
+ * after it is fully set up */
+ u = path_join(t, unit);
+ if (!u)
+ return -ENOMEM;
+
+ FOREACH_STRING(i, t, u) {
+ r = mkdir_label(i, 0700);
+ if (r < 0 && r != -EEXIST)
+ return r;
+ }
+
+ r = setup_credentials_internal(
+ context,
+ params,
+ p, /* final mount point */
+ u, /* temporary workspace to overmount */
+ true, /* reuse the workspace if it is already a mount */
+ false, /* it's OK to fall back to a plain directory if we can't mount anything */
+ uid);
+
+ (void) rmdir(u); /* remove the workspace again if we can. */
+
+ if (r < 0)
+ return r;
+
+ } else if (r == 0) {
+
+ /* We managed to set up a mount namespace, and are now in a child. That's great. In this case
+ * we can use the same directory for all cases, after turning off propagation. Question
+ * though is: where do we turn off propagation exactly, and where do we place the workspace
+ * directory? We need some place that is guaranteed to be a mount point in the host, and
+ * which is guaranteed to have a subdir we can mount over. /run/ is not suitable for this,
+ * since we ultimately want to move the resulting file system there, i.e. we need propagation
+ * for /run/ eventually. We could use our own /run/systemd/bind mount on itself, but that
+ * would be visible in the host mount table all the time, which we want to avoid. Hence, what
+ * we do here instead we use /dev/ and /dev/shm/ for our purposes. We know for sure that
+ * /dev/ is a mount point and we now for sure that /dev/shm/ exists. Hence we can turn off
+ * propagation on the former, and then overmount the latter.
+ *
+ * Yes it's nasty playing games with /dev/ and /dev/shm/ like this, since it does not exist
+ * for this purpose, but there are few other candidates that work equally well for us, and
+ * given that the we do this in a privately namespaced short-lived single-threaded process
+ * that noone else sees this should be OK to do.*/
+
+ if (mount(NULL, "/dev", NULL, MS_SLAVE|MS_REC, NULL) < 0) /* Turn off propagation from our namespace to host */
+ goto child_fail;
+
+ r = setup_credentials_internal(
+ context,
+ params,
+ p, /* final mount point */
+ "/dev/shm", /* temporary workspace to overmount */
+ false, /* do not reuse /dev/shm if it is already a mount, under no circumstances */
+ true, /* insist that something is mounted, do not allow fallback to plain directory */
+ uid);
+ if (r < 0)
+ goto child_fail;
+
+ _exit(EXIT_SUCCESS);
+
+ child_fail:
+ _exit(EXIT_FAILURE);
+ }
+
+ return 0;
+}
+
#if ENABLE_SMACK
static int setup_smack(
const ExecContext *context,
return log_unit_error_errno(unit, r, "Failed to set up special execution directory in %s: %m", params->prefix[dt]);
}
+ if (FLAGS_SET(params->flags, EXEC_WRITE_CREDENTIALS)) {
+ r = setup_credentials(context, params, unit->id, uid);
+ if (r < 0) {
+ *exit_status = EXIT_CREDENTIALS;
+ return log_unit_error_errno(unit, r, "Failed to set up credentials: %m");
+ }
+ }
+
r = build_environment(
unit,
context,
c->network_namespace_path = mfree(c->network_namespace_path);
c->log_namespace = mfree(c->log_namespace);
+
+ c->load_credentials = strv_free(c->load_credentials);
+ c->set_credentials = hashmap_free(c->set_credentials);
}
int exec_context_destroy_runtime_directory(const ExecContext *c, const char *runtime_prefix) {
return 0;
}
+int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_prefix, const char *unit) {
+ _cleanup_free_ char *p = NULL;
+
+ assert(c);
+
+ if (!runtime_prefix || !unit)
+ return 0;
+
+ p = path_join(runtime_prefix, "credentials", unit);
+ if (!p)
+ return -ENOMEM;
+
+ /* This is either a tmpfs/ramfs of its own, or a plain directory. Either way, let's first try to
+ * unmount it, and afterwards remove the mount point */
+ (void) umount2(p, MNT_DETACH|UMOUNT_NOFOLLOW);
+ (void) rm_rf(p, REMOVE_ROOT|REMOVE_CHMOD);
+
+ return 0;
+}
+
static void exec_command_done(ExecCommand *c) {
assert(c);
p->exec_fd = safe_close(p->exec_fd);
}
+ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc) {
+ if (!sc)
+ return NULL;
+
+ free(sc->id);
+ free(sc->data);
+ return mfree(sc);
+}
+
+DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_set_credential_hash_ops, char, string_hash_func, string_compare_func, ExecSetCredential, exec_set_credential_free);
+
static const char* const exec_input_table[_EXEC_INPUT_MAX] = {
[EXEC_INPUT_NULL] = "null",
[EXEC_INPUT_TTY] = "tty",
_EXEC_CLEAN_MASK_INVALID = -1,
} ExecCleanMask;
+/* A credential configured with SetCredential= */
+typedef struct ExecSetCredential {
+ char *id;
+ void *data;
+ size_t size;
+} ExecSetCredential;
+
/* Encodes configuration parameters applied to invoked commands. Does not carry runtime data, but only configuration
* changes sourced from unit files and suchlike. ExecContext objects are usually embedded into Unit objects, and do not
* change after being loaded. */
ExecDirectory directories[_EXEC_DIRECTORY_TYPE_MAX];
ExecPreserveMode runtime_directory_preserve_mode;
usec_t timeout_clean_usec;
+
+ Hashmap *set_credentials; /* output id → ExecSetCredential */
+ char **load_credentials; /* pairs of output id, path/input id */
};
static inline bool exec_context_restrict_namespaces_set(const ExecContext *c) {
EXEC_CGROUP_DELEGATE = 1 << 6,
EXEC_IS_CONTROL = 1 << 7,
EXEC_CONTROL_CGROUP = 1 << 8, /* Place the process not in the indicated cgroup but in a subcgroup '/.control', but only EXEC_CGROUP_DELEGATE and EXEC_IS_CONTROL is set, too */
+ EXEC_WRITE_CREDENTIALS = 1 << 9, /* Set up the credential store logic */
/* The following are not used by execute.c, but by consumers internally */
- EXEC_PASS_FDS = 1 << 9,
- EXEC_SETENV_RESULT = 1 << 10,
- EXEC_SET_WATCHDOG = 1 << 11,
+ EXEC_PASS_FDS = 1 << 10,
+ EXEC_SETENV_RESULT = 1 << 11,
+ EXEC_SET_WATCHDOG = 1 << 12,
} ExecFlags;
/* Parameters for a specific invocation of a command. This structure is put together right before a command is
const char *cgroup_path;
char **prefix;
+ const char *received_credentials;
const char *confirm_spawn;
void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix);
int exec_context_destroy_runtime_directory(const ExecContext *c, const char *runtime_root);
+int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_root, const char *unit);
const char* exec_context_fdname(const ExecContext *c, int fd_index);
bool exec_context_get_cpu_affinity_from_numa(const ExecContext *c);
+ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc);
+DEFINE_TRIVIAL_CLEANUP_FUNC(ExecSetCredential*, exec_set_credential_free);
+
+extern const struct hash_ops exec_set_credential_hash_ops;
+
const char* exec_output_to_string(ExecOutput i) _const_;
ExecOutput exec_output_from_string(const char *s) _pure_;
$1.LogsDirectory, config_parse_exec_directories, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_LOGS].paths)
$1.ConfigurationDirectoryMode, config_parse_mode, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].mode)
$1.ConfigurationDirectory, config_parse_exec_directories, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].paths)
+$1.SetCredential, config_parse_set_credential, 0, offsetof($1, exec_context)
+$1.LoadCredential, config_parse_load_credential, 0, offsetof($1, exec_context)
$1.TimeoutCleanSec, config_parse_sec, 0, offsetof($1, exec_context.timeout_clean_usec)
$1.ProtectHostname, config_parse_bool, 0, offsetof($1, exec_context.protect_hostname)
m4_ifdef(`HAVE_PAM',
#include "unit-name.h"
#include "unit-printf.h"
#include "user-util.h"
+#include "utf8.h"
#include "web-util.h"
static int parse_socket_protocol(const char *s) {
}
}
+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) {
+
+ _cleanup_free_ char *word = NULL, *k = NULL, *unescaped = NULL;
+ ExecContext *context = data;
+ ExecSetCredential *old;
+ Unit *u = userdata;
+ const char *p;
+ int r, l;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(context);
+
+ if (isempty(rvalue)) {
+ /* Empty assignment resets the list */
+ context->set_credentials = hashmap_free(context->set_credentials);
+ return 0;
+ }
+
+ p = rvalue;
+ r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r <= 0 || !p) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ r = unit_full_printf(u, word, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", word);
+ return 0;
+ }
+ if (!credential_name_valid(k)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name \"%s\" not valid, ignoring.", k);
+ return 0;
+ }
+
+ /* We support escape codes here, so that users can insert trailing \n if they like */
+ l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
+ if (l < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, l, "Can't unescape \"%s\", ignoring: %m", p);
+ return 0;
+ }
+
+ old = hashmap_get(context->set_credentials, k);
+ if (old) {
+ free_and_replace(old->data, unescaped);
+ old->size = l;
+ } else {
+ _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
+
+ sc = new0(ExecSetCredential, 1);
+ if (!sc)
+ return log_oom();
+
+ sc->id = TAKE_PTR(k);
+ sc->data = TAKE_PTR(unescaped);
+ sc->size = l;
+
+ r = hashmap_ensure_allocated(&context->set_credentials, &exec_set_credential_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(context->set_credentials, sc->id, sc);
+ if (r < 0)
+ return log_oom();
+
+ TAKE_PTR(sc);
+ }
+
+ 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) {
+
+ _cleanup_free_ char *word = NULL, *k = NULL, *q = NULL;
+ ExecContext *context = data;
+ Unit *u = userdata;
+ const char *p;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(context);
+
+ if (isempty(rvalue)) {
+ /* Empty assignment resets the list */
+ context->load_credentials = strv_free(context->load_credentials);
+ return 0;
+ }
+
+ p = rvalue;
+ r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r <= 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
+ return 0;
+ }
+
+ r = unit_full_printf(u, word, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", word);
+ return 0;
+ }
+ if (!credential_name_valid(k)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name \"%s\" not valid, ignoring.", k);
+ return 0;
+ }
+ r = unit_full_printf(u, p, &q);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", p);
+ return 0;
+ }
+ if (path_is_absolute(q) ? !path_is_normalized(q) : !credential_name_valid(q)) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Credential source \"%s\" not valid, ignoring.", q);
+ return 0;
+ }
+
+ r = strv_consume_pair(&context->load_credentials, TAKE_PTR(k), TAKE_PTR(q));
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+}
+
int config_parse_set_status(
const char *unit,
const char *filename,
CONFIG_PARSER_PROTOTYPE(config_parse_address_families);
CONFIG_PARSER_PROTOTYPE(config_parse_runtime_preserve_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_directories);
+CONFIG_PARSER_PROTOTYPE(config_parse_set_credential);
+CONFIG_PARSER_PROTOTYPE(config_parse_load_credential);
CONFIG_PARSER_PROTOTYPE(config_parse_set_status);
CONFIG_PARSER_PROTOTYPE(config_parse_namespace_path_strv);
CONFIG_PARSER_PROTOTYPE(config_parse_temporary_filesystems);
l,
"CACHE_DIRECTORY",
"CONFIGURATION_DIRECTORY",
+ "CREDENTIALS_DIRECTORY",
"EXIT_CODE",
"EXIT_STATUS",
"INVOCATION_ID",
int manager_new(UnitFileScope scope, ManagerTestRunFlags test_run_flags, Manager **_m) {
_cleanup_(manager_freep) Manager *m = NULL;
+ const char *e;
int r;
assert(_m);
if (r < 0)
return r;
+ e = secure_getenv("CREDENTIALS_DIRECTORY");
+ if (e) {
+ m->received_credentials = strdup(e);
+ if (!m->received_credentials)
+ return -ENOMEM;
+ }
+
r = sd_event_default(&m->event);
if (r < 0)
return r;
for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++)
m->prefix[dt] = mfree(m->prefix[dt]);
+ free(m->received_credentials);
return mfree(m);
}
/* Prefixes of e.g. RuntimeDirectory= */
char *prefix[_EXEC_DIRECTORY_TYPE_MAX];
+ char *received_credentials;
/* Used in the SIGCHLD and sd_notify() message invocation logic to avoid that we dispatch the same event
* multiple times on the same unit. */
libkmod,
libapparmor,
libselinux,
- libmount])
+ libmount,
+ libacl])
systemd_sources = files('main.c')
m->exec_runtime = exec_runtime_unref(m->exec_runtime, true);
- unit_destroy_runtime_directory(UNIT(m), &m->exec_context);
+ unit_destroy_runtime_data(UNIT(m), &m->exec_context);
unit_unref_uid_gid(UNIT(m), true);
s->exec_runtime = exec_runtime_unref(s->exec_runtime, true);
/* Also, remove the runtime directory */
- unit_destroy_runtime_directory(UNIT(s), &s->exec_context);
+ unit_destroy_runtime_data(UNIT(s), &s->exec_context);
/* Get rid of the IPC bits of the user */
unit_unref_uid_gid(UNIT(s), true);
r = service_spawn(s,
c,
timeout,
- EXEC_PASS_FDS|EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_APPLY_TTY_STDIN|EXEC_SET_WATCHDOG,
+ EXEC_PASS_FDS|EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_APPLY_TTY_STDIN|EXEC_SET_WATCHDOG|EXEC_WRITE_CREDENTIALS,
&pid);
if (r < 0)
goto fail;
s->exec_runtime = exec_runtime_unref(s->exec_runtime, true);
- unit_destroy_runtime_directory(UNIT(s), &s->exec_context);
+ unit_destroy_runtime_data(UNIT(s), &s->exec_context);
unit_unref_uid_gid(UNIT(s), true);
s->exec_runtime = exec_runtime_unref(s->exec_runtime, true);
- unit_destroy_runtime_directory(UNIT(s), &s->exec_context);
+ unit_destroy_runtime_data(UNIT(s), &s->exec_context);
unit_unref_uid_gid(UNIT(s), true);
p->cgroup_path = u->cgroup_path;
SET_FLAG(p->flags, EXEC_CGROUP_DELEGATE, unit_cgroup_delegate(u));
+ p->received_credentials = u->manager->received_credentials;
+
return 0;
}
return 0;
}
-void unit_destroy_runtime_directory(Unit *u, const ExecContext *context) {
+void unit_destroy_runtime_data(Unit *u, const ExecContext *context) {
+ assert(u);
+ assert(context);
+
if (context->runtime_directory_preserve_mode == EXEC_PRESERVE_NO ||
(context->runtime_directory_preserve_mode == EXEC_PRESERVE_RESTART && !unit_will_restart(u)))
exec_context_destroy_runtime_directory(context, u->manager->prefix[EXEC_DIRECTORY_RUNTIME]);
+
+ exec_context_destroy_credentials(context, u->manager->prefix[EXEC_DIRECTORY_RUNTIME], u->id);
}
int unit_clean(Unit *u, ExecCleanMask mask) {
int unit_test_trigger_loaded(Unit *u);
-void unit_destroy_runtime_directory(Unit *u, const ExecContext *context);
+void unit_destroy_runtime_data(Unit *u, const ExecContext *context);
int unit_clean(Unit *u, ExecCleanMask mask);
int unit_can_clean(Unit *u, ExecCleanMask *ret_mask);
return 1;
}
+ if (streq(field, "SetCredential")) {
+ r = sd_bus_message_open_container(m, 'r', "sv");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_basic(m, 's', "SetCredential");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_open_container(m, 'v', "a(say)");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ if (isempty(eq))
+ r = sd_bus_message_append(m, "a(say)", 0);
+ else {
+ _cleanup_free_ char *word = NULL, *unescaped = NULL;
+ const char *p = eq;
+ int l;
+
+ r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse SetCredential= parameter: %s", eq);
+ if (r == 0 || !p)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to SetCredential=.");
+
+ l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
+ if (l < 0)
+ return log_error_errno(l, "Failed to unescape SetCredential= value: %s", p);
+
+ r = sd_bus_message_open_container(m, 'a', "(say)");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_open_container(m, 'r', "say");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", word);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_array(m, 'y', unescaped, l);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_close_container(m);
+ }
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ return 1;
+ }
+
+ if (streq(field, "LoadCredential")) {
+ r = sd_bus_message_open_container(m, 'r', "sv");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_basic(m, 's', "LoadCredential");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_open_container(m, 'v', "a(ss)");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ if (isempty(eq))
+ r = sd_bus_message_append(m, "a(ss)", 0);
+ else {
+ _cleanup_free_ char *word = NULL;
+ const char *p = eq;
+
+ r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse LoadCredential= parameter: %s", eq);
+ if (r == 0 || !p)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to LoadCredential=.");
+
+ r = sd_bus_message_append(m, "a(ss)", 1, word, p);
+ }
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_close_container(m);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ return 1;
+ }
+
if (streq(field, "LogExtraFields")) {
r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0)
[EXIT_LOGS_DIRECTORY] = { "LOGS_DIRECTORY", EXIT_STATUS_SYSTEMD },
[EXIT_CONFIGURATION_DIRECTORY] = { "CONFIGURATION_DIRECTORY", EXIT_STATUS_SYSTEMD },
[EXIT_NUMA_POLICY] = { "NUMA_POLICY", EXIT_STATUS_SYSTEMD },
+ [EXIT_CREDENTIALS] = { "CREDENTIALS", EXIT_STATUS_SYSTEMD },
+
[EXIT_EXCEPTION] = { "EXCEPTION", EXIT_STATUS_SYSTEMD },
[EXIT_INVALIDARGUMENT] = { "INVALIDARGUMENT", EXIT_STATUS_LSB },
EXIT_LOGS_DIRECTORY, /* 240 */
EXIT_CONFIGURATION_DIRECTORY,
EXIT_NUMA_POLICY,
+ EXIT_CREDENTIALS,
EXIT_EXCEPTION = 255, /* Whenever we want to propagate an abnormal/signal exit, in line with bash */
};