Merge pull request #16568 from poettering/creds-store

credentials logic to pass privileged data to services
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2020-08-26 10:32:30 +02:00 committed by GitHub
commit b6abc2acb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1677 additions and 54 deletions

20
TODO
View File

@ -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

View File

@ -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

View File

@ -1402,7 +1402,51 @@
<listitem><para>Equivalent to <option>--console=pipe</option>.</para></listitem>
</varlistentry>
</variablelist>
</refsect2><refsect2>
<title>Credentials</title>
<variablelist>
<varlistentry>
<term><option>--load-credential=</option><replaceable>ID</replaceable>:<replaceable>PATH</replaceable></term>
<term><option>--set-credential=</option><replaceable>ID</replaceable>:<replaceable>VALUE</replaceable></term>
<para>Pass a credential to the container. These two options correspond to the
<varname>LoadCredential=</varname> and <varname>SetCredential=</varname> settings in unit files. See
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details about these concepts, as well as the syntax of the option's arguments.</para>
<para>Note:</para>
<orderedlist>
<listitem><para>When <command>systemd-nspawn</command> runs as systemd system service it can make
use and propagate credentials it received via
<varname>LoadCredential=</varname>/<varname>SetCredential=</varname> to the container
payload.</para></listitem>
<listitem><para>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.</para></listitem>
</orderedlist>
<para>Thus it is possible to easily propagate credentials from a host service manager to a
<command>systemd-nspawn</command> service and from there into its payload and services running within
it.</para>
<para>In order to embed binary data into
the credential data for <option>--set-credential=</option> use C-style escaping
(i.e. <literal>\n</literal> to embed a newline, or <literal>\x00</literal> to embed a NUL byte. Note
that the invoking shell might already apply unescaping once, hence this might require double
escaping!).</para>
</varlistentry>
</variablelist>
</refsect2><refsect2>
<title>Other</title>
<variablelist>
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />

View File

@ -2154,11 +2154,13 @@ SystemCallErrorNumber=EPERM</programlisting>
project='man-pages'><refentrytitle>environ</refentrytitle><manvolnum>7</manvolnum></citerefentry> for details
about environment variables.</para>
<para>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.</para></listitem>
<para>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 <varname>LoadCredential=</varname> (see below) to pass data to unit processes
securely.</para></listitem>
</varlistentry>
<varlistentry>
@ -2624,6 +2626,73 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
</variablelist>
</refsect1>
<refsect1>
<title>Credentials</title>
<variablelist class='unit-directives'>
<varlistentry>
<term><varname>LoadCredential=</varname><replaceable>ID</replaceable>:<replaceable>PATH</replaceable></term>
<listitem><para>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
<varname>User=</varname>/<varname>DynamicUser=</varname> settings (as well as the superuser). When
available, the location of credentials is exported as the <varname>$CREDENTIALS_DIRECTORY</varname>
environment variable to the unit's processes.</para>
<para>The <varname>LoadCredential=</varname> 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
<constant>AF_UNIX</constant> 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 <varname>$CREDENTIALS_DIRECTORY</varname> 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.</para>
<para>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 <varname>DynamicUser=</varname> 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.</para>
<para>In order to reference the path a credential may be read from within a
<varname>ExecStart=</varname> command line use <literal>${CREDENTIALS_DIRECTORY}/mycred</literal>,
e.g. <literal>ExecStart=cat ${CREDENTIALS_DIRECTORY}/mycred</literal>.</para>
<para>Currently, an accumulated credential size limit of 1M bytes per unit is
enforced.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>SetCredential=</varname><replaceable>ID</replaceable>:<replaceable>VALUE</replaceable></term>
<listitem><para>The <varname>SetCredential=</varname> setting is similar to
<varname>LoadCredential=</varname> 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
<varname>LoadCredential=</varname>. In order to embed binary data into the credential data use
C-style escaping (i.e. <literal>\n</literal> to embed a newline, or <literal>\x00</literal> to embed
a NUL byte).</para>
<para>If a credential of the same ID is listed in both <varname>LoadCredential=</varname> and
<varname>SetCredential=</varname>, 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
<varname>LoadCredential=</varname> is not considered fatal.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>System V Compatibility</title>
<variablelist class='unit-directives'>
@ -2779,6 +2848,16 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
</listitem>
</varlistentry>
<varlistentry>
<term><varname>$CREDENTIALS_DIRECTORY</varname></term>
<listitem><para>An absolute path to the per-unit directory with credentials configured via
<varname>LoadCredential=</varname>/<varname>SetCredential=</varname>. 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 <varname>User=</varname> or <varname>DynamicUser=</varname> (and
the superuser).</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>$MAINPID</varname></term>
@ -3380,7 +3459,11 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
<entry><constant>EXIT_NUMA_POLICY</constant></entry>
<entry>Failed to set up unit's NUMA memory policy. See <varname>NUMAPolicy=</varname> and <varname>NUMAMask=</varname> above.</entry>
</row>
<row>
<entry>243</entry>
<entry><constant>EXIT_CREDENTIALS</constant></entry>
<entry>Failed to set up unit's credentials. See <varname>LoadCredential=</varname> and <varname>SetCredential=</varname> above.</entry>
</row>
</tbody>
</tgroup>
</table>

View File

@ -14,6 +14,7 @@
#include "alloc-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "log.h"
@ -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);
}

View File

@ -173,3 +173,5 @@ static inline const char *empty_to_root(const char *path) {
bool path_strv_contains(char **l, const char *path);
bool prefixed_path_strv_contains(char **l, const char *path);
bool credential_name_valid(const char *s);

View File

@ -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;

View File

@ -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);

View File

@ -748,6 +748,82 @@ static int property_get_log_extra_fields(
return sd_bus_message_close_container(reply);
}
static int property_get_set_credential(
sd_bus *bus,
const char *path,
const char *interface,
const char *property,
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
ExecContext *c = userdata;
ExecSetCredential *sc;
Iterator iterator;
int r;
assert(bus);
assert(c);
assert(property);
assert(reply);
r = sd_bus_message_open_container(reply, 'a', "(say)");
if (r < 0)
return r;
HASHMAP_FOREACH(sc, c->set_credentials, iterator) {
r = sd_bus_message_open_container(reply, 'r', "say");
if (r < 0)
return r;
r = sd_bus_message_append(reply, "s", sc->id);
if (r < 0)
return r;
r = sd_bus_message_append_array(reply, 'y', sc->data, sc->size);
if (r < 0)
return r;
r = sd_bus_message_close_container(reply);
if (r < 0)
return r;
}
return sd_bus_message_close_container(reply);
}
static int property_get_load_credential(
sd_bus *bus,
const char *path,
const char *interface,
const char *property,
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
ExecContext *c = userdata;
char **i, **j;
int r;
assert(bus);
assert(c);
assert(property);
assert(reply);
r = sd_bus_message_open_container(reply, 'a', "(ss)");
if (r < 0)
return r;
STRV_FOREACH_PAIR(i, j, c->load_credentials) {
r = sd_bus_message_append(reply, "(ss)", *i, *j);
if (r < 0)
return r;
}
return sd_bus_message_close_container(reply);
}
static int property_get_root_hash(
sd_bus *bus,
const char *path,
@ -965,6 +1041,8 @@ const sd_bus_vtable bus_exec_vtable[] = {
SD_BUS_PROPERTY("Group", "s", NULL, offsetof(ExecContext, group), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("DynamicUser", "b", bus_property_get_bool, offsetof(ExecContext, dynamic_user), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RemoveIPC", "b", bus_property_get_bool, offsetof(ExecContext, remove_ipc), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("SetCredential", "a(say)", property_get_set_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("LoadCredential", "a(ss)", property_get_load_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("SupplementaryGroups", "as", NULL, offsetof(ExecContext, supplementary_groups), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("PAMName", "s", NULL, offsetof(ExecContext, pam_name), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("ReadWritePaths", "as", NULL, offsetof(ExecContext, read_write_paths), SD_BUS_VTABLE_PROPERTY_CONST),
@ -1794,6 +1872,146 @@ int bus_exec_context_set_transient_property(
return 1;
} else if (streq(name, "SetCredential")) {
bool isempty = true;
r = sd_bus_message_enter_container(message, 'a', "(say)");
if (r < 0)
return r;
for (;;) {
const char *id;
const void *p;
size_t sz;
r = sd_bus_message_enter_container(message, 'r', "say");
if (r < 0)
return r;
if (r == 0)
break;
r = sd_bus_message_read(message, "s", &id);
if (r < 0)
return r;
r = sd_bus_message_read_array(message, 'y', &p, &sz);
if (r < 0)
return r;
r = sd_bus_message_exit_container(message);
if (r < 0)
return r;
if (!credential_name_valid(id))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential ID is invalid: %s", id);
isempty = false;
if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
_cleanup_free_ char *a = NULL, *b = NULL;
_cleanup_free_ void *copy = NULL;
ExecSetCredential *old;
copy = memdup(p, sz);
if (!copy)
return -ENOMEM;
old = hashmap_get(c->set_credentials, id);
if (old) {
free_and_replace(old->data, copy);
old->size = sz;
} else {
_cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
sc = new0(ExecSetCredential, 1);
if (!sc)
return -ENOMEM;
sc->id = strdup(id);
if (!sc->id)
return -ENOMEM;
sc->data = TAKE_PTR(copy);
sc->size = sz;
r = hashmap_ensure_allocated(&c->set_credentials, &exec_set_credential_hash_ops);
if (r < 0)
return r;
r = hashmap_put(c->set_credentials, sc->id, sc);
if (r < 0)
return r;
TAKE_PTR(sc);
}
a = specifier_escape(id);
if (!a)
return -ENOMEM;
b = cescape_length(p, sz);
if (!b)
return -ENOMEM;
(void) unit_write_settingf(u, flags, name, "%s=%s:%s", name, a, b);
}
}
r = sd_bus_message_exit_container(message);
if (r < 0)
return r;
if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) {
c->set_credentials = hashmap_free(c->set_credentials);
(void) unit_write_settingf(u, flags, name, "%s=", name);
}
return 1;
} else if (streq(name, "LoadCredential")) {
bool isempty = true;
r = sd_bus_message_enter_container(message, 'a', "(ss)");
if (r < 0)
return r;
for (;;) {
const char *id, *source;
r = sd_bus_message_read(message, "(ss)", &id, &source);
if (r < 0)
return r;
if (r == 0)
break;
if (!credential_name_valid(id))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential ID is invalid: %s", id);
if (!(path_is_absolute(source) ? path_is_normalized(source) : credential_name_valid(source)))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential source is invalid: %s", source);
isempty = false;
if (!UNIT_WRITE_FLAGS_NOOP(flags)) {
r = strv_extend_strv(&c->load_credentials, STRV_MAKE(id, source), /* filter_duplicates = */ false);
if (r < 0)
return r;
(void) unit_write_settingf(u, flags|UNIT_ESCAPE_SPECIFIERS, name, "%s=%s:%s", name, id, source);
}
}
r = sd_bus_message_exit_container(message);
if (r < 0)
return r;
if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) {
c->load_credentials = strv_free(c->load_credentials);
(void) unit_write_settingf(u, flags, name, "%s=", name);
}
return 1;
} else if (streq(name, "SyslogLevel")) {
int32_t level;

View File

@ -6,6 +6,7 @@
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/personality.h>
#include <sys/prctl.h>
#include <sys/shm.h>
@ -32,6 +33,7 @@
#include "sd-messages.h"
#include "acl-util.h"
#include "af-list.h"
#include "alloc-util.h"
#if HAVE_APPARMOR
@ -41,8 +43,8 @@
#include "barrier.h"
#include "cap-list.h"
#include "capability-util.h"
#include "chown-recursive.h"
#include "cgroup-setup.h"
#include "chown-recursive.h"
#include "cpu-set-util.h"
#include "def.h"
#include "env-file.h"
@ -51,6 +53,7 @@
#include "execute.h"
#include "exit-status.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "glob-util.h"
@ -64,6 +67,7 @@
#include "memory-util.h"
#include "missing_fs.h"
#include "mkdir.h"
#include "mountpoint-util.h"
#include "namespace.h"
#include "parse-util.h"
#include "path-util.h"
@ -85,6 +89,7 @@
#include "strv.h"
#include "syslog-util.h"
#include "terminal-util.h"
#include "tmpfile-util.h"
#include "umask-util.h"
#include "unit.h"
#include "user-util.h"
@ -1417,6 +1422,14 @@ static bool context_has_no_new_privileges(const ExecContext *c) {
c->protect_hostname;
}
static bool exec_context_has_credentials(const ExecContext *context) {
assert(context);
return !hashmap_isempty(context->set_credentials) ||
context->load_credentials;
}
#if HAVE_SECCOMP
static bool skip_seccomp_unavailable(const Unit* u, const char* msg) {
@ -1725,7 +1738,7 @@ static int build_environment(
assert(p);
assert(ret);
#define N_ENV_VARS 15
#define N_ENV_VARS 16
our_env = new0(char*, N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX);
if (!our_env)
return -ENOMEM;
@ -1873,6 +1886,14 @@ static int build_environment(
our_env[n_env++] = x;
}
if (exec_context_has_credentials(c) && p->prefix[EXEC_DIRECTORY_RUNTIME]) {
x = strjoin("CREDENTIALS_DIRECTORY=", p->prefix[EXEC_DIRECTORY_RUNTIME], "/credentials/", u->id);
if (!x)
return -ENOMEM;
our_env[n_env++] = x;
}
our_env[n_env++] = NULL;
assert(n_env <= N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX);
#undef N_ENV_VARS
@ -2378,6 +2399,437 @@ fail:
return r;
}
static int write_credential(
int dfd,
const char *id,
const void *data,
size_t size,
uid_t uid,
bool ownership_ok) {
_cleanup_(unlink_and_freep) char *tmp = NULL;
_cleanup_close_ int fd = -1;
int r;
r = tempfn_random_child("", "cred", &tmp);
if (r < 0)
return r;
fd = openat(dfd, tmp, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL|O_NOFOLLOW|O_NOCTTY, 0600);
if (fd < 0) {
tmp = mfree(tmp);
return -errno;
}
r = loop_write(fd, data, size, /* do_pool = */ false);
if (r < 0)
return r;
if (fchmod(fd, 0400) < 0) /* Take away "w" bit */
return -errno;
if (uid_is_valid(uid) && uid != getuid()) {
#if HAVE_ACL
r = fd_add_uid_acl_permission(fd, uid, /* read = */ true, /* write = */ false, /* execute = */ false);
#else
r = -EOPNOTSUPP;
#endif
if (r < 0) {
if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
return r;
if (!ownership_ok) /* Ideally we use ACLs, since we can neatly express what we want
* to express: that the user gets read access and nothing
* else. But if the backing fs can't support that (e.g. ramfs)
* then we can use file ownership instead. But that's only safe if
* we can then re-mount the whole thing read-only, so that the
* user can no longer chmod() the file to gain write access. */
return r;
if (fchown(fd, uid, (gid_t) -1) < 0)
return -errno;
}
}
if (renameat(dfd, tmp, dfd, id) < 0)
return -errno;
tmp = mfree(tmp);
return 0;
}
#define CREDENTIALS_BYTES_MAX (1024LU * 1024LU) /* Refuse to pass more than 1M, after all this is unswappable memory */
static int acquire_credentials(
const ExecContext *context,
const ExecParameters *params,
const char *p,
uid_t uid,
bool ownership_ok) {
uint64_t left = CREDENTIALS_BYTES_MAX;
_cleanup_close_ int dfd = -1;
ExecSetCredential *sc;
char **id, **fn;
Iterator iterator;
int r;
assert(context);
assert(p);
dfd = open(p, O_DIRECTORY|O_CLOEXEC);
if (dfd < 0)
return -errno;
/* First we use the literally specified credentials. Note that they might be overriden again below,
* and thus act as a "default" if the same credential is specified multiple times */
HASHMAP_FOREACH(sc, context->set_credentials, iterator) {
size_t add;
add = strlen(sc->id) + sc->size;
if (add > left)
return -E2BIG;
r = write_credential(dfd, sc->id, sc->data, sc->size, uid, ownership_ok);
if (r < 0)
return r;
left -= add;
}
/* Then, load credential off disk (or acquire via AF_UNIX socket) */
STRV_FOREACH_PAIR(id, fn, context->load_credentials) {
ReadFullFileFlags flags = READ_FULL_FILE_SECURE;
_cleanup_(erase_and_freep) char *data = NULL;
_cleanup_free_ char *j = NULL;
const char *source;
size_t size, add;
if (path_is_absolute(*fn)) {
/* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */
source = *fn;
flags |= READ_FULL_FILE_CONNECT_SOCKET;
} else if (params->received_credentials) {
/* If this is a relative path, take it relative to the credentials we received
* ourselves. We don't support the AF_UNIX stuff in this mode, since we are operating
* on a credential store, i.e. this is guaranteed to be regular files. */
j = path_join(params->received_credentials, *fn);
if (!j)
return -ENOMEM;
source = j;
} else
source = NULL;
if (source)
r = read_full_file_full(AT_FDCWD, source, flags, &data, &size);
else
r = -ENOENT;
if (r == -ENOENT &&
faccessat(dfd, *id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) /* If the source file doesn't exist, but we already acquired the key otherwise, then don't fail */
continue;
if (r < 0)
return r;
add = strlen(*id) + size;
if (add > left)
return -E2BIG;
r = write_credential(dfd, *id, data, size, uid, ownership_ok);
if (r < 0)
return r;
left -= add;
}
if (fchmod(dfd, 0500) < 0) /* Now take away the "w" bit */
return -errno;
/* After we created all keys with the right perms, also make sure the credential store as a whole is
* accessible */
if (uid_is_valid(uid) && uid != getuid()) {
#if HAVE_ACL
r = fd_add_uid_acl_permission(dfd, uid, /* read = */ true, /* write = */ false, /* execute = */ true);
#else
r = -EOPNOTSUPP;
#endif
if (r < 0) {
if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
return r;
if (!ownership_ok)
return r;
if (fchown(dfd, uid, (gid_t) -1) < 0)
return -errno;
}
}
return 0;
}
static int setup_credentials_internal(
const ExecContext *context,
const ExecParameters *params,
const char *final, /* This is where the credential store shall eventually end up at */
const char *workspace, /* This is where we can prepare it before moving it to the final place */
bool reuse_workspace, /* Whether to reuse any existing workspace mount if it already is a mount */
bool must_mount, /* Whether to require that we mount something, it's not OK to use the plain directory fall back */
uid_t uid) {
int r, workspace_mounted; /* negative if we don't know yet whether we have/can mount something; true
* if we mounted something; false if we definitely can't mount anything */
bool final_mounted;
const char *where;
assert(context);
assert(final);
assert(workspace);
if (reuse_workspace) {
r = path_is_mount_point(workspace, NULL, 0);
if (r < 0)
return r;
if (r > 0)
workspace_mounted = true; /* If this is already a mount, and we are supposed to reuse it, let's keep this in mind */
else
workspace_mounted = -1; /* We need to figure out if we can mount something to the workspace */
} else
workspace_mounted = -1; /* ditto */
r = path_is_mount_point(final, NULL, 0);
if (r < 0)
return r;
if (r > 0) {
/* If the final place already has something mounted, we use that. If the workspace also has
* something mounted we assume it's actually the same mount (but with MS_RDONLY
* different). */
final_mounted = true;
if (workspace_mounted < 0) {
/* If the final place is mounted, but the workspace we isn't, then let's bind mount
* the final version to the workspace, and make it writable, so that we can make
* changes */
if (mount(final, workspace, NULL, MS_BIND|MS_REC, NULL) < 0)
return -errno;
if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0)
return -errno;
workspace_mounted = true;
}
} else
final_mounted = false;
if (workspace_mounted < 0) {
/* Nothing is mounted on the workspace yet, let's try to mount something now */
for (int try = 0;; try++) {
if (try == 0) {
/* Try "ramfs" first, since it's not swap backed */
if (mount("ramfs", workspace, "ramfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, "mode=0700") >= 0) {
workspace_mounted = true;
break;
}
} else if (try == 1) {
_cleanup_free_ char *opts = NULL;
if (asprintf(&opts, "mode=0700,nr_inodes=1024,size=%lu", CREDENTIALS_BYTES_MAX) < 0)
return -ENOMEM;
/* Fall back to "tmpfs" otherwise */
if (mount("tmpfs", workspace, "tmpfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, opts) >= 0) {
workspace_mounted = true;
break;
}
} else {
/* If that didn't work, try to make a bind mount from the final to the workspace, so that we can make it writable there. */
if (mount(final, workspace, NULL, MS_BIND|MS_REC, NULL) < 0) {
if (!ERRNO_IS_PRIVILEGE(errno)) /* Propagate anything that isn't a permission problem */
return -errno;
if (must_mount) /* If we it's not OK to use the plain directory
* fallback, propagate all errors too */
return -errno;
/* If we lack privileges to bind mount stuff, then let's gracefully
* proceed for compat with container envs, and just use the final dir
* as is. */
workspace_mounted = false;
break;
}
/* Make the new bind mount writable (i.e. drop MS_RDONLY) */
if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0)
return -errno;
workspace_mounted = true;
break;
}
}
}
assert(!must_mount || workspace_mounted > 0);
where = workspace_mounted ? workspace : final;
r = acquire_credentials(context, params, where, uid, workspace_mounted);
if (r < 0)
return r;
if (workspace_mounted) {
/* Make workspace read-only now, so that any bind mount we make from it defaults to read-only too */
if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0)
return -errno;
/* And mount it to the final place, read-only */
if (final_mounted) {
if (umount2(workspace, MNT_DETACH|UMOUNT_NOFOLLOW) < 0)
return -errno;
} else {
if (mount(workspace, final, NULL, MS_MOVE, NULL) < 0)
return -errno;
}
} else {
_cleanup_free_ char *parent = NULL;
/* If we do not have our own mount put used the plain directory fallback, then we need to
* open access to the top-level credential directory and the per-service directory now */
parent = dirname_malloc(final);
if (!parent)
return -ENOMEM;
if (chmod(parent, 0755) < 0)
return -errno;
}
return 0;
}
static int setup_credentials(
const ExecContext *context,
const ExecParameters *params,
const char *unit,
uid_t uid) {
_cleanup_free_ char *p = NULL, *q = NULL;
const char *i;
int r;
assert(context);
assert(params);
if (!exec_context_has_credentials(context))
return 0;
if (!params->prefix[EXEC_DIRECTORY_RUNTIME])
return -EINVAL;
/* This where we'll place stuff when we are done; this main credentials directory is world-readable,
* and the subdir we mount over with a read-only file system readable by the service's user */
q = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials");
if (!q)
return -ENOMEM;
r = mkdir_label(q, 0755); /* top-level dir: world readable/searchable */
if (r < 0 && r != -EEXIST)
return r;
p = path_join(q, unit);
if (!p)
return -ENOMEM;
r = mkdir_label(p, 0700); /* per-unit dir: private to user */
if (r < 0 && r != -EEXIST)
return r;
r = safe_fork("(sd-mkdcreds)", FORK_DEATHSIG|FORK_WAIT|FORK_NEW_MOUNTNS, NULL);
if (r < 0) {
_cleanup_free_ char *t = NULL, *u = NULL;
/* If this is not a privilege or support issue then propagate the error */
if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r))
return r;
/* Temporary workspace, that remains inaccessible all the time. We prepare stuff there before moving
* it into place, so that users can't access half-initialized credential stores. */
t = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "systemd/temporary-credentials");
if (!t)
return -ENOMEM;
/* We can't set up a mount namespace. In that case operate on a fixed, inaccessible per-unit
* directory outside of /run/credentials/ first, and then move it over to /run/credentials/
* after it is fully set up */
u = path_join(t, unit);
if (!u)
return -ENOMEM;
FOREACH_STRING(i, t, u) {
r = mkdir_label(i, 0700);
if (r < 0 && r != -EEXIST)
return r;
}
r = setup_credentials_internal(
context,
params,
p, /* final mount point */
u, /* temporary workspace to overmount */
true, /* reuse the workspace if it is already a mount */
false, /* it's OK to fall back to a plain directory if we can't mount anything */
uid);
(void) rmdir(u); /* remove the workspace again if we can. */
if (r < 0)
return r;
} else if (r == 0) {
/* We managed to set up a mount namespace, and are now in a child. That's great. In this case
* we can use the same directory for all cases, after turning off propagation. Question
* though is: where do we turn off propagation exactly, and where do we place the workspace
* directory? We need some place that is guaranteed to be a mount point in the host, and
* which is guaranteed to have a subdir we can mount over. /run/ is not suitable for this,
* since we ultimately want to move the resulting file system there, i.e. we need propagation
* for /run/ eventually. We could use our own /run/systemd/bind mount on itself, but that
* would be visible in the host mount table all the time, which we want to avoid. Hence, what
* we do here instead we use /dev/ and /dev/shm/ for our purposes. We know for sure that
* /dev/ is a mount point and we now for sure that /dev/shm/ exists. Hence we can turn off
* propagation on the former, and then overmount the latter.
*
* Yes it's nasty playing games with /dev/ and /dev/shm/ like this, since it does not exist
* for this purpose, but there are few other candidates that work equally well for us, and
* given that the we do this in a privately namespaced short-lived single-threaded process
* that noone else sees this should be OK to do.*/
if (mount(NULL, "/dev", NULL, MS_SLAVE|MS_REC, NULL) < 0) /* Turn off propagation from our namespace to host */
goto child_fail;
r = setup_credentials_internal(
context,
params,
p, /* final mount point */
"/dev/shm", /* temporary workspace to overmount */
false, /* do not reuse /dev/shm if it is already a mount, under no circumstances */
true, /* insist that something is mounted, do not allow fallback to plain directory */
uid);
if (r < 0)
goto child_fail;
_exit(EXIT_SUCCESS);
child_fail:
_exit(EXIT_FAILURE);
}
return 0;
}
#if ENABLE_SMACK
static int setup_smack(
const ExecContext *context,
@ -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",

View File

@ -145,6 +145,13 @@ typedef enum ExecCleanMask {
_EXEC_CLEAN_MASK_INVALID = -1,
} ExecCleanMask;
/* A credential configured with SetCredential= */
typedef struct ExecSetCredential {
char *id;
void *data;
size_t size;
} ExecSetCredential;
/* Encodes configuration parameters applied to invoked commands. Does not carry runtime data, but only configuration
* changes sourced from unit files and suchlike. ExecContext objects are usually embedded into Unit objects, and do not
* change after being loaded. */
@ -303,6 +310,9 @@ struct ExecContext {
ExecDirectory directories[_EXEC_DIRECTORY_TYPE_MAX];
ExecPreserveMode runtime_directory_preserve_mode;
usec_t timeout_clean_usec;
Hashmap *set_credentials; /* output id → ExecSetCredential */
char **load_credentials; /* pairs of output id, path/input id */
};
static inline bool exec_context_restrict_namespaces_set(const ExecContext *c) {
@ -321,11 +331,12 @@ typedef enum ExecFlags {
EXEC_CGROUP_DELEGATE = 1 << 6,
EXEC_IS_CONTROL = 1 << 7,
EXEC_CONTROL_CGROUP = 1 << 8, /* Place the process not in the indicated cgroup but in a subcgroup '/.control', but only EXEC_CGROUP_DELEGATE and EXEC_IS_CONTROL is set, too */
EXEC_WRITE_CREDENTIALS = 1 << 9, /* Set up the credential store logic */
/* The following are not used by execute.c, but by consumers internally */
EXEC_PASS_FDS = 1 << 9,
EXEC_SETENV_RESULT = 1 << 10,
EXEC_SET_WATCHDOG = 1 << 11,
EXEC_PASS_FDS = 1 << 10,
EXEC_SETENV_RESULT = 1 << 11,
EXEC_SET_WATCHDOG = 1 << 12,
} ExecFlags;
/* Parameters for a specific invocation of a command. This structure is put together right before a command is
@ -345,6 +356,7 @@ struct ExecParameters {
const char *cgroup_path;
char **prefix;
const char *received_credentials;
const char *confirm_spawn;
@ -386,6 +398,7 @@ void exec_context_done(ExecContext *c);
void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix);
int exec_context_destroy_runtime_directory(const ExecContext *c, const char *runtime_root);
int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_root, const char *unit);
const char* exec_context_fdname(const ExecContext *c, int fd_index);
@ -418,6 +431,11 @@ void exec_params_clear(ExecParameters *p);
bool exec_context_get_cpu_affinity_from_numa(const ExecContext *c);
ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc);
DEFINE_TRIVIAL_CLEANUP_FUNC(ExecSetCredential*, exec_set_credential_free);
extern const struct hash_ops exec_set_credential_hash_ops;
const char* exec_output_to_string(ExecOutput i) _const_;
ExecOutput exec_output_from_string(const char *s) _pure_;

View File

@ -147,6 +147,8 @@ $1.LogsDirectoryMode, config_parse_mode, 0,
$1.LogsDirectory, config_parse_exec_directories, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_LOGS].paths)
$1.ConfigurationDirectoryMode, config_parse_mode, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].mode)
$1.ConfigurationDirectory, config_parse_exec_directories, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].paths)
$1.SetCredential, config_parse_set_credential, 0, offsetof($1, exec_context)
$1.LoadCredential, config_parse_load_credential, 0, offsetof($1, exec_context)
$1.TimeoutCleanSec, config_parse_sec, 0, offsetof($1, exec_context.timeout_clean_usec)
$1.ProtectHostname, config_parse_bool, 0, offsetof($1, exec_context.protect_hostname)
m4_ifdef(`HAVE_PAM',

View File

@ -60,6 +60,7 @@
#include "unit-name.h"
#include "unit-printf.h"
#include "user-util.h"
#include "utf8.h"
#include "web-util.h"
static int parse_socket_protocol(const char *s) {
@ -4268,6 +4269,155 @@ int config_parse_exec_directories(
}
}
int config_parse_set_credential(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
_cleanup_free_ char *word = NULL, *k = NULL, *unescaped = NULL;
ExecContext *context = data;
ExecSetCredential *old;
Unit *u = userdata;
const char *p;
int r, l;
assert(filename);
assert(lvalue);
assert(rvalue);
assert(context);
if (isempty(rvalue)) {
/* Empty assignment resets the list */
context->set_credentials = hashmap_free(context->set_credentials);
return 0;
}
p = rvalue;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM)
return log_oom();
if (r <= 0 || !p) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
return 0;
}
r = unit_full_printf(u, word, &k);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", word);
return 0;
}
if (!credential_name_valid(k)) {
log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name \"%s\" not valid, ignoring.", k);
return 0;
}
/* We support escape codes here, so that users can insert trailing \n if they like */
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
if (l < 0) {
log_syntax(unit, LOG_WARNING, filename, line, l, "Can't unescape \"%s\", ignoring: %m", p);
return 0;
}
old = hashmap_get(context->set_credentials, k);
if (old) {
free_and_replace(old->data, unescaped);
old->size = l;
} else {
_cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL;
sc = new0(ExecSetCredential, 1);
if (!sc)
return log_oom();
sc->id = TAKE_PTR(k);
sc->data = TAKE_PTR(unescaped);
sc->size = l;
r = hashmap_ensure_allocated(&context->set_credentials, &exec_set_credential_hash_ops);
if (r < 0)
return r;
r = hashmap_put(context->set_credentials, sc->id, sc);
if (r < 0)
return log_oom();
TAKE_PTR(sc);
}
return 0;
}
int config_parse_load_credential(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
_cleanup_free_ char *word = NULL, *k = NULL, *q = NULL;
ExecContext *context = data;
Unit *u = userdata;
const char *p;
int r;
assert(filename);
assert(lvalue);
assert(rvalue);
assert(context);
if (isempty(rvalue)) {
/* Empty assignment resets the list */
context->load_credentials = strv_free(context->load_credentials);
return 0;
}
p = rvalue;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM)
return log_oom();
if (r <= 0) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
return 0;
}
r = unit_full_printf(u, word, &k);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", word);
return 0;
}
if (!credential_name_valid(k)) {
log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name \"%s\" not valid, ignoring.", k);
return 0;
}
r = unit_full_printf(u, p, &q);
if (r < 0) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", p);
return 0;
}
if (path_is_absolute(q) ? !path_is_normalized(q) : !credential_name_valid(q)) {
log_syntax(unit, LOG_WARNING, filename, line, r, "Credential source \"%s\" not valid, ignoring.", q);
return 0;
}
r = strv_consume_pair(&context->load_credentials, TAKE_PTR(k), TAKE_PTR(q));
if (r < 0)
return log_oom();
return 0;
}
int config_parse_set_status(
const char *unit,
const char *filename,

View File

@ -90,6 +90,8 @@ CONFIG_PARSER_PROTOTYPE(config_parse_exec_smack_process_label);
CONFIG_PARSER_PROTOTYPE(config_parse_address_families);
CONFIG_PARSER_PROTOTYPE(config_parse_runtime_preserve_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_exec_directories);
CONFIG_PARSER_PROTOTYPE(config_parse_set_credential);
CONFIG_PARSER_PROTOTYPE(config_parse_load_credential);
CONFIG_PARSER_PROTOTYPE(config_parse_set_status);
CONFIG_PARSER_PROTOTYPE(config_parse_namespace_path_strv);
CONFIG_PARSER_PROTOTYPE(config_parse_temporary_filesystems);

View File

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

View File

@ -424,6 +424,7 @@ struct Manager {
/* Prefixes of e.g. RuntimeDirectory= */
char *prefix[_EXEC_DIRECTORY_TYPE_MAX];
char *received_credentials;
/* Used in the SIGCHLD and sd_notify() message invocation logic to avoid that we dispatch the same event
* multiple times on the same unit. */

View File

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

View File

@ -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

View File

@ -869,7 +869,7 @@ static void mount_enter_dead(Mount *m, MountResult f) {
m->exec_runtime = exec_runtime_unref(m->exec_runtime, true);
unit_destroy_runtime_directory(UNIT(m), &m->exec_context);
unit_destroy_runtime_data(UNIT(m), &m->exec_context);
unit_unref_uid_gid(UNIT(m), true);

View File

@ -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;

View File

@ -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,

View File

@ -1801,7 +1801,7 @@ static void service_enter_dead(Service *s, ServiceResult f, bool allow_restart)
s->exec_runtime = exec_runtime_unref(s->exec_runtime, true);
/* Also, remove the runtime directory */
unit_destroy_runtime_directory(UNIT(s), &s->exec_context);
unit_destroy_runtime_data(UNIT(s), &s->exec_context);
/* Get rid of the IPC bits of the user */
unit_unref_uid_gid(UNIT(s), true);
@ -2156,7 +2156,7 @@ static void service_enter_start(Service *s) {
r = service_spawn(s,
c,
timeout,
EXEC_PASS_FDS|EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_APPLY_TTY_STDIN|EXEC_SET_WATCHDOG,
EXEC_PASS_FDS|EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_APPLY_TTY_STDIN|EXEC_SET_WATCHDOG|EXEC_WRITE_CREDENTIALS,
&pid);
if (r < 0)
goto fail;

View File

@ -2080,7 +2080,7 @@ static void socket_enter_dead(Socket *s, SocketResult f) {
s->exec_runtime = exec_runtime_unref(s->exec_runtime, true);
unit_destroy_runtime_directory(UNIT(s), &s->exec_context);
unit_destroy_runtime_data(UNIT(s), &s->exec_context);
unit_unref_uid_gid(UNIT(s), true);

View File

@ -706,7 +706,7 @@ static void swap_enter_dead(Swap *s, SwapResult f) {
s->exec_runtime = exec_runtime_unref(s->exec_runtime, true);
unit_destroy_runtime_directory(UNIT(s), &s->exec_context);
unit_destroy_runtime_data(UNIT(s), &s->exec_context);
unit_unref_uid_gid(UNIT(s), true);

View File

@ -5429,6 +5429,8 @@ int unit_set_exec_params(Unit *u, ExecParameters *p) {
p->cgroup_path = u->cgroup_path;
SET_FLAG(p->flags, EXEC_CGROUP_DELEGATE, unit_cgroup_delegate(u));
p->received_credentials = u->manager->received_credentials;
return 0;
}
@ -6123,10 +6125,15 @@ int unit_test_trigger_loaded(Unit *u) {
return 0;
}
void unit_destroy_runtime_directory(Unit *u, const ExecContext *context) {
void unit_destroy_runtime_data(Unit *u, const ExecContext *context) {
assert(u);
assert(context);
if (context->runtime_directory_preserve_mode == EXEC_PRESERVE_NO ||
(context->runtime_directory_preserve_mode == EXEC_PRESERVE_RESTART && !unit_will_restart(u)))
exec_context_destroy_runtime_directory(context, u->manager->prefix[EXEC_DIRECTORY_RUNTIME]);
exec_context_destroy_credentials(context, u->manager->prefix[EXEC_DIRECTORY_RUNTIME], u->id);
}
int unit_clean(Unit *u, ExecCleanMask mask) {

View File

@ -880,7 +880,7 @@ int unit_failure_action_exit_status(Unit *u);
int unit_test_trigger_loaded(Unit *u);
void unit_destroy_runtime_directory(Unit *u, const ExecContext *context);
void unit_destroy_runtime_data(Unit *u, const ExecContext *context);
int unit_clean(Unit *u, ExecCleanMask mask);
int unit_can_clean(Unit *u, ExecCleanMask *ret_mask);

View File

@ -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

View File

@ -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

View File

@ -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

25
src/nspawn/nspawn-creds.c Normal file
View File

@ -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);
}

12
src/nspawn/nspawn-creds.h Normal file
View File

@ -0,0 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <sys/types.h>
typedef struct Credential {
char *id;
void *data;
size_t size;
} Credential;
void credential_free_all(Credential *creds, size_t n);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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. */

View File

@ -973,6 +973,117 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
return 1;
}
if (streq(field, "SetCredential")) {
r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append_basic(m, 's', "SetCredential");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_open_container(m, 'v', "a(say)");
if (r < 0)
return bus_log_create_error(r);
if (isempty(eq))
r = sd_bus_message_append(m, "a(say)", 0);
else {
_cleanup_free_ char *word = NULL, *unescaped = NULL;
const char *p = eq;
int l;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM)
return log_oom();
if (r < 0)
return log_error_errno(r, "Failed to parse SetCredential= parameter: %s", eq);
if (r == 0 || !p)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to SetCredential=.");
l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped);
if (l < 0)
return log_error_errno(l, "Failed to unescape SetCredential= value: %s", p);
r = sd_bus_message_open_container(m, 'a', "(say)");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_open_container(m, 'r', "say");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append(m, "s", word);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append_array(m, 'y', unescaped, l);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_close_container(m);
}
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
return 1;
}
if (streq(field, "LoadCredential")) {
r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append_basic(m, 's', "LoadCredential");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_open_container(m, 'v', "a(ss)");
if (r < 0)
return bus_log_create_error(r);
if (isempty(eq))
r = sd_bus_message_append(m, "a(ss)", 0);
else {
_cleanup_free_ char *word = NULL;
const char *p = eq;
r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
if (r == -ENOMEM)
return log_oom();
if (r < 0)
return log_error_errno(r, "Failed to parse LoadCredential= parameter: %s", eq);
if (r == 0 || !p)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to LoadCredential=.");
r = sd_bus_message_append(m, "a(ss)", 1, word, p);
}
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
return 1;
}
if (streq(field, "LogExtraFields")) {
r = sd_bus_message_open_container(m, 'r', "sv");
if (r < 0)

View File

@ -70,6 +70,8 @@ const ExitStatusMapping exit_status_mappings[256] = {
[EXIT_LOGS_DIRECTORY] = { "LOGS_DIRECTORY", EXIT_STATUS_SYSTEMD },
[EXIT_CONFIGURATION_DIRECTORY] = { "CONFIGURATION_DIRECTORY", EXIT_STATUS_SYSTEMD },
[EXIT_NUMA_POLICY] = { "NUMA_POLICY", EXIT_STATUS_SYSTEMD },
[EXIT_CREDENTIALS] = { "CREDENTIALS", EXIT_STATUS_SYSTEMD },
[EXIT_EXCEPTION] = { "EXCEPTION", EXIT_STATUS_SYSTEMD },
[EXIT_INVALIDARGUMENT] = { "INVALIDARGUMENT", EXIT_STATUS_LSB },

View File

@ -70,6 +70,7 @@ enum {
EXIT_LOGS_DIRECTORY, /* 240 */
EXIT_CONFIGURATION_DIRECTORY,
EXIT_NUMA_POLICY,
EXIT_CREDENTIALS,
EXIT_EXCEPTION = 255, /* Whenever we want to propagate an abnormal/signal exit, in line with bash */
};

View File

@ -658,6 +658,10 @@ tests += [
[],
[]],
[['src/test/test-rm-rf.c'],
[],
[]],
[['src/test/test-chase-symlinks.c'],
[],
[],

View File

@ -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);

View File

@ -163,6 +163,7 @@ static void test_protect_kernel_logs(void) {
NULL,
NULL,
NULL,
NULL,
0,
NULL,
0,

View File

@ -78,6 +78,7 @@ int main(int argc, char *argv[]) {
tmp_dir,
var_tmp_dir,
NULL,
NULL,
0,
NULL,
0,

74
src/test/test-rm-rf.c Normal file
View File

@ -0,0 +1,74 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include <unistd.h>
#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;
}

1
test/TEST-54-CREDS/Makefile Symbolic link
View File

@ -0,0 +1 @@
../TEST-01-BASIC/Makefile

7
test/TEST-54-CREDS/test.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
TEST_DESCRIPTION="test credentials"
. $TEST_BASE_DIR/test-functions
do_test "$@" 54

View File

@ -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

31
test/units/testsuite-54.sh Executable file
View File

@ -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