core: add credentials logic

Fixes: #15778 #16060
This commit is contained in:
Lennart Poettering 2020-07-23 08:49:52 +02:00
parent 9db59d9283
commit bb0c0d6f29
20 changed files with 1039 additions and 13 deletions

View File

@ -14,6 +14,7 @@
#include "alloc-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "log.h"
@ -1127,3 +1128,9 @@ bool prefixed_path_strv_contains(char **l, const char *path) {
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);
}

View File

@ -173,3 +173,5 @@ static inline const char *empty_to_root(const char *path) {
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);

View File

@ -748,6 +748,82 @@ static int property_get_log_extra_fields(
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,
@ -965,6 +1041,8 @@ const sd_bus_vtable bus_exec_vtable[] = {
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),
@ -1794,6 +1872,146 @@ int bus_exec_context_set_transient_property(
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;

View File

@ -6,6 +6,7 @@
#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>
@ -32,6 +33,7 @@
#include "sd-messages.h"
#include "acl-util.h"
#include "af-list.h"
#include "alloc-util.h"
#if HAVE_APPARMOR
@ -41,8 +43,8 @@
#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"
@ -51,6 +53,7 @@
#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"
@ -64,6 +67,7 @@
#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"
@ -85,6 +89,7 @@
#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"
@ -1417,6 +1422,14 @@ static bool context_has_no_new_privileges(const ExecContext *c) {
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) {
@ -1725,7 +1738,7 @@ static int build_environment(
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;
@ -1873,6 +1886,14 @@ static int build_environment(
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
@ -2378,6 +2399,437 @@ fail:
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,
@ -3489,6 +3941,14 @@ static int exec_child(
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,
@ -4276,6 +4736,9 @@ void exec_context_done(ExecContext *c) {
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) {
@ -4304,6 +4767,26 @@ int exec_context_destroy_runtime_directory(const ExecContext *c, const char *run
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);
@ -5812,6 +6295,17 @@ void exec_params_clear(ExecParameters *p) {
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",

View File

@ -145,6 +145,13 @@ typedef enum ExecCleanMask {
_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. */
@ -303,6 +310,9 @@ struct ExecContext {
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) {
@ -321,11 +331,12 @@ typedef enum ExecFlags {
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
@ -345,6 +356,7 @@ struct ExecParameters {
const char *cgroup_path;
char **prefix;
const char *received_credentials;
const char *confirm_spawn;
@ -386,6 +398,7 @@ void exec_context_done(ExecContext *c);
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);
@ -418,6 +431,11 @@ void exec_params_clear(ExecParameters *p);
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_;

View File

@ -147,6 +147,8 @@ $1.LogsDirectoryMode, config_parse_mode, 0,
$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',

View File

@ -60,6 +60,7 @@
#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) {
@ -4268,6 +4269,155 @@ int config_parse_exec_directories(
}
}
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,

View File

@ -90,6 +90,8 @@ CONFIG_PARSER_PROTOTYPE(config_parse_exec_smack_process_label);
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);

View File

@ -591,6 +591,7 @@ static char** sanitize_environment(char **l) {
l,
"CACHE_DIRECTORY",
"CONFIGURATION_DIRECTORY",
"CREDENTIALS_DIRECTORY",
"EXIT_CODE",
"EXIT_STATUS",
"INVOCATION_ID",
@ -754,6 +755,7 @@ static int manager_setup_sigchld_event_source(Manager *m) {
int manager_new(UnitFileScope scope, ManagerTestRunFlags test_run_flags, Manager **_m) {
_cleanup_(manager_freep) Manager *m = NULL;
const char *e;
int r;
assert(_m);
@ -857,6 +859,13 @@ int manager_new(UnitFileScope scope, ManagerTestRunFlags test_run_flags, Manager
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;
@ -1420,6 +1429,7 @@ Manager* manager_free(Manager *m) {
for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++)
m->prefix[dt] = mfree(m->prefix[dt]);
free(m->received_credentials);
return mfree(m);
}

View File

@ -424,6 +424,7 @@ struct Manager {
/* 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. */

View File

@ -178,7 +178,8 @@ libcore = static_library(
libkmod,
libapparmor,
libselinux,
libmount])
libmount,
libacl])
systemd_sources = files('main.c')

View File

@ -869,7 +869,7 @@ static void mount_enter_dead(Mount *m, MountResult f) {
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);

View File

@ -1801,7 +1801,7 @@ static void service_enter_dead(Service *s, ServiceResult f, bool allow_restart)
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);
@ -2156,7 +2156,7 @@ static void service_enter_start(Service *s) {
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;

View File

@ -2080,7 +2080,7 @@ static void socket_enter_dead(Socket *s, SocketResult f) {
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);

View File

@ -706,7 +706,7 @@ static void swap_enter_dead(Swap *s, SwapResult f) {
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);

View File

@ -5429,6 +5429,8 @@ int unit_set_exec_params(Unit *u, ExecParameters *p) {
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;
}
@ -6123,10 +6125,15 @@ int unit_test_trigger_loaded(Unit *u) {
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) {

View File

@ -880,7 +880,7 @@ int unit_failure_action_exit_status(Unit *u);
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);

View File

@ -973,6 +973,117 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
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)

View File

@ -70,6 +70,8 @@ const ExitStatusMapping exit_status_mappings[256] = {
[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 },

View File

@ -70,6 +70,7 @@ enum {
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 */
};