diff --git a/TODO b/TODO index 179d17e234..e25ca89510 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: during login resize fs automatically towards size goal. Specifically, resize to diskSize if possible, but leave a certain amount (configured by a 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 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 + + 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. + diff --git a/src/basic/path-util.c b/src/basic/path-util.c index 749f2eeecc..9fab0e7b5a 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" @@ -1130,3 +1131,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/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/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..aede50c5fe 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, @@ -2604,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; @@ -2672,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, @@ -2685,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, @@ -3489,6 +3949,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 +4744,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 +4775,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 +6303,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-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/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/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/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/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/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; diff --git a/src/shared/acl-util.c b/src/shared/acl-util.c index dd2b1efb11..02c94f9358 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; } @@ -376,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)); @@ -397,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/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 */ }; 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-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); 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, 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; +} 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