From 2899fb024f066f1cb14989fb470e188de7d6dc88 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Jul 2020 15:24:54 +0200 Subject: [PATCH 01/11] rm-rf: add new flag REMOVE_CHMOD When removing a directory tree as unprivileged user we might encounter files owned by us but not deletable since the containing directory might have the "r" bit missing in its access mode. Let's try to deal with this: optionally if we get EACCES try to set the bit and see if it works then. --- src/basic/rm-rf.c | 54 ++++++++++++++++++++++++++----- src/basic/rm-rf.h | 1 + src/test/meson.build | 4 +++ src/test/test-rm-rf.c | 74 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 src/test/test-rm-rf.c diff --git a/src/basic/rm-rf.c b/src/basic/rm-rf.c index 23cdfa4696..01ff6bb331 100644 --- a/src/basic/rm-rf.c +++ b/src/basic/rm-rf.c @@ -23,6 +23,46 @@ static bool is_physical_fs(const struct statfs *sfs) { return !is_temporary_fs(sfs) && !is_cgroup_fs(sfs); } +static int unlinkat_harder( + int dfd, + const char *filename, + int unlink_flags, + RemoveFlags remove_flags) { + + struct stat st; + int r; + + /* Like unlinkat(), but tries harder: if we get EACCESS we'll try to set the r/w/x bits on the + * directory. This is useful if we run unprivileged and have some files where the w bit is + * missing. */ + + if (unlinkat(dfd, filename, unlink_flags) >= 0) + return 0; + if (errno != EACCES || !FLAGS_SET(remove_flags, REMOVE_CHMOD)) + return -errno; + + if (fstat(dfd, &st) < 0) + return -errno; + if (!S_ISDIR(st.st_mode)) + return -ENOTDIR; + if ((st.st_mode & 0700) == 0700) /* Already set? */ + return -EACCES; /* original error */ + if (st.st_uid != geteuid()) /* this only works if the UID matches ours */ + return -EACCES; + + if (fchmod(dfd, (st.st_mode | 0700) & 07777) < 0) + return -errno; + + if (unlinkat(dfd, filename, unlink_flags) < 0) { + r = -errno; + /* Try to restore the original access mode if this didn't work */ + (void) fchmod(dfd, st.st_mode & 07777); + return r; + } + + return 0; +} + int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) { _cleanup_closedir_ DIR *d = NULL; struct dirent *de; @@ -132,17 +172,15 @@ int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev) { if (r < 0 && ret == 0) ret = r; - if (unlinkat(fd, de->d_name, AT_REMOVEDIR) < 0) { - if (ret == 0 && errno != ENOENT) - ret = -errno; - } + r = unlinkat_harder(fd, de->d_name, AT_REMOVEDIR, flags); + if (r < 0 && r != -ENOENT && ret == 0) + ret = r; } else if (!(flags & REMOVE_ONLY_DIRECTORIES)) { - if (unlinkat(fd, de->d_name, 0) < 0) { - if (ret == 0 && errno != ENOENT) - ret = -errno; - } + r = unlinkat_harder(fd, de->d_name, 0, flags); + if (r < 0 && r != -ENOENT && ret == 0) + ret = r; } } return ret; diff --git a/src/basic/rm-rf.h b/src/basic/rm-rf.h index 40cbff21c0..0edf01ee1c 100644 --- a/src/basic/rm-rf.h +++ b/src/basic/rm-rf.h @@ -11,6 +11,7 @@ typedef enum RemoveFlags { REMOVE_PHYSICAL = 1 << 2, /* If not set, only removes files on tmpfs, never physical file systems */ REMOVE_SUBVOLUME = 1 << 3, /* Drop btrfs subvolumes in the tree too */ REMOVE_MISSING_OK = 1 << 4, /* If the top-level directory is missing, ignore the ENOENT for it */ + REMOVE_CHMOD = 1 << 5, /* chmod() for write access if we cannot delete something */ } RemoveFlags; int rm_rf_children(int fd, RemoveFlags flags, struct stat *root_dev); diff --git a/src/test/meson.build b/src/test/meson.build index 132989f197..835be6466e 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -658,6 +658,10 @@ tests += [ [], []], + [['src/test/test-rm-rf.c'], + [], + []], + [['src/test/test-chase-symlinks.c'], [], [], diff --git a/src/test/test-rm-rf.c b/src/test/test-rm-rf.c new file mode 100644 index 0000000000..d6e426c0fb --- /dev/null +++ b/src/test/test-rm-rf.c @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "string-util.h" +#include "tests.h" +#include "tmpfile-util.h" + +static void test_rm_rf_chmod_inner(void) { + _cleanup_free_ char *d = NULL; + const char *x, *y; + + assert_se(getuid() != 0); + + assert_se(mkdtemp_malloc(NULL, &d) >= 0); + + x = strjoina(d, "/d"); + assert_se(mkdir(x, 0700) >= 0); + y = strjoina(x, "/f"); + assert_se(mknod(y, S_IFREG | 0600, 0) >= 0); + + assert_se(chmod(y, 0400) >= 0); + assert_se(chmod(x, 0500) >= 0); + assert_se(chmod(d, 0500) >= 0); + + assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_ROOT) == -EACCES); + + assert_se(access(d, F_OK) >= 0); + assert_se(access(x, F_OK) >= 0); + assert_se(access(y, F_OK) >= 0); + + assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_ROOT|REMOVE_CHMOD) >= 0); + + errno = 0; + assert_se(access(d, F_OK) < 0 && errno == ENOENT); +} + +static void test_rm_rf_chmod(void) { + int r; + + log_info("/* %s */", __func__); + + if (getuid() == 0) { + /* This test only works unpriv (as only then the access mask for the owning user matters), + * hence drop privs here */ + + r = safe_fork("(setresuid)", FORK_DEATHSIG|FORK_WAIT, NULL); + assert_se(r >= 0); + + if (r == 0) { + /* child */ + + assert_se(setresuid(1, 1, 1) >= 0); + + test_rm_rf_chmod_inner(); + _exit(EXIT_SUCCESS); + } + + return; + } + + test_rm_rf_chmod_inner(); +} + +int main(int argc, char **argv) { + test_setup_logging(LOG_DEBUG); + + test_rm_rf_chmod(); + + return 0; +} From 71281a7655d637bed06071e61c28a96fbf7370bb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 21 Jul 2020 22:13:12 +0200 Subject: [PATCH 02/11] acl-util: make sure acl_find_uid() initializes return parameters on success Let's follow our usual coding style and initialize return parameters on success in all cases. --- src/shared/acl-util.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shared/acl-util.c b/src/shared/acl-util.c index dd2b1efb11..641e5bda7a 100644 --- a/src/shared/acl-util.c +++ b/src/shared/acl-util.c @@ -12,12 +12,13 @@ #include "user-util.h" #include "util.h" -int acl_find_uid(acl_t acl, uid_t uid, acl_entry_t *entry) { +int acl_find_uid(acl_t acl, uid_t uid, acl_entry_t *ret_entry) { acl_entry_t i; int r; assert(acl); - assert(entry); + assert(uid_is_valid(uid)); + assert(ret_entry); for (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &i); r > 0; @@ -41,13 +42,14 @@ int acl_find_uid(acl_t acl, uid_t uid, acl_entry_t *entry) { acl_free(u); if (b) { - *entry = i; + *ret_entry = i; return 1; } } if (r < 0) return -errno; + *ret_entry = NULL; return 0; } From 9db59d928356e63f0d06bdcc528e579b92c20b89 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 21 Jul 2020 22:14:57 +0200 Subject: [PATCH 03/11] acl-util: beef up add_acls_for_user() Let's add support for controlling r/w/x bits separetely. This is useful for using it to control access to directories, where r + x shall be enabled. --- src/coredump/coredump.c | 2 +- src/journal/journald-server.c | 2 +- src/shared/acl-util.c | 23 ++++++++++++++++++----- src/shared/acl-util.h | 2 +- src/test/test-acl-util.c | 6 +++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/coredump/coredump.c b/src/coredump/coredump.c index 8b052dac26..9b7811ae54 100644 --- a/src/coredump/coredump.c +++ b/src/coredump/coredump.c @@ -186,7 +186,7 @@ static int fix_acl(int fd, uid_t uid) { return 0; /* Make sure normal users can read (but not write or delete) their own coredumps */ - r = add_acls_for_user(fd, uid); + r = fd_add_uid_acl_permission(fd, uid, /* read = */ true, /* write = */ false, /* execute = */ false); if (r < 0) return log_error_errno(r, "Failed to adjust ACL of coredump: %m"); #endif diff --git a/src/journal/journald-server.c b/src/journal/journald-server.c index 0d8e3618ee..8a8c41b7a5 100644 --- a/src/journal/journald-server.c +++ b/src/journal/journald-server.c @@ -256,7 +256,7 @@ static void server_add_acls(JournalFile *f, uid_t uid) { if (uid_for_system_journal(uid)) return; - r = add_acls_for_user(f->fd, uid); + r = fd_add_uid_acl_permission(f->fd, uid, /* read = */ true, /* write = */ false, /* execute = */ false); if (r < 0) log_warning_errno(r, "Failed to set ACL on %s, ignoring: %m", f->path); #endif diff --git a/src/shared/acl-util.c b/src/shared/acl-util.c index 641e5bda7a..02c94f9358 100644 --- a/src/shared/acl-util.c +++ b/src/shared/acl-util.c @@ -378,12 +378,21 @@ int acls_for_file(const char *path, acl_type_t type, acl_t new, acl_t *acl) { return 0; } -int add_acls_for_user(int fd, uid_t uid) { +int fd_add_uid_acl_permission( + int fd, + uid_t uid, + bool rd, + bool wr, + bool ex) { + _cleanup_(acl_freep) acl_t acl = NULL; acl_permset_t permset; acl_entry_t entry; int r; + /* Adds an ACL entry for the specified file to allow the indicated access to the specified + * user. Operates purely incrementally. */ + assert(fd >= 0); assert(uid_is_valid(uid)); @@ -399,10 +408,14 @@ int add_acls_for_user(int fd, uid_t uid) { return -errno; } - /* We do not recalculate the mask unconditionally here, so that the fchmod() mask above stays - * intact. */ - if (acl_get_permset(entry, &permset) < 0 || - acl_add_perm(permset, ACL_READ) < 0) + if (acl_get_permset(entry, &permset) < 0) + return -errno; + + if (rd && acl_add_perm(permset, ACL_READ) < 0) + return -errno; + if (wr && acl_add_perm(permset, ACL_WRITE) < 0) + return -errno; + if (ex && acl_add_perm(permset, ACL_EXECUTE) < 0) return -errno; r = calc_acl_mask_if_needed(&acl); diff --git a/src/shared/acl-util.h b/src/shared/acl-util.h index 10b2a3d9f0..ace0fe0955 100644 --- a/src/shared/acl-util.h +++ b/src/shared/acl-util.h @@ -15,7 +15,7 @@ int add_base_acls_if_needed(acl_t *acl_p, const char *path); int acl_search_groups(const char* path, char ***ret_groups); int parse_acl(const char *text, acl_t *acl_access, acl_t *acl_default, bool want_mask); int acls_for_file(const char *path, acl_type_t type, acl_t new, acl_t *acl); -int add_acls_for_user(int fd, uid_t uid); +int fd_add_uid_acl_permission(int fd, uid_t uid, bool rd, bool wr, bool ex); /* acl_free takes multiple argument types. * Multiple cleanup functions are necessary. */ diff --git a/src/test/test-acl-util.c b/src/test/test-acl-util.c index 9f0e594e67..9a3db3c8e3 100644 --- a/src/test/test-acl-util.c +++ b/src/test/test-acl-util.c @@ -41,8 +41,8 @@ static void test_add_acls_for_user(void) { } else uid = getuid(); - r = add_acls_for_user(fd, uid); - log_info_errno(r, "add_acls_for_user(%d, "UID_FMT"): %m", fd, uid); + r = fd_add_uid_acl_permission(fd, uid, true, false, false); + log_info_errno(r, "fd_add_uid_acl_permission(%i, "UID_FMT", true, false, false): %m", fd, uid); assert_se(r >= 0); cmd = strjoina("ls -l ", fn); @@ -53,7 +53,7 @@ static void test_add_acls_for_user(void) { /* set the acls again */ - r = add_acls_for_user(fd, uid); + r = fd_add_uid_acl_permission(fd, uid, true, false, false); assert_se(r >= 0); cmd = strjoina("ls -l ", fn); From bb0c0d6f29236645c8beb7ba662b2c4e7b241407 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Jul 2020 08:49:52 +0200 Subject: [PATCH 04/11] core: add credentials logic Fixes: #15778 #16060 --- src/basic/path-util.c | 7 + src/basic/path-util.h | 2 + src/core/dbus-execute.c | 218 +++++++++++ src/core/execute.c | 498 +++++++++++++++++++++++++- src/core/execute.h | 24 +- src/core/load-fragment-gperf.gperf.m4 | 2 + src/core/load-fragment.c | 150 ++++++++ src/core/load-fragment.h | 2 + src/core/manager.c | 10 + src/core/manager.h | 1 + src/core/meson.build | 3 +- src/core/mount.c | 2 +- src/core/service.c | 4 +- src/core/socket.c | 2 +- src/core/swap.c | 2 +- src/core/unit.c | 9 +- src/core/unit.h | 2 +- src/shared/bus-unit-util.c | 111 ++++++ src/shared/exit-status.c | 2 + src/shared/exit-status.h | 1 + 20 files changed, 1039 insertions(+), 13 deletions(-) diff --git a/src/basic/path-util.c b/src/basic/path-util.c index c463ae23ab..deffa8a425 100644 --- a/src/basic/path-util.c +++ b/src/basic/path-util.c @@ -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); +} diff --git a/src/basic/path-util.h b/src/basic/path-util.h index 99c4e35782..1afbebd77f 100644 --- a/src/basic/path-util.h +++ b/src/basic/path-util.h @@ -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); diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index c96c654ff0..9042f0baa8 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -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; diff --git a/src/core/execute.c b/src/core/execute.c index d5107288a1..81829007b4 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -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", diff --git a/src/core/execute.h b/src/core/execute.h index 1ea7e51fd7..810e585fa8 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -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_; diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4 index 9a1fcca46e..45147f0d57 100644 --- a/src/core/load-fragment-gperf.gperf.m4 +++ b/src/core/load-fragment-gperf.gperf.m4 @@ -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', diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index df93fbb28f..a8eee28502 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -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, diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index ae134610b1..e90953b80f 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -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); diff --git a/src/core/manager.c b/src/core/manager.c index b7ca184081..49bf5419d8 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -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); } diff --git a/src/core/manager.h b/src/core/manager.h index 81b0c13a95..d507dc1f3b 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -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. */ diff --git a/src/core/meson.build b/src/core/meson.build index fa95108523..72a00f1955 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -178,7 +178,8 @@ libcore = static_library( libkmod, libapparmor, libselinux, - libmount]) + libmount, + libacl]) systemd_sources = files('main.c') diff --git a/src/core/mount.c b/src/core/mount.c index 337e94e90e..6e9d61ff18 100644 --- a/src/core/mount.c +++ b/src/core/mount.c @@ -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); diff --git a/src/core/service.c b/src/core/service.c index 641391752a..dec6b2c9ea 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -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; diff --git a/src/core/socket.c b/src/core/socket.c index 127195c9fe..0588a34e38 100644 --- a/src/core/socket.c +++ b/src/core/socket.c @@ -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); diff --git a/src/core/swap.c b/src/core/swap.c index 20179de2d2..83ae66c964 100644 --- a/src/core/swap.c +++ b/src/core/swap.c @@ -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); diff --git a/src/core/unit.c b/src/core/unit.c index 0e66c13294..e81ef2fd9c 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -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) { diff --git a/src/core/unit.h b/src/core/unit.h index d5e4c65989..e217cec219 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -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); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index d010d3bf3e..2ad196e824 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -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) diff --git a/src/shared/exit-status.c b/src/shared/exit-status.c index 82b1422b2d..e2b6c67953 100644 --- a/src/shared/exit-status.c +++ b/src/shared/exit-status.c @@ -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 }, diff --git a/src/shared/exit-status.h b/src/shared/exit-status.h index 9ea147c842..7ac99a4810 100644 --- a/src/shared/exit-status.h +++ b/src/shared/exit-status.h @@ -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 */ }; From bbb4e7f39f2c68c719c26c2c65f8b7b91b009e92 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Aug 2020 15:54:48 +0200 Subject: [PATCH 05/11] core: hide /run/credentials whenever namespacing is requested Ideally we would like to hide all other service's credentials for all services. That would imply for us to enable mount namespacing for all services, which is something we cannot do, both due to compatibility with the status quo ante, and because a number of services legitimately should be able to install mounts in the host hierarchy. Hence we do the second best thing, we hide the credentials automatically for all services that opt into mount namespacing otherwise. This is quite different from other mount sandboxing options: usually you have to explicitly opt into each. However, given that the credentials logic is a brand new concept we invented right here and now, and particularly security sensitive it's OK to reverse this, and by default hide credentials whenever we can (i.e. whenever mount namespacing is otherwise opt-ed in to). Long story short: if you want to hide other service's credentials, the most basic options is to just turn on PrivateMounts= and there you go, they should all be gone. --- src/core/execute.c | 8 ++++++++ src/core/mount-setup.c | 3 +++ src/core/namespace.c | 33 +++++++++++++++++++++++++++++++++ src/core/namespace.h | 1 + src/test/test-namespace.c | 1 + src/test/test-ns.c | 1 + 6 files changed, 47 insertions(+) diff --git a/src/core/execute.c b/src/core/execute.c index 81829007b4..aede50c5fe 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -3056,6 +3056,7 @@ static int apply_mount_namespace( _cleanup_strv_free_ char **empty_directories = NULL; const char *tmp_dir = NULL, *var_tmp_dir = NULL; const char *root_dir = NULL, *root_image = NULL; + _cleanup_free_ char *creds_path = NULL; NamespaceInfo ns_info; bool needs_sandboxing; BindMount *bind_mounts = NULL; @@ -3124,6 +3125,12 @@ static int apply_mount_namespace( if (context->mount_flags == 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]) { + creds_path = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials", u->id); + if (!creds_path) + return -ENOMEM; + } + r = setup_namespace(root_dir, root_image, context->root_image_options, &ns_info, context->read_write_paths, needs_sandboxing ? context->read_only_paths : NULL, @@ -3137,6 +3144,7 @@ static int apply_mount_namespace( context->n_mount_images, tmp_dir, var_tmp_dir, + creds_path, context->log_namespace, context->mount_flags, context->root_hash, context->root_hash_size, context->root_hash_path, diff --git a/src/core/mount-setup.c b/src/core/mount-setup.c index d0b707f3c5..cc43bcdc7b 100644 --- a/src/core/mount-setup.c +++ b/src/core/mount-setup.c @@ -537,6 +537,9 @@ int mount_setup(bool loaded_policy, bool leave_propagation) { (void) mkdir_label("/run/systemd", 0755); (void) mkdir_label("/run/systemd/system", 0755); + /* Make sure we have a mount point to hide in sandboxes */ + (void) mkdir_label("/run/credentials", 0755); + /* Also create /run/systemd/inaccessible nodes, so that we always have something to mount * inaccessible nodes from. If we run in a container the host might have created these for us already * in /run/host/inaccessible/. Use those if we can, since tht way we likely get access to block/char diff --git a/src/core/namespace.c b/src/core/namespace.c index 1f78d66a34..5a5095ee0a 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1270,6 +1270,7 @@ static size_t namespace_calculate_mounts( size_t n_mount_images, const char* tmp_dir, const char* var_tmp_dir, + const char *creds_path, const char* log_namespace) { size_t protect_home_cnt; @@ -1305,6 +1306,7 @@ static size_t namespace_calculate_mounts( protect_home_cnt + protect_system_cnt + (ns_info->protect_hostname ? 2 : 0) + (namespace_info_mount_apivfs(ns_info) ? ELEMENTSOF(apivfs_table) : 0) + + (creds_path ? 2 : 1) + !!log_namespace; } @@ -1389,6 +1391,7 @@ int setup_namespace( size_t n_mount_images, const char* tmp_dir, const char* var_tmp_dir, + const char *creds_path, const char *log_namespace, unsigned long mount_flags, const void *root_hash, @@ -1494,6 +1497,7 @@ int setup_namespace( n_temporary_filesystems, n_mount_images, tmp_dir, var_tmp_dir, + creds_path, log_namespace); if (n_mounts > 0) { @@ -1619,6 +1623,35 @@ int setup_namespace( }; } + if (creds_path) { + /* If our service has a credentials store configured, then bind that one in, but hide + * everything else. */ + + *(m++) = (MountEntry) { + .path_const = "/run/credentials", + .mode = TMPFS, + .read_only = true, + .options_const = "mode=0755" TMPFS_LIMITS_EMPTY_OR_ALMOST, + .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, + }; + } else { + /* If our service has no credentials store configured, then make the whole + * credentials tree inaccessible wholesale. */ + + *(m++) = (MountEntry) { + .path_const = "/run/credentials", + .mode = INACCESSIBLE, + .ignore = true, + }; + } + if (log_namespace) { _cleanup_free_ char *q; diff --git a/src/core/namespace.h b/src/core/namespace.h index e682eae794..13cc0e80cb 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -117,6 +117,7 @@ int setup_namespace( size_t n_mount_images, const char *tmp_dir, const char *var_tmp_dir, + const char *creds_path, const char *log_namespace, unsigned long mount_flags, const void *root_hash, diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index af48e69668..3972207329 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -163,6 +163,7 @@ static void test_protect_kernel_logs(void) { NULL, NULL, NULL, + NULL, 0, NULL, 0, diff --git a/src/test/test-ns.c b/src/test/test-ns.c index 29f6dc5e1f..5d7931f619 100644 --- a/src/test/test-ns.c +++ b/src/test/test-ns.c @@ -78,6 +78,7 @@ int main(int argc, char *argv[]) { tmp_dir, var_tmp_dir, NULL, + NULL, 0, NULL, 0, From 3652872add1a08f7b7268c6233e047733e2092f6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Jul 2020 08:47:08 +0200 Subject: [PATCH 06/11] nspawn: add --set-credential= and --load-credential= Let's allow passing in creds to containers, so that PID 1 inside the container can pick them up. --- src/nspawn/meson.build | 2 + src/nspawn/nspawn-creds.c | 25 +++++ src/nspawn/nspawn-creds.h | 12 +++ src/nspawn/nspawn-settings.h | 7 +- src/nspawn/nspawn.c | 189 ++++++++++++++++++++++++++++++++++- 5 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/nspawn/nspawn-creds.c create mode 100644 src/nspawn/nspawn-creds.h diff --git a/src/nspawn/meson.build b/src/nspawn/meson.build index c049ac6754..ae3d72faca 100644 --- a/src/nspawn/meson.build +++ b/src/nspawn/meson.build @@ -3,6 +3,8 @@ libnspawn_core_sources = files(''' nspawn-cgroup.c nspawn-cgroup.h + nspawn-creds.c + nspawn-creds.h nspawn-def.h nspawn-expose-ports.c nspawn-expose-ports.h diff --git a/src/nspawn/nspawn-creds.c b/src/nspawn/nspawn-creds.c new file mode 100644 index 0000000000..41a38d37ea --- /dev/null +++ b/src/nspawn/nspawn-creds.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "alloc-util.h" +#include "macro.h" +#include "memory-util.h" +#include "nspawn-creds.h" + +static void credential_free(Credential *cred) { + assert(cred); + + cred->id = mfree(cred->id); + cred->data = erase_and_free(cred->data); + cred->size = 0; +} + +void credential_free_all(Credential *creds, size_t n) { + size_t i; + + assert(creds || n == 0); + + for (i = 0; i < n; i++) + credential_free(creds + i); + + free(creds); +} diff --git a/src/nspawn/nspawn-creds.h b/src/nspawn/nspawn-creds.h new file mode 100644 index 0000000000..b3c90bb17a --- /dev/null +++ b/src/nspawn/nspawn-creds.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +typedef struct Credential { + char *id; + void *data; + size_t size; +} Credential; + +void credential_free_all(Credential *creds, size_t n); diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index ab31c05a9e..b8fa145f77 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -116,9 +116,10 @@ typedef enum SettingsMask { SETTING_USE_CGNS = UINT64_C(1) << 27, SETTING_CLONE_NS_FLAGS = UINT64_C(1) << 28, SETTING_CONSOLE_MODE = UINT64_C(1) << 29, - SETTING_RLIMIT_FIRST = UINT64_C(1) << 30, /* we define one bit per resource limit here */ - SETTING_RLIMIT_LAST = UINT64_C(1) << (30 + _RLIMIT_MAX - 1), - _SETTINGS_MASK_ALL = (UINT64_C(1) << (30 + _RLIMIT_MAX)) -1, + SETTING_CREDENTIALS = UINT64_C(1) << 30, + SETTING_RLIMIT_FIRST = UINT64_C(1) << 31, /* we define one bit per resource limit here */ + SETTING_RLIMIT_LAST = UINT64_C(1) << (31 + _RLIMIT_MAX - 1), + _SETTINGS_MASK_ALL = (UINT64_C(1) << (31 + _RLIMIT_MAX)) -1, _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3ad8829855..26469759d9 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -36,6 +36,7 @@ #include "dev-setup.h" #include "dissect-image.h" #include "env-util.h" +#include "escape.h" #include "fd-util.h" #include "fdset.h" #include "fileio.h" @@ -45,6 +46,7 @@ #include "hexdecoct.h" #include "hostname-util.h" #include "id128-util.h" +#include "io-util.h" #include "log.h" #include "loop-util.h" #include "loopback-setup.h" @@ -58,6 +60,7 @@ #include "namespace-util.h" #include "netlink-util.h" #include "nspawn-cgroup.h" +#include "nspawn-creds.h" #include "nspawn-def.h" #include "nspawn-expose-ports.h" #include "nspawn-mount.h" @@ -219,6 +222,8 @@ static DeviceNode* arg_extra_nodes = NULL; static size_t arg_n_extra_nodes = 0; static char **arg_sysctl = NULL; static ConsoleMode arg_console_mode = _CONSOLE_MODE_INVALID; +static Credential *arg_credentials = NULL; +static size_t arg_n_credentials = 0; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_template, freep); @@ -406,7 +411,13 @@ static int help(void) { "%3$sInput/Output:%4$s\n" " --console=MODE Select how stdin/stdout/stderr and /dev/console are\n" " set up for the container.\n" - " -P --pipe Equivalent to --console=pipe\n" + " -P --pipe Equivalent to --console=pipe\n\n" + "%3$sCredentials:%4$s\n" + " --set-credential=ID:VALUE\n" + " Pass a credential with literal value to container.\n" + " --load-credential=ID:PATH\n" + " Load credential to pass to container from file or\n" + " AF_UNIX stream socket.\n" "\nSee the %2$s for details.\n" , program_invocation_short_name , link @@ -675,6 +686,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_PAGER, ARG_VERITY_DATA, ARG_ROOT_HASH_SIG, + ARG_SET_CREDENTIAL, + ARG_LOAD_CREDENTIAL, }; static const struct option options[] = { @@ -742,6 +755,8 @@ static int parse_argv(int argc, char *argv[]) { { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, + { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, + { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, {} }; @@ -1496,6 +1511,105 @@ static int parse_argv(int argc, char *argv[]) { arg_pager_flags |= PAGER_DISABLE; break; + case ARG_SET_CREDENTIAL: { + _cleanup_free_ char *word = NULL, *data = NULL; + const char *p = optarg; + Credential *a; + size_t i; + 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 --set-credential= parameter: %m"); + if (r == 0 || !p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing value for --set-credential=: %s", optarg); + + if (!credential_name_valid(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name is not valid: %s", word); + + for (i = 0; i < arg_n_credentials; i++) + if (streq(arg_credentials[i].id, word)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate credential '%s', refusing.", word); + + l = cunescape(p, UNESCAPE_ACCEPT_NUL, &data); + if (l < 0) + return log_error_errno(l, "Failed to unescape credential data: %s", p); + + a = reallocarray(arg_credentials, arg_n_credentials + 1, sizeof(Credential)); + if (!a) + return log_oom(); + + a[arg_n_credentials++] = (Credential) { + .id = TAKE_PTR(word), + .data = TAKE_PTR(data), + .size = l, + }; + + arg_credentials = a; + + arg_settings_mask |= SETTING_CREDENTIALS; + break; + } + + case ARG_LOAD_CREDENTIAL: { + ReadFullFileFlags flags = READ_FULL_FILE_SECURE; + _cleanup_(erase_and_freep) char *data = NULL; + _cleanup_free_ char *word = NULL, *j = NULL; + const char *p = optarg; + Credential *a; + size_t size, i; + + 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 --set-credential= parameter: %m"); + if (r == 0 || !p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing value for --set-credential=: %s", optarg); + + if (!credential_name_valid(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name is not valid: %s", word); + + for (i = 0; i < arg_n_credentials; i++) + if (streq(arg_credentials[i].id, word)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate credential '%s', refusing.", word); + + if (path_is_absolute(p)) + flags |= READ_FULL_FILE_CONNECT_SOCKET; + else { + const char *e; + + e = getenv("CREDENTIALS_DIRECTORY"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential not available (no credentials passed at all): %s", word); + + j = path_join(e, p); + if (!j) + return log_oom(); + } + + r = read_full_file_full(AT_FDCWD, j ?: p, flags, &data, &size); + if (r < 0) + return log_error_errno(r, "Failed to read credential '%s': %m", j ?: p); + + a = reallocarray(arg_credentials, arg_n_credentials + 1, sizeof(Credential)); + if (!a) + return log_oom(); + + a[arg_n_credentials++] = (Credential) { + .id = TAKE_PTR(word), + .data = TAKE_PTR(data), + .size = size, + }; + + arg_credentials = a; + + arg_settings_mask |= SETTING_CREDENTIALS; + break; + } + case '?': return -EINVAL; @@ -2228,6 +2342,66 @@ static int setup_keyring(void) { return 0; } +static int setup_credentials(const char *root) { + const char *q; + int r; + + if (arg_n_credentials <= 0) + return 0; + + r = userns_mkdir(root, "/run/host", 0755, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to create /run/host: %m"); + + r = userns_mkdir(root, "/run/host/credentials", 0700, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to create /run/host/credentials: %m"); + + q = prefix_roota(root, "/run/host/credentials"); + r = mount_verbose(LOG_ERR, NULL, q, "ramfs", MS_NOSUID|MS_NOEXEC|MS_NODEV, "mode=0700"); + if (r < 0) + return r; + + for (size_t i = 0; i < arg_n_credentials; i++) { + _cleanup_free_ char *j = NULL; + _cleanup_close_ int fd = -1; + + j = path_join(q, arg_credentials[i].id); + if (!j) + return log_oom(); + + fd = open(j, O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC|O_NOFOLLOW, 0600); + if (fd < 0) + return log_error_errno(errno, "Failed to create credential file %s: %m", j); + + r = loop_write(fd, arg_credentials[i].data, arg_credentials[i].size, /* do_poll= */ false); + if (r < 0) + return log_error_errno(r, "Failed to write credential to file %s: %m", j); + + if (fchmod(fd, 0400) < 0) + return log_error_errno(errno, "Failed to adjust access mode of %s: %m", j); + + if (arg_userns_mode != USER_NAMESPACE_NO) { + if (fchown(fd, arg_uid_shift, arg_uid_shift) < 0) + return log_error_errno(errno, "Failed to adjust ownership of %s: %m", j); + } + } + + if (chmod(q, 0500) < 0) + return log_error_errno(errno, "Failed to adjust access mode of %s: %m", q); + + r = userns_lchown(q, 0, 0); + if (r < 0) + return r; + + /* Make both mount and superblock read-only now */ + r = mount_verbose(LOG_ERR, NULL, q, NULL, MS_REMOUNT|MS_BIND|MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL); + if (r < 0) + return r; + + return mount_verbose(LOG_ERR, NULL, q, NULL, MS_REMOUNT|MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, "mode=0500"); +} + static int setup_kmsg(int kmsg_socket) { _cleanup_(unlink_and_freep) char *from = NULL; _cleanup_free_ char *fifo = NULL; @@ -2941,6 +3115,7 @@ static int inner_child( NULL, /* LISTEN_FDS */ NULL, /* LISTEN_PID */ NULL, /* NOTIFY_SOCKET */ + NULL, /* CREDENTIALS_DIRECTORY */ NULL }; const char *exec_target; @@ -3191,6 +3366,13 @@ static int inner_child( if (asprintf((char **)(envp + n_env++), "NOTIFY_SOCKET=%s", NSPAWN_NOTIFY_SOCKET_PATH) < 0) return log_oom(); + if (arg_n_credentials > 0) { + envp[n_env] = strdup("CREDENTIALS_DIRECTORY=/run/host/credentials"); + if (!envp[n_env]) + return log_oom(); + n_env++; + } + env_use = strv_env_merge(3, envp, os_release_pairs, arg_setenv); if (!env_use) return log_oom(); @@ -3538,6 +3720,10 @@ static int outer_child( if (r < 0) return r; + r = setup_credentials(directory); + if (r < 0) + return r; + r = mount_custom( directory, arg_custom_mounts, @@ -5339,6 +5525,7 @@ finish: expose_port_free_all(arg_expose_ports); rlimit_free_all(arg_rlimit); device_node_array_free(arg_extra_nodes, arg_n_extra_nodes); + credential_free_all(arg_credentials, arg_n_credentials); if (r < 0) return r; From 3220cf394c7be9f8056126cf7de2b067f10f9540 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Jul 2020 18:05:54 +0200 Subject: [PATCH 07/11] man: document pid1's new credentials logic --- man/systemd.exec.xml | 95 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 6d1c4e0528..d54648a966 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -2154,11 +2154,13 @@ SystemCallErrorNumber=EPERM project='man-pages'>environ7 for details about environment variables. - Note that environment variables are not suitable for passing secrets (such as passwords, key material, …) - to service processes. Environment variables set for a unit are exposed to unprivileged clients via D-Bus IPC, - and generally not understood as being data that requires protection. Moreover, environment variables are - propagated down the process tree, including across security boundaries (such as setuid/setgid executables), and - hence might leak to processes that should not have access to the secret data. + Note that environment variables are not suitable for passing secrets (such as passwords, key + material, …) to service processes. Environment variables set for a unit are exposed to unprivileged + clients via D-Bus IPC, and generally not understood as being data that requires protection. Moreover, + environment variables are propagated down the process tree, including across security boundaries + (such as setuid/setgid executables), and hence might leak to processes that should not have access to + the secret data. Use LoadCredential= (see below) to pass data to unit processes + securely. @@ -2624,6 +2626,73 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy + + Credentials + + + + + LoadCredential=ID:PATH + + Pass a credential to the unit. Credentials are limited-size binary or textual objects + that may be passed to unit processes. They are primarily used for passing cryptographic keys (both + public and private) or certificates, user account information or identity information from host to + services. The data is accessible from the unit's processes via the file system, at a read-only + location that (if possible and permitted) is backed by non-swappable memory. The data is only + accessible to the user associated with the unit, via the + User=/DynamicUser= settings (as well as the superuser). When + available, the location of credentials is exported as the $CREDENTIALS_DIRECTORY + environment variable to the unit's processes. + + The LoadCredential= setting takes a textual ID to use as name for a + credential plus a file system path. The ID must be a short ASCII string suitable as filename in the + filesystem, and may be chosen freely by the user. If the specified path is absolute it is opened as + regular file and the credential data is read from it. If the absolute path refers to an + AF_UNIX stream socket in the file system a connection is made to it and the + credential data read from the connection, providing an easy IPC integration point for dynamically + providing credentials from other services. If the specified path is not absolute and itself qualifies + as valid credential identifier it is understood to refer to a credential that the service manager + itself received via the $CREDENTIALS_DIRECTORY environment variable, which may be + used to propagate credentials from an invoking environment (e.g. a container manager that invoked the + service manager) into a service. The contents of the file/socket may be arbitrary binary or textual + data, including newline characters and NUL bytes. This option may be used multiple times, each time + defining an additional credential to pass to the unit. + + The credential files/IPC sockets must be accessible to the service manager, but don't have to + be directly accessible to the unit's processes: the credential data is read and copied into separate, + read-only copies for the unit that are accessible to appropriately privileged processes. This is + particularly useful in combination with DynamicUser= as this way privileged data + can be made available to processes running under a dynamic UID (i.e. not a previously known one) + without having to open up access to all users. + + In order to reference the path a credential may be read from within a + ExecStart= command line use ${CREDENTIALS_DIRECTORY}/mycred, + e.g. ExecStart=cat ${CREDENTIALS_DIRECTORY}/mycred. + + Currently, an accumulated credential size limit of 1M bytes per unit is + enforced. + + + + SetCredential=ID:VALUE + + The SetCredential= setting is similar to + LoadCredential= but accepts a literal value to use as data for the credential, + instead of a file system path to read the data from. Do not use this option for data that is supposed + to be secret, as it is accessible to unprivileged processes via IPC. It's only safe to use this for + user IDs, public key material and similar non-sensitive data. For everything else use + LoadCredential=. In order to embed binary data into the credential data use + C-style escaping (i.e. \n to embed a newline, or \x00 to embed + a NUL byte). + + If a credential of the same ID is listed in both LoadCredential= and + SetCredential=, the latter will act as default if the former cannot be + retrieved. In this case not being able to retrieve the credential from the path specified in + LoadCredential= is not considered fatal. + + + + System V Compatibility @@ -2779,6 +2848,16 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy + + $CREDENTIALS_DIRECTORY + + An absolute path to the per-unit directory with credentials configured via + LoadCredential=/SetCredential=. The directory is marked + read-only and is placed in unswappable memory (if supported and permitted), and is only accessible to + the UID associated with the unit via User= or DynamicUser= (and + the superuser). + + $MAINPID @@ -3380,7 +3459,11 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy EXIT_NUMA_POLICY Failed to set up unit's NUMA memory policy. See NUMAPolicy= and NUMAMask= above. - + + 243 + EXIT_CREDENTIALS + Failed to set up unit's credentials. See LoadCredential= and SetCredential= above. + From 60cc90b95989371268ba7ef5f9cabb72643c26b5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Jul 2020 17:43:18 +0200 Subject: [PATCH 08/11] man: document nspawn's new credential switches --- man/systemd-nspawn.xml | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index 69558ac85c..e1fec3d7a8 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -1402,7 +1402,51 @@ Equivalent to . + + + Credentials + + + + ID:PATH + ID:VALUE + + Pass a credential to the container. These two options correspond to the + LoadCredential= and SetCredential= settings in unit files. See + systemd.exec5 for + details about these concepts, as well as the syntax of the option's arguments. + + Note: + + + When systemd-nspawn runs as systemd system service it can make + use and propagate credentials it received via + LoadCredential=/SetCredential= to the container + payload. + + A systemd service manager running as PID 1 in the container can make use of + credentials passed in this way, and propagate them further to services it itself + runs. + + + Thus it is possible to easily propagate credentials from a host service manager to a + systemd-nspawn service and from there into its payload and services running within + it. + + In order to embed binary data into + the credential data for use C-style escaping + (i.e. \n to embed a newline, or \x00 to embed a NUL byte. Note + that the invoking shell might already apply unescaping once, hence this might require double + escaping!). + + + + + + Other + + From b0d29bfdfd503a589e0b050fe14d6fc4297af5ae Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 28 Jul 2020 09:10:18 +0200 Subject: [PATCH 09/11] man: document credentials passing in the container interface --- docs/CONTAINER_INTERFACE.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/CONTAINER_INTERFACE.md b/docs/CONTAINER_INTERFACE.md index c7c57c7c06..40b1533595 100644 --- a/docs/CONTAINER_INTERFACE.md +++ b/docs/CONTAINER_INTERFACE.md @@ -131,6 +131,17 @@ manager, please consider supporting the following interfaces. `$container_host_variant_id=server` `$container_host_version_id=10` +5. systemd supports passing immutable binary data blobs with limited size and + restricted access to services via the `LoadCredential=` and `SetCredential=` + settings. The same protocol may be used to pass credentials from the + container manager to systemd itself. The credential data should be placed in + some location (ideally a read-only and non-swappable file system, like + 'ramfs'), and the absolute path to this directory exported in the + `$CREDENTIALS_DIRECTORY` environment variable. If the container managers + does this, the credentials passed to the service manager can be propagated + to services via `LoadCredential=` (see ...). The container manager can + choose any path, but `/run/host/credentials` is recommended." + ## Advanced Integration 1. Consider syncing `/etc/localtime` from the host file system into the @@ -228,7 +239,7 @@ care should be taken to avoid naming conflicts. `systemd` (and in particular inaccessible. Note that systemd when run as PID 1 in the container payload will create these nodes on its own if not passed in by the container manager. However, in that case it likely lacks the privileges to create the - character and block devices nodes (there all fallbacks for this case). + character and block devices nodes (there are fallbacks for this case). 3. The `/run/host/notify` path is a good choice to place the `sd_notify()` socket in, that may be used for the container's PID 1 to report to the @@ -252,6 +263,9 @@ care should be taken to avoid naming conflicts. `systemd` (and in particular as the `$container_uuid` environment variable (see above). This file should be newline terminated. +7. The `/run/host/credentials/` directory is a good place to pass credentials + into the container, using the `$CREDENTIALS_DIRECTORY` protocol, see above. + ## What You Shouldn't Do 1. Do not drop `CAP_MKNOD` from the container. `PrivateDevices=` is a commonly From 30dd9f7391dce4711809eb43bac1e03316f99154 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 11 Aug 2020 17:08:41 +0200 Subject: [PATCH 10/11] test: add test suite for new credentials logic --- test/TEST-54-CREDS/Makefile | 1 + test/TEST-54-CREDS/test.sh | 7 +++++++ test/units/testsuite-54.service | 7 +++++++ test/units/testsuite-54.sh | 31 +++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 120000 test/TEST-54-CREDS/Makefile create mode 100755 test/TEST-54-CREDS/test.sh create mode 100644 test/units/testsuite-54.service create mode 100755 test/units/testsuite-54.sh diff --git a/test/TEST-54-CREDS/Makefile b/test/TEST-54-CREDS/Makefile new file mode 120000 index 0000000000..e9f93b1104 --- /dev/null +++ b/test/TEST-54-CREDS/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile \ No newline at end of file diff --git a/test/TEST-54-CREDS/test.sh b/test/TEST-54-CREDS/test.sh new file mode 100755 index 0000000000..5feb15e7f1 --- /dev/null +++ b/test/TEST-54-CREDS/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -e +TEST_DESCRIPTION="test credentials" + +. $TEST_BASE_DIR/test-functions + +do_test "$@" 54 diff --git a/test/units/testsuite-54.service b/test/units/testsuite-54.service new file mode 100644 index 0000000000..862dd1c0fb --- /dev/null +++ b/test/units/testsuite-54.service @@ -0,0 +1,7 @@ +[Unit] +Description=TESTSUITE-54-CREDS + +[Service] +ExecStartPre=rm -f /failed /testok +ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh +Type=oneshot diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh new file mode 100755 index 0000000000..aabc56f348 --- /dev/null +++ b/test/units/testsuite-54.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -ex + +systemd-analyze log-level debug + +# Verify that the creds are properly loaded and we can read them from the service's unpriv user +systemd-run -p LoadCredential=passwd:/etc/passwd \ + -p LoadCredential=shadow:/etc/shadow \ + -p SetCredential=dog:wuff \ + -p DynamicUser=1 \ + --wait \ + --pipe \ + cat '${CREDENTIALS_DIRECTORY}/passwd' '${CREDENTIALS_DIRECTORY}/shadow' '${CREDENTIALS_DIRECTORY}/dog' > /tmp/ts54-concat +( cat /etc/passwd /etc/shadow && echo -n wuff ) | cmp /tmp/ts54-concat +rm /tmp/ts54-concat + +# Verify that the creds are immutable +! systemd-run -p LoadCredential=passwd:/etc/passwd \ + -p DynamicUser=1 \ + --wait \ + touch '${CREDENTIALS_DIRECTORY}/passwd' +! systemd-run -p LoadCredential=passwd:/etc/passwd \ + -p DynamicUser=1 \ + --wait \ + rm '${CREDENTIALS_DIRECTORY}/passwd' + +systemd-analyze log-level info + +echo OK > /testok + +exit 0 From fabece9ccb77e773bd5e9ac91edfa841e2d78f38 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Jul 2020 08:46:43 +0200 Subject: [PATCH 11/11] update TODO --- TODO | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/TODO b/TODO index b63b436e29..c4c20f71a5 100644 --- a/TODO +++ b/TODO @@ -119,14 +119,18 @@ Features: * seccomp: maybe merge all filters we install into one with that libseccomp API that allows merging. -* per-service credential system. Specifically: add LoadCredential= (for loading - cred from file), AcquireCredential= (for asking user for cred, via - ask-password), PassCredential= (for passing on credential systemd itself - got). Then, place credentials in a per-service, immutable ramfs instance (so - that it cannot be swapped out), destroy after use. Also pass via keyring - (with graceful fallback to cover for containers). Define CredentialPath= for - defining subdir of /run/credentials/ where to place it. Set $CREDENTIAL_PATH - env var for services to the result. Also pass via fd passing (optionally). +* credentials system: + - maybe add AcquireCredential= for querying a cred via ask-password + - maybe try to acquire creds via keyring? + - maybe try to pass creds via keyring? + - maybe optionally pass creds via memfd + - maybe add support for decrypting creds via TPM + - maybe add support for decrypting/importing creds via pkcs11 + - make systemd-cryptsetup acquire pw via creds logic + - make PAMName= acquire pw via creds logic + - make macsec/wireguard code in networkd read key via creds logic + - make gatwayd/remote read key via creds logic + - add sd_notify() command for flushing out creds not needed anymore * homed: add native recovery key support. use 48 lowercase modhex characters (192bit), show qr code of it, include pattern expression in user record.