From 602c74cf78921b36f2bd6cd93dbd0f47d9da8d6b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 10 Aug 2023 12:52:13 +0900 Subject: [PATCH 1/6] core: split out functions and definitions from execute.[ch] to credential.[ch] The functions and definitions related to credential consume about 1000 lines in execute.c. Let's split out them to dedicated files. --- src/core/credential.c | 958 +++++++++++++++++++++++++++++++++++++++ src/core/credential.h | 45 ++ src/core/dbus-execute.c | 1 + src/core/execute.c | 937 +------------------------------------- src/core/execute.h | 26 -- src/core/load-fragment.c | 1 + src/core/meson.build | 1 + src/core/unit.c | 1 + 8 files changed, 1008 insertions(+), 962 deletions(-) create mode 100644 src/core/credential.c create mode 100644 src/core/credential.h diff --git a/src/core/credential.c b/src/core/credential.c new file mode 100644 index 0000000000..c598decdf2 --- /dev/null +++ b/src/core/credential.c @@ -0,0 +1,958 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "acl-util.h" +#include "credential.h" +#include "creds-util.h" +#include "execute.h" +#include "fileio.h" +#include "glob-util.h" +#include "io-util.h" +#include "label-util.h" +#include "mkdir-label.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "process-util.h" +#include "random-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "tmpfile-util.h" + +ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc) { + if (!sc) + return NULL; + + free(sc->id); + free(sc->data); + return mfree(sc); +} + +ExecLoadCredential *exec_load_credential_free(ExecLoadCredential *lc) { + if (!lc) + return NULL; + + free(lc->id); + free(lc->path); + return mfree(lc); +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + exec_set_credential_hash_ops, + char, string_hash_func, string_compare_func, + ExecSetCredential, exec_set_credential_free); + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + exec_load_credential_hash_ops, + char, string_hash_func, string_compare_func, + ExecLoadCredential, exec_load_credential_free); + +bool exec_context_has_credentials(const ExecContext *c) { + assert(c); + + return !hashmap_isempty(c->set_credentials) || + !hashmap_isempty(c->load_credentials) || + !set_isempty(c->import_credentials); +} + +bool exec_context_has_encrypted_credentials(ExecContext *c) { + ExecLoadCredential *load_cred; + ExecSetCredential *set_cred; + + assert(c); + + HASHMAP_FOREACH(load_cred, c->load_credentials) + if (load_cred->encrypted) + return true; + + HASHMAP_FOREACH(set_cred, c->set_credentials) + if (set_cred->encrypted) + return true; + + return false; +} + +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 int write_credential( + int dfd, + const char *id, + const void *data, + size_t size, + uid_t uid, + gid_t gid, + bool ownership_ok) { + + _cleanup_(unlink_and_freep) char *tmp = NULL; + _cleanup_close_ int fd = -EBADF; + 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_poll = */ false); + if (r < 0) + return r; + + if (fchmod(fd, 0400) < 0) /* Take away "w" bit */ + return -errno; + + if (uid_is_valid(uid) && uid != getuid()) { + r = fd_add_uid_acl_permission(fd, uid, ACL_READ); + 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) < 0) + return -errno; + } + } + + if (renameat(dfd, tmp, dfd, id) < 0) + return -errno; + + tmp = mfree(tmp); + return 0; +} + +typedef enum CredentialSearchPath { + CREDENTIAL_SEARCH_PATH_TRUSTED, + CREDENTIAL_SEARCH_PATH_ENCRYPTED, + CREDENTIAL_SEARCH_PATH_ALL, + _CREDENTIAL_SEARCH_PATH_MAX, + _CREDENTIAL_SEARCH_PATH_INVALID = -EINVAL, +} CredentialSearchPath; + +static char **credential_search_path(const ExecParameters *params, CredentialSearchPath path) { + + _cleanup_strv_free_ char **l = NULL; + + assert(params); + assert(path >= 0 && path < _CREDENTIAL_SEARCH_PATH_MAX); + + /* Assemble a search path to find credentials in. For non-encrypted credentials, We'll look in + * /etc/credstore/ (and similar directories in /usr/lib/ + /run/). If we're looking for encrypted + * credentials, we'll look in /etc/credstore.encrypted/ (and similar dirs). */ + + if (IN_SET(path, CREDENTIAL_SEARCH_PATH_ENCRYPTED, CREDENTIAL_SEARCH_PATH_ALL)) { + if (strv_extend(&l, params->received_encrypted_credentials_directory) < 0) + return NULL; + + if (strv_extend_strv(&l, CONF_PATHS_STRV("credstore.encrypted"), /* filter_duplicates= */ true) < 0) + return NULL; + } + + if (IN_SET(path, CREDENTIAL_SEARCH_PATH_TRUSTED, CREDENTIAL_SEARCH_PATH_ALL)) { + if (params->received_credentials_directory) + if (strv_extend(&l, params->received_credentials_directory) < 0) + return NULL; + + if (strv_extend_strv(&l, CONF_PATHS_STRV("credstore"), /* filter_duplicates= */ true) < 0) + return NULL; + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *t = strv_join(l, ":"); + + log_debug("Credential search path is: %s", strempty(t)); + } + + return TAKE_PTR(l); +} + +static int maybe_decrypt_and_write_credential( + int dir_fd, + const char *id, + bool encrypted, + uid_t uid, + gid_t gid, + bool ownership_ok, + const char *data, + size_t size, + uint64_t *left) { + + _cleanup_free_ void *plaintext = NULL; + size_t add; + int r; + + if (encrypted) { + size_t plaintext_size = 0; + + r = decrypt_credential_and_warn(id, now(CLOCK_REALTIME), NULL, NULL, data, size, + &plaintext, &plaintext_size); + if (r < 0) + return r; + + data = plaintext; + size = plaintext_size; + } + + add = strlen(id) + size; + if (add > *left) + return -E2BIG; + + r = write_credential(dir_fd, id, data, size, uid, gid, ownership_ok); + if (r < 0) + return log_debug_errno(r, "Failed to write credential '%s': %m", id); + + *left -= add; + return 0; +} + +static int load_credential_glob( + const char *path, + bool encrypted, + char **search_path, + ReadFullFileFlags flags, + int write_dfd, + uid_t uid, + gid_t gid, + bool ownership_ok, + uint64_t *left) { + + int r; + + STRV_FOREACH(d, search_path) { + _cleanup_globfree_ glob_t pglob = {}; + _cleanup_free_ char *j = NULL; + + j = path_join(*d, path); + if (!j) + return -ENOMEM; + + r = safe_glob(j, 0, &pglob); + if (r == -ENOENT) + continue; + if (r < 0) + return r; + + for (size_t n = 0; n < pglob.gl_pathc; n++) { + _cleanup_free_ char *fn = NULL; + _cleanup_(erase_and_freep) char *data = NULL; + size_t size; + + /* path is absolute, hence pass AT_FDCWD as nop dir fd here */ + r = read_full_file_full( + AT_FDCWD, + pglob.gl_pathv[n], + UINT64_MAX, + encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX, + flags, + NULL, + &data, &size); + if (r < 0) + return log_debug_errno(r, "Failed to read credential '%s': %m", + pglob.gl_pathv[n]); + + r = path_extract_filename(pglob.gl_pathv[n], &fn); + if (r < 0) + return log_debug_errno(r, "Failed to extract filename from '%s': %m", + pglob.gl_pathv[n]); + + r = maybe_decrypt_and_write_credential( + write_dfd, + fn, + encrypted, + uid, + gid, + ownership_ok, + data, size, + left); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + } + + return 0; +} + +static int load_credential( + const ExecContext *context, + const ExecParameters *params, + const char *id, + const char *path, + bool encrypted, + const char *unit, + int read_dfd, + int write_dfd, + uid_t uid, + gid_t gid, + bool ownership_ok, + uint64_t *left) { + + ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER; + _cleanup_strv_free_ char **search_path = NULL; + _cleanup_(erase_and_freep) char *data = NULL; + _cleanup_free_ char *bindname = NULL; + const char *source = NULL; + bool missing_ok = true; + size_t size, maxsz; + int r; + + assert(context); + assert(params); + assert(id); + assert(path); + assert(unit); + assert(read_dfd >= 0 || read_dfd == AT_FDCWD); + assert(write_dfd >= 0); + assert(left); + + if (read_dfd >= 0) { + /* If a directory fd is specified, then read the file directly from that dir. In this case we + * won't do AF_UNIX stuff (we simply don't want to recursively iterate down a tree of AF_UNIX + * IPC sockets). It's OK if a file vanishes here in the time we enumerate it and intend to + * open it. */ + + if (!filename_is_valid(path)) /* safety check */ + return -EINVAL; + + missing_ok = true; + source = path; + + } else if (path_is_absolute(path)) { + /* If this is an absolute path, read the data directly from it, and support AF_UNIX + * sockets */ + + if (!path_is_valid(path)) /* safety check */ + return -EINVAL; + + flags |= READ_FULL_FILE_CONNECT_SOCKET; + + /* Pass some minimal info about the unit and the credential name we are looking to acquire + * via the source socket address in case we read off an AF_UNIX socket. */ + if (asprintf(&bindname, "@%" PRIx64"/unit/%s/%s", random_u64(), unit, id) < 0) + return -ENOMEM; + + missing_ok = false; + source = path; + + } else if (credential_name_valid(path)) { + /* If this is a relative path, take it as credential name relative to the credentials + * directory 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. */ + + search_path = credential_search_path(params, CREDENTIAL_SEARCH_PATH_ALL); + if (!search_path) + return -ENOMEM; + + missing_ok = true; + } else + source = NULL; + + if (encrypted) + flags |= READ_FULL_FILE_UNBASE64; + + maxsz = encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX; + + if (search_path) { + STRV_FOREACH(d, search_path) { + _cleanup_free_ char *j = NULL; + + j = path_join(*d, path); + if (!j) + return -ENOMEM; + + r = read_full_file_full( + AT_FDCWD, j, /* path is absolute, hence pass AT_FDCWD as nop dir fd here */ + UINT64_MAX, + maxsz, + flags, + NULL, + &data, &size); + if (r != -ENOENT) + break; + } + } else if (source) + r = read_full_file_full( + read_dfd, source, + UINT64_MAX, + maxsz, + flags, + bindname, + &data, &size); + else + r = -ENOENT; + + if (r == -ENOENT && (missing_ok || hashmap_contains(context->set_credentials, id))) { + /* Make a missing inherited credential non-fatal, let's just continue. After all apps + * will get clear errors if we don't pass such a missing credential on as they + * themselves will get ENOENT when trying to read them, which should not be much + * worse than when we handle the error here and make it fatal. + * + * Also, if the source file doesn't exist, but a fallback is set via SetCredentials= + * we are fine, too. */ + log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", path); + return 0; + } + if (r < 0) + return log_debug_errno(r, "Failed to read credential '%s': %m", path); + + return maybe_decrypt_and_write_credential(write_dfd, id, encrypted, uid, gid, ownership_ok, data, size, left); +} + +struct load_cred_args { + const ExecContext *context; + const ExecParameters *params; + bool encrypted; + const char *unit; + int dfd; + uid_t uid; + gid_t gid; + bool ownership_ok; + uint64_t *left; +}; + +static int load_cred_recurse_dir_cb( + RecurseDirEvent event, + const char *path, + int dir_fd, + int inode_fd, + const struct dirent *de, + const struct statx *sx, + void *userdata) { + + struct load_cred_args *args = ASSERT_PTR(userdata); + _cleanup_free_ char *sub_id = NULL; + int r; + + if (event != RECURSE_DIR_ENTRY) + return RECURSE_DIR_CONTINUE; + + if (!IN_SET(de->d_type, DT_REG, DT_SOCK)) + return RECURSE_DIR_CONTINUE; + + sub_id = strreplace(path, "/", "_"); + if (!sub_id) + return -ENOMEM; + + if (!credential_name_valid(sub_id)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Credential would get ID %s, which is not valid, refusing", sub_id); + + if (faccessat(args->dfd, sub_id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) { + log_debug("Skipping credential with duplicated ID %s at %s", sub_id, path); + return RECURSE_DIR_CONTINUE; + } + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sub_id); + + r = load_credential( + args->context, + args->params, + sub_id, + de->d_name, + args->encrypted, + args->unit, + dir_fd, + args->dfd, + args->uid, + args->gid, + args->ownership_ok, + args->left); + if (r < 0) + return r; + + return RECURSE_DIR_CONTINUE; +} + +static int acquire_credentials( + const ExecContext *context, + const ExecParameters *params, + const char *unit, + const char *p, + uid_t uid, + gid_t gid, + bool ownership_ok) { + + uint64_t left = CREDENTIALS_TOTAL_SIZE_MAX; + _cleanup_close_ int dfd = -EBADF; + const char *ic; + ExecLoadCredential *lc; + ExecSetCredential *sc; + int r; + + assert(context); + assert(p); + + dfd = open(p, O_DIRECTORY|O_CLOEXEC); + if (dfd < 0) + return -errno; + + r = fd_acl_make_writable(dfd); /* Add the "w" bit, if we are reusing an already set up credentials dir where it was unset */ + if (r < 0) + return r; + + /* First, load credentials off disk (or acquire via AF_UNIX socket) */ + HASHMAP_FOREACH(lc, context->load_credentials) { + _cleanup_close_ int sub_fd = -EBADF; + + /* If this is an absolute path, then try to open it as a directory. If that works, then we'll + * recurse into it. If it is an absolute path but it isn't a directory, then we'll open it as + * a regular file. Finally, if it's a relative path we will use it as a credential name to + * propagate a credential passed to us from further up. */ + + if (path_is_absolute(lc->path)) { + sub_fd = open(lc->path, O_DIRECTORY|O_CLOEXEC|O_RDONLY); + if (sub_fd < 0 && !IN_SET(errno, + ENOTDIR, /* Not a directory */ + ENOENT)) /* Doesn't exist? */ + return log_debug_errno(errno, "Failed to open '%s': %m", lc->path); + } + + if (sub_fd < 0) + /* Regular file (incl. a credential passed in from higher up) */ + r = load_credential( + context, + params, + lc->id, + lc->path, + lc->encrypted, + unit, + AT_FDCWD, + dfd, + uid, + gid, + ownership_ok, + &left); + else + /* Directory */ + r = recurse_dir( + sub_fd, + /* path= */ lc->id, /* recurse_dir() will suffix the subdir paths from here to the top-level id */ + /* statx_mask= */ 0, + /* n_depth_max= */ UINT_MAX, + RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, + load_cred_recurse_dir_cb, + &(struct load_cred_args) { + .context = context, + .params = params, + .encrypted = lc->encrypted, + .unit = unit, + .dfd = dfd, + .uid = uid, + .gid = gid, + .ownership_ok = ownership_ok, + .left = &left, + }); + if (r < 0) + return r; + } + + /* Next, look for system credentials and credentials in the credentials store. Note that these do not + * override any credentials found earlier. */ + SET_FOREACH(ic, context->import_credentials) { + _cleanup_free_ char **search_path = NULL; + + search_path = credential_search_path(params, CREDENTIAL_SEARCH_PATH_TRUSTED); + if (!search_path) + return -ENOMEM; + + r = load_credential_glob( + ic, + /* encrypted = */ false, + search_path, + READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, + dfd, + uid, + gid, + ownership_ok, + &left); + if (r < 0) + return r; + + search_path = strv_free(search_path); + search_path = credential_search_path(params, CREDENTIAL_SEARCH_PATH_ENCRYPTED); + if (!search_path) + return -ENOMEM; + + r = load_credential_glob( + ic, + /* encrypted = */ true, + search_path, + READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_UNBASE64, + dfd, + uid, + gid, + ownership_ok, + &left); + if (r < 0) + return r; + } + + /* Finally, we add in literally specified credentials. If the credentials already exist, we'll not + * add them, so that they can act as a "default" if the same credential is specified multiple times. */ + HASHMAP_FOREACH(sc, context->set_credentials) { + _cleanup_(erase_and_freep) void *plaintext = NULL; + const char *data; + size_t size, add; + + /* Note that we check ahead of time here instead of relying on O_EXCL|O_CREAT later to return + * EEXIST if the credential already exists. That's because the TPM2-based decryption is kinda + * slow and involved, hence it's nice to be able to skip that if the credential already + * exists anyway. */ + if (faccessat(dfd, sc->id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) + continue; + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sc->id); + + if (sc->encrypted) { + r = decrypt_credential_and_warn(sc->id, now(CLOCK_REALTIME), NULL, NULL, sc->data, sc->size, &plaintext, &size); + if (r < 0) + return r; + + data = plaintext; + } else { + data = sc->data; + size = sc->size; + } + + add = strlen(sc->id) + size; + if (add > left) + return -E2BIG; + + r = write_credential(dfd, sc->id, data, size, uid, gid, ownership_ok); + if (r < 0) + return r; + + left -= add; + } + + r = fd_acl_make_read_only(dfd); /* Now take away the "w" bit */ + if (r < 0) + return r; + + /* 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()) { + r = fd_add_uid_acl_permission(dfd, uid, ACL_READ | ACL_EXECUTE); + 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) < 0) + return -errno; + } + } + + return 0; +} + +static int setup_credentials_internal( + const ExecContext *context, + const ExecParameters *params, + const char *unit, + 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, + gid_t gid) { + + 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 isn't, then let's bind mount + * the final version to the workspace, and make it writable, so that we can make + * changes */ + + r = mount_nofollow_verbose(LOG_DEBUG, final, workspace, NULL, MS_BIND|MS_REC, NULL); + if (r < 0) + return r; + + r = mount_nofollow_verbose(LOG_DEBUG, NULL, workspace, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ false), NULL); + if (r < 0) + return r; + + 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 */ + + r = mount_credentials_fs(workspace, CREDENTIALS_TOTAL_SIZE_MAX, /* ro= */ false); + if (r < 0) { + /* 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. */ + r = mount_nofollow_verbose(LOG_DEBUG, final, workspace, NULL, MS_BIND|MS_REC, NULL); + if (r < 0) { + if (!ERRNO_IS_PRIVILEGE(r)) + /* Propagate anything that isn't a permission problem. */ + return r; + + if (must_mount) + /* If it's not OK to use the plain directory fallback, propagate all + * errors too. */ + return r; + + /* 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; + } else { + /* Make the new bind mount writable (i.e. drop MS_RDONLY) */ + r = mount_nofollow_verbose(LOG_DEBUG, NULL, workspace, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ false), NULL); + if (r < 0) + return r; + + workspace_mounted = true; + } + } else + workspace_mounted = true; + } + + assert(!must_mount || workspace_mounted > 0); + where = workspace_mounted ? workspace : final; + + (void) label_fix_full(AT_FDCWD, where, final, 0); + + r = acquire_credentials(context, params, unit, where, uid, gid, workspace_mounted); + if (r < 0) + return r; + + if (workspace_mounted) { + bool install; + + /* Determine if we should actually install the prepared mount in the final location by bind + * mounting it there. We do so only if the mount is not established there already, and if the + * mount is actually non-empty (i.e. carries at least one credential). Not that in the best + * case we are doing all this in a mount namespace, thus no one else will see that we + * allocated a file system we are getting rid of again here. */ + if (final_mounted) + install = false; /* already installed */ + else { + r = dir_is_empty(where, /* ignore_hidden_or_backup= */ false); + if (r < 0) + return r; + + install = r == 0; /* install only if non-empty */ + } + + if (install) { + /* Make workspace read-only now, so that any bind mount we make from it defaults to + * read-only too */ + r = mount_nofollow_verbose(LOG_DEBUG, NULL, workspace, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ true), NULL); + if (r < 0) + return r; + + /* And mount it to the final place, read-only */ + r = mount_nofollow_verbose(LOG_DEBUG, workspace, final, NULL, MS_MOVE, NULL); + } else + /* Otherwise get rid of it */ + r = umount_verbose(LOG_DEBUG, workspace, MNT_DETACH|UMOUNT_NOFOLLOW); + if (r < 0) + return r; + } 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 */ + + r = path_extract_directory(final, &parent); + if (r < 0) + return r; + if (chmod(parent, 0755) < 0) + return -errno; + } + + return 0; +} + +int setup_credentials( + const ExecContext *context, + const ExecParameters *params, + const char *unit, + uid_t uid, + gid_t gid) { + + _cleanup_free_ char *p = NULL, *q = NULL; + 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, + unit, + 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, + gid); + + (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 we do this in a privately namespaced short-lived single-threaded process that + * no one else sees this should be OK to do. */ + + /* Turn off propagation from our namespace to host */ + r = mount_nofollow_verbose(LOG_DEBUG, NULL, "/dev", NULL, MS_SLAVE|MS_REC, NULL); + if (r < 0) + goto child_fail; + + r = setup_credentials_internal( + context, + params, + unit, + 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, + gid); + if (r < 0) + goto child_fail; + + _exit(EXIT_SUCCESS); + + child_fail: + _exit(EXIT_FAILURE); + } + + /* If the credentials dir is empty and not a mount point, then there's no point in having it. Let's + * try to remove it. This matters in particular if we created the dir as mount point but then didn't + * actually end up mounting anything on it. In that case we'd rather have ENOENT than EACCESS being + * seen by users when trying access this inode. */ + (void) rmdir(p); + return 0; +} diff --git a/src/core/credential.h b/src/core/credential.h new file mode 100644 index 0000000000..b1cc4ec22e --- /dev/null +++ b/src/core/credential.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include +#include +#include + +#include "hash-funcs.h" + +typedef struct ExecContext ExecContext; +typedef struct ExecParameters ExecParameters; + +/* A credential configured with LoadCredential= */ +typedef struct ExecLoadCredential { + char *id, *path; + bool encrypted; +} ExecLoadCredential; + +/* A credential configured with SetCredential= */ +typedef struct ExecSetCredential { + char *id; + bool encrypted; + void *data; + size_t size; +} ExecSetCredential; + +ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc); +DEFINE_TRIVIAL_CLEANUP_FUNC(ExecSetCredential*, exec_set_credential_free); + +ExecLoadCredential *exec_load_credential_free(ExecLoadCredential *lc); +DEFINE_TRIVIAL_CLEANUP_FUNC(ExecLoadCredential*, exec_load_credential_free); + +extern const struct hash_ops exec_set_credential_hash_ops; +extern const struct hash_ops exec_load_credential_hash_ops; + +bool exec_context_has_encrypted_credentials(ExecContext *c); +bool exec_context_has_credentials(const ExecContext *c); + +int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_root, const char *unit); +int setup_credentials( + const ExecContext *context, + const ExecParameters *params, + const char *unit, + uid_t uid, + gid_t gid); diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 395051e57f..2a32063ddd 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -9,6 +9,7 @@ #include "cap-list.h" #include "capability-util.h" #include "cpu-set-util.h" +#include "credential.h" #include "creds-util.h" #include "dbus-execute.h" #include "dbus-util.h" diff --git a/src/core/execute.c b/src/core/execute.c index 5ffb088aee..14e196d4c7 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -32,7 +31,6 @@ #include "sd-messages.h" -#include "acl-util.h" #include "af-list.h" #include "alloc-util.h" #if HAVE_APPARMOR @@ -51,7 +49,7 @@ #include "chown-recursive.h" #include "constants.h" #include "cpu-set-util.h" -#include "creds-util.h" +#include "credential.h" #include "data-fd-util.h" #include "env-file.h" #include "env-util.h" @@ -60,13 +58,11 @@ #include "execute.h" #include "exit-status.h" #include "fd-util.h" -#include "fileio.h" #include "format-util.h" #include "glob-util.h" #include "hexdecoct.h" #include "io-util.h" #include "ioprio-util.h" -#include "label-util.h" #include "lock-util.h" #include "log.h" #include "macro.h" @@ -77,16 +73,12 @@ #include "missing_ioprio.h" #include "missing_prctl.h" #include "mkdir-label.h" -#include "mount-util.h" -#include "mountpoint-util.h" #include "namespace.h" #include "parse-util.h" #include "path-util.h" #include "proc-cmdline.h" #include "process-util.h" #include "psi-util.h" -#include "random-util.h" -#include "recurse-dir.h" #include "rlimit-util.h" #include "rm-rf.h" #include "seccomp-util.h" @@ -1509,15 +1501,6 @@ static bool context_has_no_new_privileges(const ExecContext *c) { context_has_syscall_logs(c); } -bool exec_context_has_credentials(const ExecContext *context) { - - assert(context); - - return !hashmap_isempty(context->set_credentials) || - !hashmap_isempty(context->load_credentials) || - !set_isempty(context->import_credentials); -} - #if HAVE_SECCOMP static bool skip_seccomp_unavailable(const Unit* u, const char* msg) { @@ -2734,866 +2717,6 @@ fail: return r; } -static int write_credential( - int dfd, - const char *id, - const void *data, - size_t size, - uid_t uid, - gid_t gid, - bool ownership_ok) { - - _cleanup_(unlink_and_freep) char *tmp = NULL; - _cleanup_close_ int fd = -EBADF; - 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_poll = */ false); - if (r < 0) - return r; - - if (fchmod(fd, 0400) < 0) /* Take away "w" bit */ - return -errno; - - if (uid_is_valid(uid) && uid != getuid()) { - r = fd_add_uid_acl_permission(fd, uid, ACL_READ); - 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) < 0) - return -errno; - } - } - - if (renameat(dfd, tmp, dfd, id) < 0) - return -errno; - - tmp = mfree(tmp); - return 0; -} - -typedef enum CredentialSearchPath { - CREDENTIAL_SEARCH_PATH_TRUSTED, - CREDENTIAL_SEARCH_PATH_ENCRYPTED, - CREDENTIAL_SEARCH_PATH_ALL, - _CREDENTIAL_SEARCH_PATH_MAX, - _CREDENTIAL_SEARCH_PATH_INVALID = -EINVAL, -} CredentialSearchPath; - -static char **credential_search_path(const ExecParameters *params, CredentialSearchPath path) { - - _cleanup_strv_free_ char **l = NULL; - - assert(params); - assert(path >= 0 && path < _CREDENTIAL_SEARCH_PATH_MAX); - - /* Assemble a search path to find credentials in. For non-encrypted credentials, We'll look in - * /etc/credstore/ (and similar directories in /usr/lib/ + /run/). If we're looking for encrypted - * credentials, we'll look in /etc/credstore.encrypted/ (and similar dirs). */ - - if (IN_SET(path, CREDENTIAL_SEARCH_PATH_ENCRYPTED, CREDENTIAL_SEARCH_PATH_ALL)) { - if (strv_extend(&l, params->received_encrypted_credentials_directory) < 0) - return NULL; - - if (strv_extend_strv(&l, CONF_PATHS_STRV("credstore.encrypted"), /* filter_duplicates= */ true) < 0) - return NULL; - } - - if (IN_SET(path, CREDENTIAL_SEARCH_PATH_TRUSTED, CREDENTIAL_SEARCH_PATH_ALL)) { - if (params->received_credentials_directory) - if (strv_extend(&l, params->received_credentials_directory) < 0) - return NULL; - - if (strv_extend_strv(&l, CONF_PATHS_STRV("credstore"), /* filter_duplicates= */ true) < 0) - return NULL; - } - - if (DEBUG_LOGGING) { - _cleanup_free_ char *t = strv_join(l, ":"); - - log_debug("Credential search path is: %s", strempty(t)); - } - - return TAKE_PTR(l); -} - -static int maybe_decrypt_and_write_credential( - int dir_fd, - const char *id, - bool encrypted, - uid_t uid, - gid_t gid, - bool ownership_ok, - const char *data, - size_t size, - uint64_t *left) { - - _cleanup_free_ void *plaintext = NULL; - size_t add; - int r; - - if (encrypted) { - size_t plaintext_size = 0; - - r = decrypt_credential_and_warn(id, now(CLOCK_REALTIME), NULL, NULL, data, size, - &plaintext, &plaintext_size); - if (r < 0) - return r; - - data = plaintext; - size = plaintext_size; - } - - add = strlen(id) + size; - if (add > *left) - return -E2BIG; - - r = write_credential(dir_fd, id, data, size, uid, gid, ownership_ok); - if (r < 0) - return log_debug_errno(r, "Failed to write credential '%s': %m", id); - - *left -= add; - return 0; -} - -static int load_credential_glob( - const char *path, - bool encrypted, - char **search_path, - ReadFullFileFlags flags, - int write_dfd, - uid_t uid, - gid_t gid, - bool ownership_ok, - uint64_t *left) { - - int r; - - STRV_FOREACH(d, search_path) { - _cleanup_globfree_ glob_t pglob = {}; - _cleanup_free_ char *j = NULL; - - j = path_join(*d, path); - if (!j) - return -ENOMEM; - - r = safe_glob(j, 0, &pglob); - if (r == -ENOENT) - continue; - if (r < 0) - return r; - - for (size_t n = 0; n < pglob.gl_pathc; n++) { - _cleanup_free_ char *fn = NULL; - _cleanup_(erase_and_freep) char *data = NULL; - size_t size; - - /* path is absolute, hence pass AT_FDCWD as nop dir fd here */ - r = read_full_file_full( - AT_FDCWD, - pglob.gl_pathv[n], - UINT64_MAX, - encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX, - flags, - NULL, - &data, &size); - if (r < 0) - return log_debug_errno(r, "Failed to read credential '%s': %m", - pglob.gl_pathv[n]); - - r = path_extract_filename(pglob.gl_pathv[n], &fn); - if (r < 0) - return log_debug_errno(r, "Failed to extract filename from '%s': %m", - pglob.gl_pathv[n]); - - r = maybe_decrypt_and_write_credential( - write_dfd, - fn, - encrypted, - uid, - gid, - ownership_ok, - data, size, - left); - if (r == -EEXIST) - continue; - if (r < 0) - return r; - } - } - - return 0; -} - -static int load_credential( - const ExecContext *context, - const ExecParameters *params, - const char *id, - const char *path, - bool encrypted, - const char *unit, - int read_dfd, - int write_dfd, - uid_t uid, - gid_t gid, - bool ownership_ok, - uint64_t *left) { - - ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER; - _cleanup_strv_free_ char **search_path = NULL; - _cleanup_(erase_and_freep) char *data = NULL; - _cleanup_free_ char *bindname = NULL; - const char *source = NULL; - bool missing_ok = true; - size_t size, maxsz; - int r; - - assert(context); - assert(params); - assert(id); - assert(path); - assert(unit); - assert(read_dfd >= 0 || read_dfd == AT_FDCWD); - assert(write_dfd >= 0); - assert(left); - - if (read_dfd >= 0) { - /* If a directory fd is specified, then read the file directly from that dir. In this case we - * won't do AF_UNIX stuff (we simply don't want to recursively iterate down a tree of AF_UNIX - * IPC sockets). It's OK if a file vanishes here in the time we enumerate it and intend to - * open it. */ - - if (!filename_is_valid(path)) /* safety check */ - return -EINVAL; - - missing_ok = true; - source = path; - - } else if (path_is_absolute(path)) { - /* If this is an absolute path, read the data directly from it, and support AF_UNIX - * sockets */ - - if (!path_is_valid(path)) /* safety check */ - return -EINVAL; - - flags |= READ_FULL_FILE_CONNECT_SOCKET; - - /* Pass some minimal info about the unit and the credential name we are looking to acquire - * via the source socket address in case we read off an AF_UNIX socket. */ - if (asprintf(&bindname, "@%" PRIx64"/unit/%s/%s", random_u64(), unit, id) < 0) - return -ENOMEM; - - missing_ok = false; - source = path; - - } else if (credential_name_valid(path)) { - /* If this is a relative path, take it as credential name relative to the credentials - * directory 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. */ - - search_path = credential_search_path(params, CREDENTIAL_SEARCH_PATH_ALL); - if (!search_path) - return -ENOMEM; - - missing_ok = true; - } else - source = NULL; - - if (encrypted) - flags |= READ_FULL_FILE_UNBASE64; - - maxsz = encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX; - - if (search_path) { - STRV_FOREACH(d, search_path) { - _cleanup_free_ char *j = NULL; - - j = path_join(*d, path); - if (!j) - return -ENOMEM; - - r = read_full_file_full( - AT_FDCWD, j, /* path is absolute, hence pass AT_FDCWD as nop dir fd here */ - UINT64_MAX, - maxsz, - flags, - NULL, - &data, &size); - if (r != -ENOENT) - break; - } - } else if (source) - r = read_full_file_full( - read_dfd, source, - UINT64_MAX, - maxsz, - flags, - bindname, - &data, &size); - else - r = -ENOENT; - - if (r == -ENOENT && (missing_ok || hashmap_contains(context->set_credentials, id))) { - /* Make a missing inherited credential non-fatal, let's just continue. After all apps - * will get clear errors if we don't pass such a missing credential on as they - * themselves will get ENOENT when trying to read them, which should not be much - * worse than when we handle the error here and make it fatal. - * - * Also, if the source file doesn't exist, but a fallback is set via SetCredentials= - * we are fine, too. */ - log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", path); - return 0; - } - if (r < 0) - return log_debug_errno(r, "Failed to read credential '%s': %m", path); - - return maybe_decrypt_and_write_credential(write_dfd, id, encrypted, uid, gid, ownership_ok, data, size, left); -} - -struct load_cred_args { - const ExecContext *context; - const ExecParameters *params; - bool encrypted; - const char *unit; - int dfd; - uid_t uid; - gid_t gid; - bool ownership_ok; - uint64_t *left; -}; - -static int load_cred_recurse_dir_cb( - RecurseDirEvent event, - const char *path, - int dir_fd, - int inode_fd, - const struct dirent *de, - const struct statx *sx, - void *userdata) { - - struct load_cred_args *args = ASSERT_PTR(userdata); - _cleanup_free_ char *sub_id = NULL; - int r; - - if (event != RECURSE_DIR_ENTRY) - return RECURSE_DIR_CONTINUE; - - if (!IN_SET(de->d_type, DT_REG, DT_SOCK)) - return RECURSE_DIR_CONTINUE; - - sub_id = strreplace(path, "/", "_"); - if (!sub_id) - return -ENOMEM; - - if (!credential_name_valid(sub_id)) - return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Credential would get ID %s, which is not valid, refusing", sub_id); - - if (faccessat(args->dfd, sub_id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) { - log_debug("Skipping credential with duplicated ID %s at %s", sub_id, path); - return RECURSE_DIR_CONTINUE; - } - if (errno != ENOENT) - return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sub_id); - - r = load_credential( - args->context, - args->params, - sub_id, - de->d_name, - args->encrypted, - args->unit, - dir_fd, - args->dfd, - args->uid, - args->gid, - args->ownership_ok, - args->left); - if (r < 0) - return r; - - return RECURSE_DIR_CONTINUE; -} - -static int acquire_credentials( - const ExecContext *context, - const ExecParameters *params, - const char *unit, - const char *p, - uid_t uid, - gid_t gid, - bool ownership_ok) { - - uint64_t left = CREDENTIALS_TOTAL_SIZE_MAX; - _cleanup_close_ int dfd = -EBADF; - const char *ic; - ExecLoadCredential *lc; - ExecSetCredential *sc; - int r; - - assert(context); - assert(p); - - dfd = open(p, O_DIRECTORY|O_CLOEXEC); - if (dfd < 0) - return -errno; - - r = fd_acl_make_writable(dfd); /* Add the "w" bit, if we are reusing an already set up credentials dir where it was unset */ - if (r < 0) - return r; - - /* First, load credentials off disk (or acquire via AF_UNIX socket) */ - HASHMAP_FOREACH(lc, context->load_credentials) { - _cleanup_close_ int sub_fd = -EBADF; - - /* If this is an absolute path, then try to open it as a directory. If that works, then we'll - * recurse into it. If it is an absolute path but it isn't a directory, then we'll open it as - * a regular file. Finally, if it's a relative path we will use it as a credential name to - * propagate a credential passed to us from further up. */ - - if (path_is_absolute(lc->path)) { - sub_fd = open(lc->path, O_DIRECTORY|O_CLOEXEC|O_RDONLY); - if (sub_fd < 0 && !IN_SET(errno, - ENOTDIR, /* Not a directory */ - ENOENT)) /* Doesn't exist? */ - return log_debug_errno(errno, "Failed to open '%s': %m", lc->path); - } - - if (sub_fd < 0) - /* Regular file (incl. a credential passed in from higher up) */ - r = load_credential( - context, - params, - lc->id, - lc->path, - lc->encrypted, - unit, - AT_FDCWD, - dfd, - uid, - gid, - ownership_ok, - &left); - else - /* Directory */ - r = recurse_dir( - sub_fd, - /* path= */ lc->id, /* recurse_dir() will suffix the subdir paths from here to the top-level id */ - /* statx_mask= */ 0, - /* n_depth_max= */ UINT_MAX, - RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, - load_cred_recurse_dir_cb, - &(struct load_cred_args) { - .context = context, - .params = params, - .encrypted = lc->encrypted, - .unit = unit, - .dfd = dfd, - .uid = uid, - .gid = gid, - .ownership_ok = ownership_ok, - .left = &left, - }); - if (r < 0) - return r; - } - - /* Next, look for system credentials and credentials in the credentials store. Note that these do not - * override any credentials found earlier. */ - SET_FOREACH(ic, context->import_credentials) { - _cleanup_free_ char **search_path = NULL; - - search_path = credential_search_path(params, CREDENTIAL_SEARCH_PATH_TRUSTED); - if (!search_path) - return -ENOMEM; - - r = load_credential_glob( - ic, - /* encrypted = */ false, - search_path, - READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER, - dfd, - uid, - gid, - ownership_ok, - &left); - if (r < 0) - return r; - - search_path = strv_free(search_path); - search_path = credential_search_path(params, CREDENTIAL_SEARCH_PATH_ENCRYPTED); - if (!search_path) - return -ENOMEM; - - r = load_credential_glob( - ic, - /* encrypted = */ true, - search_path, - READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_UNBASE64, - dfd, - uid, - gid, - ownership_ok, - &left); - if (r < 0) - return r; - } - - /* Finally, we add in literally specified credentials. If the credentials already exist, we'll not - * add them, so that they can act as a "default" if the same credential is specified multiple times. */ - HASHMAP_FOREACH(sc, context->set_credentials) { - _cleanup_(erase_and_freep) void *plaintext = NULL; - const char *data; - size_t size, add; - - /* Note that we check ahead of time here instead of relying on O_EXCL|O_CREAT later to return - * EEXIST if the credential already exists. That's because the TPM2-based decryption is kinda - * slow and involved, hence it's nice to be able to skip that if the credential already - * exists anyway. */ - if (faccessat(dfd, sc->id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) - continue; - if (errno != ENOENT) - return log_debug_errno(errno, "Failed to test if credential %s exists: %m", sc->id); - - if (sc->encrypted) { - r = decrypt_credential_and_warn(sc->id, now(CLOCK_REALTIME), NULL, NULL, sc->data, sc->size, &plaintext, &size); - if (r < 0) - return r; - - data = plaintext; - } else { - data = sc->data; - size = sc->size; - } - - add = strlen(sc->id) + size; - if (add > left) - return -E2BIG; - - r = write_credential(dfd, sc->id, data, size, uid, gid, ownership_ok); - if (r < 0) - return r; - - left -= add; - } - - r = fd_acl_make_read_only(dfd); /* Now take away the "w" bit */ - if (r < 0) - return r; - - /* 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()) { - r = fd_add_uid_acl_permission(dfd, uid, ACL_READ | ACL_EXECUTE); - 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) < 0) - return -errno; - } - } - - return 0; -} - -static int setup_credentials_internal( - const ExecContext *context, - const ExecParameters *params, - const char *unit, - 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, - gid_t gid) { - - 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 isn't, then let's bind mount - * the final version to the workspace, and make it writable, so that we can make - * changes */ - - r = mount_nofollow_verbose(LOG_DEBUG, final, workspace, NULL, MS_BIND|MS_REC, NULL); - if (r < 0) - return r; - - r = mount_nofollow_verbose(LOG_DEBUG, NULL, workspace, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ false), NULL); - if (r < 0) - return r; - - 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 */ - - r = mount_credentials_fs(workspace, CREDENTIALS_TOTAL_SIZE_MAX, /* ro= */ false); - if (r < 0) { - /* 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. */ - r = mount_nofollow_verbose(LOG_DEBUG, final, workspace, NULL, MS_BIND|MS_REC, NULL); - if (r < 0) { - if (!ERRNO_IS_PRIVILEGE(r)) /* Propagate anything that isn't a permission problem */ - return r; - - if (must_mount) /* If we it's not OK to use the plain directory - * fallback, propagate all errors too */ - return r; - - /* 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; - } else { - /* Make the new bind mount writable (i.e. drop MS_RDONLY) */ - r = mount_nofollow_verbose(LOG_DEBUG, NULL, workspace, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ false), NULL); - if (r < 0) - return r; - - workspace_mounted = true; - } - } else - workspace_mounted = true; - } - - assert(!must_mount || workspace_mounted > 0); - where = workspace_mounted ? workspace : final; - - (void) label_fix_full(AT_FDCWD, where, final, 0); - - r = acquire_credentials(context, params, unit, where, uid, gid, workspace_mounted); - if (r < 0) - return r; - - if (workspace_mounted) { - bool install; - - /* Determine if we should actually install the prepared mount in the final location by bind - * mounting it there. We do so only if the mount is not established there already, and if the - * mount is actually non-empty (i.e. carries at least one credential). Not that in the best - * case we are doing all this in a mount namespace, thus no one else will see that we - * allocated a file system we are getting rid of again here. */ - if (final_mounted) - install = false; /* already installed */ - else { - r = dir_is_empty(where, /* ignore_hidden_or_backup= */ false); - if (r < 0) - return r; - - install = r == 0; /* install only if non-empty */ - } - - if (install) { - /* Make workspace read-only now, so that any bind mount we make from it defaults to read-only too */ - r = mount_nofollow_verbose(LOG_DEBUG, NULL, workspace, NULL, MS_BIND|MS_REMOUNT|credentials_fs_mount_flags(/* ro= */ true), NULL); - if (r < 0) - return r; - - /* And mount it to the final place, read-only */ - r = mount_nofollow_verbose(LOG_DEBUG, workspace, final, NULL, MS_MOVE, NULL); - } else - /* Otherwise get rid of it */ - r = umount_verbose(LOG_DEBUG, workspace, MNT_DETACH|UMOUNT_NOFOLLOW); - if (r < 0) - return r; - } 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 */ - - r = path_extract_directory(final, &parent); - if (r < 0) - return r; - 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, - gid_t gid) { - - _cleanup_free_ char *p = NULL, *q = NULL; - 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, - unit, - 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, - gid); - - (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 no one else sees this should be OK to do. */ - - r = mount_nofollow_verbose(LOG_DEBUG, NULL, "/dev", NULL, MS_SLAVE|MS_REC, NULL); /* Turn off propagation from our namespace to host */ - if (r < 0) - goto child_fail; - - r = setup_credentials_internal( - context, - params, - unit, - 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, - gid); - if (r < 0) - goto child_fail; - - _exit(EXIT_SUCCESS); - - child_fail: - _exit(EXIT_FAILURE); - } - - /* If the credentials dir is empty and not a mount point, then there's no point in having it. Let's - * try to remove it. This matters in particular if we created the dir as mount point but then didn't - * actually end up mounting anything on it. In that case we'd rather have ENOENT than EACCESS being - * seen by users when trying access this inode. */ - (void) rmdir(p); - return 0; -} - #if ENABLE_SMACK static int setup_smack( const Manager *manager, @@ -6268,26 +5391,6 @@ 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; -} - int exec_context_destroy_mount_ns_dir(Unit *u) { _cleanup_free_ char *p = NULL; @@ -7216,23 +6319,6 @@ int exec_context_get_clean_mask(ExecContext *c, ExecCleanMask *ret) { return 0; } -bool exec_context_has_encrypted_credentials(ExecContext *c) { - ExecLoadCredential *load_cred; - ExecSetCredential *set_cred; - - assert(c); - - HASHMAP_FOREACH(load_cred, c->load_credentials) - if (load_cred->encrypted) - return true; - - HASHMAP_FOREACH(set_cred, c->set_credentials) - if (set_cred->encrypted) - return true; - - return false; -} - void exec_status_start(ExecStatus *s, pid_t pid) { assert(s); @@ -7941,24 +7027,6 @@ 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); -} - -ExecLoadCredential *exec_load_credential_free(ExecLoadCredential *lc) { - if (!lc) - return NULL; - - free(lc->id); - free(lc->path); - return mfree(lc); -} - void exec_directory_done(ExecDirectory *d) { if (!d) return; @@ -8068,9 +7136,6 @@ ExecCleanMask exec_clean_mask_from_string(const char *s) { return 1U << t; } -DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_set_credential_hash_ops, char, string_hash_func, string_compare_func, ExecSetCredential, exec_set_credential_free); -DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_load_credential_hash_ops, char, string_hash_func, string_compare_func, ExecLoadCredential, exec_load_credential_free); - static const char* const exec_input_table[_EXEC_INPUT_MAX] = { [EXEC_INPUT_NULL] = "null", [EXEC_INPUT_TTY] = "tty", diff --git a/src/core/execute.h b/src/core/execute.h index f784c1a565..33fe77bf7c 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -176,20 +176,6 @@ typedef enum ExecCleanMask { _EXEC_CLEAN_MASK_INVALID = -EINVAL, } ExecCleanMask; -/* A credential configured with LoadCredential= */ -typedef struct ExecLoadCredential { - char *id, *path; - bool encrypted; -} ExecLoadCredential; - -/* A credential configured with SetCredential= */ -typedef struct ExecSetCredential { - char *id; - bool encrypted; - 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. */ @@ -482,15 +468,12 @@ 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); int exec_context_destroy_mount_ns_dir(Unit *u); const char* exec_context_fdname(const ExecContext *c, int fd_index); bool exec_context_may_touch_console(const ExecContext *c); bool exec_context_maintains_privileges(const ExecContext *c); -bool exec_context_has_encrypted_credentials(ExecContext *c); -bool exec_context_has_credentials(const ExecContext *context); int exec_context_get_effective_ioprio(const ExecContext *c); bool exec_context_get_effective_mount_apivfs(const ExecContext *c); @@ -526,21 +509,12 @@ 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); - -ExecLoadCredential *exec_load_credential_free(ExecLoadCredential *lc); -DEFINE_TRIVIAL_CLEANUP_FUNC(ExecLoadCredential*, exec_load_credential_free); - void exec_directory_done(ExecDirectory *d); int exec_directory_add(ExecDirectory *d, const char *path, const char *symlink); void exec_directory_sort(ExecDirectory *d); ExecCleanMask exec_clean_mask_from_string(const char *s); -extern const struct hash_ops exec_set_credential_hash_ops; -extern const struct hash_ops exec_load_credential_hash_ops; - const char* exec_output_to_string(ExecOutput i) _const_; ExecOutput exec_output_from_string(const char *s) _pure_; diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index 9d1fd0f7a7..3c931568a0 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -28,6 +28,7 @@ #include "conf-parser.h" #include "core-varlink.h" #include "cpu-set-util.h" +#include "credential.h" #include "creds-util.h" #include "env-util.h" #include "errno-list.h" diff --git a/src/core/meson.build b/src/core/meson.build index 9f1d84e3f9..045ad31a51 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -11,6 +11,7 @@ libcore_sources = files( 'bpf-socket-bind.c', 'cgroup.c', 'core-varlink.c', + 'credential.c', 'dbus-automount.c', 'dbus-cgroup.c', 'dbus-device.c', diff --git a/src/core/unit.c b/src/core/unit.c index 0451a23509..660a94399c 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -20,6 +20,7 @@ #include "cgroup-util.h" #include "chase.h" #include "core-varlink.h" +#include "credential.h" #include "dbus-unit.h" #include "dbus.h" #include "dropin.h" From 7d202fb35d40ea87d3b4b8e7e415821d8ad3669b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 12 Aug 2023 15:06:43 +0900 Subject: [PATCH 2/6] core/credential: split out unit_add_default_credential_dependencies() No functional change, just refactoring. --- src/core/credential.c | 53 +++++++++++++++++++++++++++++++++++++++---- src/core/credential.h | 3 +++ src/core/unit.c | 25 ++++---------------- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/core/credential.c b/src/core/credential.c index c598decdf2..b8b8b4edaa 100644 --- a/src/core/credential.c +++ b/src/core/credential.c @@ -72,18 +72,63 @@ bool exec_context_has_encrypted_credentials(ExecContext *c) { return false; } -int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_prefix, const char *unit) { - _cleanup_free_ char *p = NULL; +static int get_credential_directory( + const char *runtime_prefix, + const char *unit, + char **ret) { - assert(c); + char *p; - if (!runtime_prefix || !unit) + assert(ret); + + if (!runtime_prefix || !unit) { + *ret = NULL; return 0; + } p = path_join(runtime_prefix, "credentials", unit); if (!p) return -ENOMEM; + *ret = p; + return 1; +} + +int unit_add_default_credential_dependencies(Unit *u, const ExecContext *c) { + _cleanup_free_ char *p = NULL, *m = NULL; + int r; + + assert(u); + assert(c); + + if (!exec_context_has_credentials(c)) + return 0; + + /* Let's make sure the credentials directory of this service is unmounted *after* the service itself + * shuts down. This only matters if mount namespacing is not used for the service, and hence the + * credentials mount appears on the host. */ + + r = get_credential_directory(u->manager->prefix[EXEC_DIRECTORY_RUNTIME], u->id, &p); + if (r <= 0) + return r; + + r = unit_name_from_path(p, ".mount", &m); + if (r < 0) + return r; + + return unit_add_dependency_by_name(u, UNIT_AFTER, m, /* add_reference= */ true, UNIT_DEPENDENCY_FILE); +} + +int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_prefix, const char *unit) { + _cleanup_free_ char *p = NULL; + int r; + + assert(c); + + r = get_credential_directory(runtime_prefix, unit, &p); + if (r <= 0) + return r; + /* 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); diff --git a/src/core/credential.h b/src/core/credential.h index b1cc4ec22e..54155f515b 100644 --- a/src/core/credential.h +++ b/src/core/credential.h @@ -9,6 +9,7 @@ typedef struct ExecContext ExecContext; typedef struct ExecParameters ExecParameters; +typedef struct Unit Unit; /* A credential configured with LoadCredential= */ typedef struct ExecLoadCredential { @@ -36,6 +37,8 @@ extern const struct hash_ops exec_load_credential_hash_ops; bool exec_context_has_encrypted_credentials(ExecContext *c); bool exec_context_has_credentials(const ExecContext *c); +int unit_add_default_credential_dependencies(Unit *u, const ExecContext *c); + int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_root, const char *unit); int setup_credentials( const ExecContext *context, diff --git a/src/core/unit.c b/src/core/unit.c index 660a94399c..1fc5ae03dc 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -1376,31 +1376,16 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { r = unit_add_two_dependencies_by_name(u, UNIT_AFTER, UNIT_REQUIRES, varlink_socket_unit, true, UNIT_DEPENDENCY_FILE); if (r < 0) return r; - } else + } else { r = unit_add_dependency_by_name(u, UNIT_AFTER, SPECIAL_JOURNALD_SOCKET, true, UNIT_DEPENDENCY_FILE); - if (r < 0) - return r; - - if (exec_context_has_credentials(c) && u->manager->prefix[EXEC_DIRECTORY_RUNTIME]) { - _cleanup_free_ char *p = NULL, *m = NULL; - - /* Let's make sure the credentials directory of this service is unmounted *after* the service - * itself shuts down. This only matters if mount namespacing is not used for the service, and - * hence the credentials mount appears on the host. */ - - p = path_join(u->manager->prefix[EXEC_DIRECTORY_RUNTIME], "credentials", u->id); - if (!p) - return -ENOMEM; - - r = unit_name_from_path(p, ".mount", &m); - if (r < 0) - return r; - - r = unit_add_dependency_by_name(u, UNIT_AFTER, m, /* add_reference= */ true, UNIT_DEPENDENCY_FILE); if (r < 0) return r; } + r = unit_add_default_credential_dependencies(u, c); + if (r < 0) + return r; + return 0; } From fbaf3b23ae4aa79110ebd37aada70ce6a044c692 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 12 Aug 2023 15:25:03 +0900 Subject: [PATCH 3/6] core: set $CREDENTIALS_DIRECTORY only when we set up credentials It is not necessary to set the environment variable on an execution step we do not support credentials. --- src/core/execute.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/execute.c b/src/core/execute.c index 14e196d4c7..78d8988e8b 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -2041,7 +2041,9 @@ static int build_environment( our_env[n_env++] = x; } - if (exec_context_has_credentials(c) && p->prefix[EXEC_DIRECTORY_RUNTIME]) { + if (exec_context_has_credentials(c) && + p->prefix[EXEC_DIRECTORY_RUNTIME] && + FLAGS_SET(p->flags, EXEC_WRITE_CREDENTIALS)) { x = strjoin("CREDENTIALS_DIRECTORY=", p->prefix[EXEC_DIRECTORY_RUNTIME], "/credentials/", u->id); if (!x) return -ENOMEM; From 7241b9cd72d6e6079d5140cf24c34e78d3cf43cc Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Thu, 10 Aug 2023 15:55:25 +0900 Subject: [PATCH 4/6] core/credential: make setup_credentials() return path to credentials directory Then, we can reuse the path when building environment variables and setting up mount namespace. No functional change, just refactoring. --- src/core/credential.c | 10 ++++++++-- src/core/credential.h | 3 ++- src/core/execute.c | 25 +++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/core/credential.c b/src/core/credential.c index b8b8b4edaa..7c9d6f6a9b 100644 --- a/src/core/credential.c +++ b/src/core/credential.c @@ -876,16 +876,20 @@ int setup_credentials( const ExecParameters *params, const char *unit, uid_t uid, - gid_t gid) { + gid_t gid, + char **ret_path) { _cleanup_free_ char *p = NULL, *q = NULL; int r; assert(context); assert(params); + assert(ret_path); - if (!exec_context_has_credentials(context)) + if (!exec_context_has_credentials(context)) { + *ret_path = NULL; return 0; + } if (!params->prefix[EXEC_DIRECTORY_RUNTIME]) return -EINVAL; @@ -999,5 +1003,7 @@ int setup_credentials( * actually end up mounting anything on it. In that case we'd rather have ENOENT than EACCESS being * seen by users when trying access this inode. */ (void) rmdir(p); + + *ret_path = TAKE_PTR(p); return 0; } diff --git a/src/core/credential.h b/src/core/credential.h index 54155f515b..2afd88dfc5 100644 --- a/src/core/credential.h +++ b/src/core/credential.h @@ -45,4 +45,5 @@ int setup_credentials( const ExecParameters *params, const char *unit, uid_t uid, - gid_t gid); + gid_t gid, + char **ret_path); diff --git a/src/core/execute.c b/src/core/execute.c index 78d8988e8b..9c900095ce 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -1864,6 +1864,7 @@ static int build_environment( dev_t journal_stream_dev, ino_t journal_stream_ino, const char *memory_pressure_path, + const char *creds_path, char ***ret) { _cleanup_strv_free_ char **our_env = NULL; @@ -2041,10 +2042,8 @@ static int build_environment( our_env[n_env++] = x; } - if (exec_context_has_credentials(c) && - p->prefix[EXEC_DIRECTORY_RUNTIME] && - FLAGS_SET(p->flags, EXEC_WRITE_CREDENTIALS)) { - x = strjoin("CREDENTIALS_DIRECTORY=", p->prefix[EXEC_DIRECTORY_RUNTIME], "/credentials/", u->id); + if (creds_path) { + x = strjoin("CREDENTIALS_DIRECTORY=", creds_path); if (!x) return -ENOMEM; @@ -3112,12 +3111,13 @@ static int apply_mount_namespace( const ExecParameters *params, ExecRuntime *runtime, const char *memory_pressure_path, + const char *creds_path, char **error_path) { _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; _cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL, **read_write_paths_cleanup = NULL; - _cleanup_free_ char *creds_path = NULL, *incoming_dir = NULL, *propagate_dir = NULL, + _cleanup_free_ char *incoming_dir = NULL, *propagate_dir = NULL, *extension_dir = NULL, *host_os_release_stage = NULL; const char *root_dir = NULL, *root_image = NULL, *tmp_dir = NULL, *var_tmp_dir = NULL; char **read_write_paths; @@ -3219,14 +3219,6 @@ static int apply_mount_namespace( if (context->mount_propagation_flag == MS_SHARED) log_unit_debug(u, "shared mount propagation hidden by other fs namespacing unit settings: ignoring"); - if (exec_context_has_credentials(context) && - params->prefix[EXEC_DIRECTORY_RUNTIME] && - FLAGS_SET(params->flags, EXEC_WRITE_CREDENTIALS)) { - creds_path = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials", u->id); - if (!creds_path) - return -ENOMEM; - } - if (params->runtime_scope == RUNTIME_SCOPE_SYSTEM) { propagate_dir = path_join("/run/systemd/propagate/", u->id); if (!propagate_dir) @@ -3948,7 +3940,7 @@ static int exec_child( int r, ngids = 0, exec_fd; _cleanup_free_ gid_t *supplementary_gids = NULL; const char *username = NULL, *groupname = NULL; - _cleanup_free_ char *home_buffer = NULL, *memory_pressure_path = NULL; + _cleanup_free_ char *home_buffer = NULL, *memory_pressure_path = NULL, *creds_path = NULL; const char *home = NULL, *shell = NULL; char **final_argv = NULL; dev_t journal_stream_dev = 0; @@ -4429,7 +4421,7 @@ static int exec_child( } if (FLAGS_SET(params->flags, EXEC_WRITE_CREDENTIALS)) { - r = setup_credentials(context, params, unit->id, uid, gid); + r = setup_credentials(context, params, unit->id, uid, gid, &creds_path); if (r < 0) { *exit_status = EXIT_CREDENTIALS; return log_unit_error_errno(unit, r, "Failed to set up credentials: %m"); @@ -4449,6 +4441,7 @@ static int exec_child( journal_stream_dev, journal_stream_ino, memory_pressure_path, + creds_path, &our_env); if (r < 0) { *exit_status = EXIT_MEMORY; @@ -4642,7 +4635,7 @@ static int exec_child( if (needs_mount_namespace) { _cleanup_free_ char *error_path = NULL; - r = apply_mount_namespace(unit, command->flags, context, params, runtime, memory_pressure_path, &error_path); + r = apply_mount_namespace(unit, command->flags, context, params, runtime, memory_pressure_path, creds_path, &error_path); if (r < 0) { *exit_status = EXIT_NAMESPACE; return log_unit_error_errno(unit, r, "Failed to set up mount namespacing%s%s: %m", From 94fe4cf2557d1f70f20ee02d32f4c2ae6bc1fb3f Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 12 Aug 2023 15:18:41 +0900 Subject: [PATCH 5/6] core: do not leak mount for credentials directory if mount namespace is enabled Since kernel v5.2, open_tree() and move_mount() are added. If a service loads or sets credentials, then let's try to clone the mount that contains credentials with open_tree(), then mount it after a (private) mount namespace is initialized for the service. Then, we can setup a mount for credentials directory without leaking it to the main shared mount namespace. With this change, the credentials for services that request their own private mount namespace become much much safer. And, the number of mount events triggered by setting up credential directories can be decreased. Unfortunately, this does not 'fix' the original issue #25527, as the reported service does not requests private mount namespace, but the situation should be better now. --- src/core/credential.c | 70 +++++++++++++++++++++++++++++++++++---- src/core/credential.h | 3 +- src/core/execute.c | 22 ++++++++++-- src/core/namespace.c | 34 ++++++++++++++----- src/core/namespace.h | 1 + src/test/test-namespace.c | 1 + src/test/test-ns.c | 1 + 7 files changed, 115 insertions(+), 17 deletions(-) diff --git a/src/core/credential.c b/src/core/credential.c index 7c9d6f6a9b..d9a88004c5 100644 --- a/src/core/credential.c +++ b/src/core/credential.c @@ -10,6 +10,7 @@ #include "glob-util.h" #include "io-util.h" #include "label-util.h" +#include "missing_syscall.h" #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" @@ -17,6 +18,7 @@ #include "random-util.h" #include "recurse-dir.h" #include "rm-rf.h" +#include "socket-util.h" #include "tmpfile-util.h" ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc) { @@ -730,7 +732,8 @@ static int setup_credentials_internal( 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, - gid_t gid) { + gid_t gid, + int *ret_mount_fd) { 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 */ @@ -848,6 +851,27 @@ static int setup_credentials_internal( if (r < 0) return r; + if (ret_mount_fd) { + _cleanup_close_ int mount_fd = -EBADF; + + r = mount_fd = RET_NERRNO(open_tree(AT_FDCWD, workspace, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC)); + if (r >= 0) { + /* The workspace is already cloned in the above, and not necessary + * anymore. Even though the workspace is unmounted when the short-lived + * child process exits, let's explicitly unmount it here for safety. */ + r = umount_verbose(LOG_DEBUG, workspace, MNT_DETACH|UMOUNT_NOFOLLOW); + if (r < 0) + return r; + + *ret_mount_fd = TAKE_FD(mount_fd); + return 0; + } + + /* Old kernel? Unprivileged? */ + if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r)) + return r; + } + /* And mount it to the final place, read-only */ r = mount_nofollow_verbose(LOG_DEBUG, workspace, final, NULL, MS_MOVE, NULL); } else @@ -868,6 +892,8 @@ static int setup_credentials_internal( return -errno; } + if (ret_mount_fd) + *ret_mount_fd = -EBADF; return 0; } @@ -877,17 +903,22 @@ int setup_credentials( const char *unit, uid_t uid, gid_t gid, - char **ret_path) { + char **ret_path, + int *ret_mount_fd) { + _cleanup_close_pair_ int socket_pair[2] = PIPE_EBADF; _cleanup_free_ char *p = NULL, *q = NULL; + _cleanup_close_ int mount_fd = -EBADF; int r; assert(context); assert(params); assert(ret_path); + assert(ret_mount_fd); if (!exec_context_has_credentials(context)) { *ret_path = NULL; + *ret_mount_fd = -EBADF; return 0; } @@ -912,7 +943,12 @@ int setup_credentials( if (r < 0 && r != -EEXIST) return r; - r = safe_fork("(sd-mkdcreds)", FORK_DEATHSIG|FORK_WAIT|FORK_NEW_MOUNTNS, NULL); + if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, socket_pair) < 0) + return -errno; + + r = safe_fork_full("(sd-mkdcreds)", + NULL, &socket_pair[1], 1, + FORK_DEATHSIG|FORK_CLOSE_ALL_FDS|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_REOPEN_LOG, NULL); if (r < 0) { _cleanup_free_ char *t = NULL, *u = NULL; @@ -948,14 +984,15 @@ int setup_credentials( 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, - gid); + gid, + NULL); (void) rmdir(u); /* remove the workspace again if we can. */ if (r < 0) return r; - } else if (r == 0) { + } else if (r == 0) { /* child */ /* 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 @@ -974,6 +1011,8 @@ int setup_credentials( * given that we do this in a privately namespaced short-lived single-threaded process that * no one else sees this should be OK to do. */ + _cleanup_close_ int fd = -EBADF; + /* Turn off propagation from our namespace to host */ r = mount_nofollow_verbose(LOG_DEBUG, NULL, "/dev", NULL, MS_SLAVE|MS_REC, NULL); if (r < 0) @@ -988,7 +1027,14 @@ int setup_credentials( 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, - gid); + gid, + &fd); + if (r < 0) + goto child_fail; + + r = send_one_fd_iov(socket_pair[1], fd, + &IOVEC_MAKE((int[]) { fd >= 0 }, sizeof(int)), 1, + MSG_DONTWAIT); if (r < 0) goto child_fail; @@ -996,6 +1042,17 @@ int setup_credentials( child_fail: _exit(EXIT_FAILURE); + + } else { /* parent */ + + int ret; + struct iovec iov = IOVEC_MAKE(&ret, sizeof(int)); + + r = receive_one_fd_iov(socket_pair[0], &iov, 1, MSG_DONTWAIT, &mount_fd); + if (r < 0) + return r; + if (ret > 0 && mount_fd < 0) + return -EIO; } /* If the credentials dir is empty and not a mount point, then there's no point in having it. Let's @@ -1005,5 +1062,6 @@ int setup_credentials( (void) rmdir(p); *ret_path = TAKE_PTR(p); + *ret_mount_fd = TAKE_FD(mount_fd); return 0; } diff --git a/src/core/credential.h b/src/core/credential.h index 2afd88dfc5..4b936b32d6 100644 --- a/src/core/credential.h +++ b/src/core/credential.h @@ -46,4 +46,5 @@ int setup_credentials( const char *unit, uid_t uid, gid_t gid, - char **ret_path); + char **ret_path, + int *ret_mount_fd); diff --git a/src/core/execute.c b/src/core/execute.c index 9c900095ce..eb452a45ec 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,7 @@ #include "missing_fs.h" #include "missing_ioprio.h" #include "missing_prctl.h" +#include "missing_syscall.h" #include "mkdir-label.h" #include "namespace.h" #include "parse-util.h" @@ -3112,6 +3114,7 @@ static int apply_mount_namespace( ExecRuntime *runtime, const char *memory_pressure_path, const char *creds_path, + int creds_fd, char **error_path) { _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; @@ -3287,6 +3290,7 @@ static int apply_mount_namespace( tmp_dir, var_tmp_dir, creds_path, + creds_fd, context->log_namespace, context->mount_propagation_flag, &verity, @@ -3971,6 +3975,7 @@ static int exec_child( int ngids_after_pam = 0; _cleanup_free_ int *fds = NULL; _cleanup_strv_free_ char **fdnames = NULL; + _cleanup_close_ int creds_fd = -EBADF; assert(unit); assert(command); @@ -4421,7 +4426,7 @@ static int exec_child( } if (FLAGS_SET(params->flags, EXEC_WRITE_CREDENTIALS)) { - r = setup_credentials(context, params, unit->id, uid, gid, &creds_path); + r = setup_credentials(context, params, unit->id, uid, gid, &creds_path, &creds_fd); if (r < 0) { *exit_status = EXIT_CREDENTIALS; return log_unit_error_errno(unit, r, "Failed to set up credentials: %m"); @@ -4635,7 +4640,7 @@ static int exec_child( if (needs_mount_namespace) { _cleanup_free_ char *error_path = NULL; - r = apply_mount_namespace(unit, command->flags, context, params, runtime, memory_pressure_path, creds_path, &error_path); + r = apply_mount_namespace(unit, command->flags, context, params, runtime, memory_pressure_path, creds_path, creds_fd, &error_path); if (r < 0) { *exit_status = EXIT_NAMESPACE; return log_unit_error_errno(unit, r, "Failed to set up mount namespacing%s%s: %m", @@ -4643,6 +4648,19 @@ static int exec_child( } } + if (creds_fd >= 0) { + assert(creds_path); + + /* When a mount namespace is not requested, then the target directory may not exist yet. + * Here, we ignore the failure, as if it fails, the subsequent move_mount() will fail. */ + (void) mkdir_p_label(creds_path, 0755); + + if (move_mount(creds_fd, "", AT_FDCWD, creds_path, MOVE_MOUNT_F_EMPTY_PATH) < 0) { + *exit_status = EXIT_CREDENTIALS; + return log_unit_error_errno(unit, errno, "Failed to mount credentials directory on %s: %m", creds_path); + } + } + if (needs_sandboxing) { r = apply_protect_hostname(unit, context, exit_status); if (r < 0) diff --git a/src/core/namespace.c b/src/core/namespace.c index 51b5aad9c9..86406007ad 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -74,7 +74,8 @@ typedef enum MountMode { EXTENSION_DIRECTORIES, /* Bind-mounted outside the root directory, and used by subsequent mounts */ EXTENSION_IMAGES, /* Mounted outside the root directory, and used by subsequent mounts */ MQUEUEFS, - READWRITE_IMPLICIT, /* Should have the lowest priority. */ + READWRITE_IMPLICIT, /* Should have the 2nd lowest priority. */ + MKDIR, /* Should have the lowest priority. */ _MOUNT_MODE_MAX, } MountMode; @@ -227,6 +228,7 @@ static const char * const mount_mode_table[_MOUNT_MODE_MAX] = { [EXEC] = "exec", [NOEXEC] = "noexec", [MQUEUEFS] = "mqueuefs", + [MKDIR] = "mkdir", }; /* Helper struct for naming simplicity and reusability */ @@ -1551,6 +1553,12 @@ static int apply_one_mount( case OVERLAY_MOUNT: return mount_overlay(m); + case MKDIR: + r = mkdir_p_label(mount_entry_path(m), 0755); + if (r < 0) + return r; + return 1; + default: assert_not_reached(); } @@ -2017,6 +2025,7 @@ int setup_namespace( const char* tmp_dir, const char* var_tmp_dir, const char *creds_path, + int creds_fd, const char *log_namespace, unsigned long mount_propagation_flag, VeritySettings *verity, @@ -2339,13 +2348,22 @@ int setup_namespace( .flags = MS_NODEV|MS_STRICTATIME|MS_NOSUID|MS_NOEXEC, }; - *(m++) = (MountEntry) { - .path_const = creds_path, - .mode = BIND_MOUNT, - .read_only = true, - .source_const = creds_path, - .ignore = true, - }; + /* If we have mount fd for credentials directory, then it will be mounted after + * namespace is set up. So, here we only create the mount point. */ + + if (creds_fd < 0) + *(m++) = (MountEntry) { + .path_const = creds_path, + .mode = BIND_MOUNT, + .read_only = true, + .source_const = creds_path, + .ignore = true, + }; + else + *(m++) = (MountEntry) { + .path_const = creds_path, + .mode = MKDIR, + }; } else { /* If our service has no credentials store configured, then make the whole * credentials tree inaccessible wholesale. */ diff --git a/src/core/namespace.h b/src/core/namespace.h index b6132154c5..b8615cbd98 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -122,6 +122,7 @@ int setup_namespace( const char *tmp_dir, const char *var_tmp_dir, const char *creds_path, + int creds_fd, const char *log_namespace, unsigned long mount_propagation_flag, VeritySettings *verity, diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index 25aafc35ca..436a784e86 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -194,6 +194,7 @@ TEST(protect_kernel_logs) { NULL, NULL, NULL, + -EBADF, NULL, 0, NULL, diff --git a/src/test/test-ns.c b/src/test/test-ns.c index 77afd2f6b9..56f3de83b6 100644 --- a/src/test/test-ns.c +++ b/src/test/test-ns.c @@ -96,6 +96,7 @@ int main(int argc, char *argv[]) { tmp_dir, var_tmp_dir, NULL, + -EBADF, NULL, 0, NULL, From 9ae3624889b98f75efa6fd0c5f4b4de3eaf328d4 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sat, 12 Aug 2023 22:08:47 +0900 Subject: [PATCH 6/6] test-execute: add tests for credentials directory with mount namespace This also adds cases that open_tree() and move_mount() are filtered, to emulate old kernel behavior. --- src/test/test-execute.c | 4 ++++ .../exec-load-credential-with-mount-namespace.service | 9 +++++++++ .../exec-load-credential-with-seccomp.service | 9 +++++++++ .../exec-set-credential-with-mount-namespace.service | 9 +++++++++ .../exec-set-credential-with-seccomp.service | 9 +++++++++ 5 files changed, 40 insertions(+) create mode 100644 test/test-execute/exec-load-credential-with-mount-namespace.service create mode 100644 test/test-execute/exec-load-credential-with-seccomp.service create mode 100644 test/test-execute/exec-set-credential-with-mount-namespace.service create mode 100644 test/test-execute/exec-set-credential-with-seccomp.service diff --git a/src/test/test-execute.c b/src/test/test-execute.c index 0be66c2c7b..847415b9ae 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -282,7 +282,11 @@ static void test_exec_cpuaffinity(Manager *m) { static void test_exec_credentials(Manager *m) { test(m, "exec-set-credential.service", 0, CLD_EXITED); + test(m, "exec-set-credential-with-mount-namespace.service", 0, CLD_EXITED); + test(m, "exec-set-credential-with-seccomp.service", 0, CLD_EXITED); test(m, "exec-load-credential.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_CREDENTIALS, CLD_EXITED); + test(m, "exec-load-credential-with-mount-namespace.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_CREDENTIALS, CLD_EXITED); + test(m, "exec-load-credential-with-seccomp.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_CREDENTIALS, CLD_EXITED); test(m, "exec-credentials-dir-specifier.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_CREDENTIALS, CLD_EXITED); } diff --git a/test/test-execute/exec-load-credential-with-mount-namespace.service b/test/test-execute/exec-load-credential-with-mount-namespace.service new file mode 100644 index 0000000000..fd71cf6717 --- /dev/null +++ b/test/test-execute/exec-load-credential-with-mount-namespace.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Test for LoadCredential= + +[Service] +ExecStart=/bin/sh -x -c 'test "$$(cat %d/test-execute.load-credential)" = "foo"' +Type=oneshot +LoadCredential=test-execute.load-credential +PrivateMounts=yes diff --git a/test/test-execute/exec-load-credential-with-seccomp.service b/test/test-execute/exec-load-credential-with-seccomp.service new file mode 100644 index 0000000000..67303f2713 --- /dev/null +++ b/test/test-execute/exec-load-credential-with-seccomp.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Test for LoadCredential= + +[Service] +ExecStart=/bin/sh -x -c 'test "$$(cat %d/test-execute.load-credential)" = "foo"' +Type=oneshot +LoadCredential=test-execute.load-credential +SystemCallFilter=~open_tree move_mount diff --git a/test/test-execute/exec-set-credential-with-mount-namespace.service b/test/test-execute/exec-set-credential-with-mount-namespace.service new file mode 100644 index 0000000000..67d15e5dbb --- /dev/null +++ b/test/test-execute/exec-set-credential-with-mount-namespace.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Test for SetCredential= + +[Service] +ExecStart=/bin/sh -x -c 'test "$$(cat %d/test-execute.set-credential)" = "hoge"' +Type=oneshot +SetCredential=test-execute.set-credential:hoge +PrivateMounts=yes diff --git a/test/test-execute/exec-set-credential-with-seccomp.service b/test/test-execute/exec-set-credential-with-seccomp.service new file mode 100644 index 0000000000..778777b947 --- /dev/null +++ b/test/test-execute/exec-set-credential-with-seccomp.service @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Unit] +Description=Test for SetCredential= + +[Service] +ExecStart=/bin/sh -x -c 'test "$$(cat %d/test-execute.set-credential)" = "hoge"' +Type=oneshot +SetCredential=test-execute.set-credential:hoge +SystemCallFilter=~open_tree move_mount