]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: teach LoadCredential= to load from a directory
authorAlbert Brox <albert@exypno.tech>
Sat, 24 Jul 2021 16:38:22 +0000 (12:38 -0400)
committerLuca Boccassi <luca.boccassi@gmail.com>
Sat, 8 Jan 2022 13:17:51 +0000 (13:17 +0000)
TODO
man/systemd.exec.xml
src/core/execute.c
test/units/testsuite-54.sh

diff --git a/TODO b/TODO
index 47ae975b3eda46f1bb0a06cde6a3cc94af95c8c2..d00e56eca490b2e2fab7463c43e31db228d68dee 100644 (file)
--- a/TODO
+++ b/TODO
@@ -303,9 +303,6 @@ Features:
   - make gatwayd/remote read key via creds logic
   - add sd_notify() command for flushing out creds not needed anymore
 
-* teach LoadCredential= the ability to load all files from a specified dir as
-  individual creds
-
 * add tpm.target or so which is delayed until TPM2 device showed up in case
   firmware indicates there is one.
 
index 3378689b7599aa71ab6099b6c3c20e9e0198b357..079ff14aeaffc6357922b0e9130b828dce13fedf 100644 (file)
@@ -3007,7 +3007,10 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
         newline characters and <constant>NUL</constant> bytes. If the file system path is omitted it is
         chosen identical to the credential name, i.e. this is a terse way do declare credentials to inherit
         from the service manager into a service. This option may be used multiple times, each time defining
-        an additional credential to pass to the unit.</para>
+        an additional credential to pass to the unit. Alternatively, if the path is a directory, every file
+        in that directory will be loaded as a separate credential. The ID for each credential will be the
+        provided ID suffixed with <literal>_$FILENAME</literal> (e.g., <literal>Key_file1</literal>). When
+        loading from a directory, symlinks will be ignored.</para>
 
         <para>The <varname>LoadCredentialEncrypted=</varname> setting is identical to
         <varname>LoadCredential=</varname>, except that the credential data is decrypted before being passed
index 4c96c30cf476566b4e7c11f6136459f50e925e13..fec4f65884d3891ab7f1133bd67008f5ffa2f4a4 100644 (file)
@@ -80,6 +80,7 @@
 #include "path-util.h"
 #include "process-util.h"
 #include "random-util.h"
+#include "recurse-dir.h"
 #include "rlimit-util.h"
 #include "rm-rf.h"
 #if HAVE_SECCOMP
@@ -2613,6 +2614,168 @@ static int write_credential(
         return 0;
 }
 
+static int load_credential(
+                const ExecContext *context,
+                const ExecParameters *params,
+                ExecLoadCredential *lc,
+                const char *unit,
+                int read_dfd,
+                int write_dfd,
+                uid_t uid,
+                bool ownership_ok,
+                uint64_t *left) {
+
+        assert(context);
+        assert(lc);
+        assert(unit);
+        assert(write_dfd >= 0);
+        assert(left);
+
+        ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
+        _cleanup_(erase_and_freep) char *data = NULL;
+        _cleanup_free_ char *j = NULL, *bindname = NULL;
+        bool missing_ok = true;
+        const char *source;
+        size_t size, add;
+        int r;
+
+        if (path_is_absolute(lc->path) || read_dfd >= 0) {
+                /* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */
+                source = lc->path;
+                flags |= READ_FULL_FILE_CONNECT_SOCKET;
+
+                /* Pass some minimal info about the unit and the credential name we are looking to acquire
+                 * via the source socket address in case we read off an AF_UNIX socket. */
+                if (asprintf(&bindname, "@%" PRIx64"/unit/%s/%s", random_u64(), unit, lc->id) < 0)
+                        return -ENOMEM;
+
+                missing_ok = false;
+
+        } 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, lc->path);
+                if (!j)
+                        return -ENOMEM;
+
+                source = j;
+        } else
+                source = NULL;
+
+        if (source)
+                r = read_full_file_full(
+                                read_dfd, source,
+                                UINT64_MAX,
+                                lc->encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX,
+                                flags | (lc->encrypted ? READ_FULL_FILE_UNBASE64 : 0),
+                                bindname,
+                                &data, &size);
+        else
+                r = -ENOENT;
+
+        if (r == -ENOENT && (missing_ok || hashmap_contains(context->set_credentials, lc->id))) {
+                /* Make a missing inherited credential non-fatal, let's just continue. After all apps
+                 * will get clear errors if we don't pass such a missing credential on as they
+                 * themselves will get ENOENT when trying to read them, which should not be much
+                 * worse than when we handle the error here and make it fatal.
+                 *
+                 * Also, if the source file doesn't exist, but a fallback is set via SetCredentials=
+                 * we are fine, too. */
+                log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", lc->path);
+                return 0;
+        }
+        if (r < 0)
+                return log_debug_errno(r, "Failed to read credential '%s': %m", lc->path);
+
+        if (lc->encrypted) {
+                _cleanup_free_ void *plaintext = NULL;
+                size_t plaintext_size = 0;
+
+                r = decrypt_credential_and_warn(lc->id, now(CLOCK_REALTIME), NULL, data, size, &plaintext, &plaintext_size);
+                if (r < 0)
+                        return r;
+
+                free_and_replace(data, plaintext);
+                size = plaintext_size;
+        }
+
+        add = strlen(lc->id) + size;
+        if (add > *left)
+                return -E2BIG;
+
+        r = write_credential(write_dfd, lc->id, data, size, uid, ownership_ok);
+        if (r < 0)
+                return r;
+
+        *left -= add;
+        return 0;
+}
+
+struct load_cred_args {
+        Set *seen_creds;
+
+        const ExecContext *context;
+        const ExecParameters *params;
+        ExecLoadCredential *parent_local_credential;
+        const char *unit;
+        int dfd;
+        uid_t uid;
+        bool ownership_ok;
+        uint64_t *left;
+};
+
+static int load_cred_recurse_dir_cb(
+                RecurseDirEvent event,
+                const char *path,
+                int dir_fd,
+                int inode_fd,
+                const struct dirent *de,
+                const struct statx *sx,
+                void *userdata) {
+
+        _cleanup_free_ char *credname = NULL, *sub_id = NULL;
+        struct load_cred_args *args = userdata;
+        int r;
+
+        if (event != RECURSE_DIR_ENTRY)
+                return RECURSE_DIR_CONTINUE;
+
+        if (!IN_SET(de->d_type, DT_REG, DT_SOCK))
+                return RECURSE_DIR_CONTINUE;
+
+        credname = strreplace(path, "/", "_");
+        if (!credname)
+                return -ENOMEM;
+
+        sub_id = strjoin(args->parent_local_credential->id, "_", credname);
+        if (!sub_id)
+                return -ENOMEM;
+
+        if (!credential_name_valid(sub_id))
+                return -EINVAL;
+
+        if (set_contains(args->seen_creds, sub_id)) {
+                log_debug("Skipping credential with duplicated ID %s at %s", sub_id, path);
+                return RECURSE_DIR_CONTINUE;
+        }
+
+        r = set_put_strdup(&args->seen_creds, sub_id);
+        if (r < 0)
+                return r;
+
+        r = load_credential(args->context, args->params,
+                &(ExecLoadCredential) {
+                        .id = sub_id,
+                        .path = (char *) de->d_name,
+                        .encrypted = args->parent_local_credential->encrypted,
+                }, args->unit, dir_fd, args->dfd, args->uid, args->ownership_ok, args->left);
+        if (r < 0)
+                return r;
+
+        return RECURSE_DIR_CONTINUE;
+}
+
 static int acquire_credentials(
                 const ExecContext *context,
                 const ExecParameters *params,
@@ -2623,6 +2786,7 @@ static int acquire_credentials(
 
         uint64_t left = CREDENTIALS_TOTAL_SIZE_MAX;
         _cleanup_close_ int dfd = -1;
+        _cleanup_set_free_ Set *seen_creds = NULL;
         ExecLoadCredential *lc;
         ExecSetCredential *sc;
         int r;
@@ -2634,84 +2798,53 @@ static int acquire_credentials(
         if (dfd < 0)
                 return -errno;
 
+        seen_creds = set_new(&string_hash_ops_free);
+        if (!seen_creds)
+                return -ENOMEM;
+
         /* First, load credentials off disk (or acquire via AF_UNIX socket) */
         HASHMAP_FOREACH(lc, context->load_credentials) {
-                ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
-                _cleanup_(erase_and_freep) char *data = NULL;
-                _cleanup_free_ char *j = NULL, *bindname = NULL;
-                bool missing_ok = true;
-                const char *source;
-                size_t size, add;
-
-                if (path_is_absolute(lc->path)) {
-                        /* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */
-                        source = lc->path;
-                        flags |= READ_FULL_FILE_CONNECT_SOCKET;
-
-                        /* Pass some minimal info about the unit and the credential name we are looking to acquire
-                         * via the source socket address in case we read off an AF_UNIX socket. */
-                        if (asprintf(&bindname, "@%" PRIx64"/unit/%s/%s", random_u64(), unit, lc->id) < 0)
-                                return -ENOMEM;
-
-                        missing_ok = false;
+                _cleanup_close_ int sub_fd = -1;
 
-                } 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, lc->path);
-                        if (!j)
-                                return -ENOMEM;
-
-                        source = j;
-                } else
-                        source = NULL;
-
-                if (source)
-                        r = read_full_file_full(
-                                        AT_FDCWD, source,
-                                        UINT64_MAX,
-                                        lc->encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX,
-                                        flags | (lc->encrypted ? READ_FULL_FILE_UNBASE64 : 0),
-                                        bindname,
-                                        &data, &size);
-                else
-                        r = -ENOENT;
-                if (r == -ENOENT && (missing_ok || hashmap_contains(context->set_credentials, lc->id))) {
-                        /* Make a missing inherited credential non-fatal, let's just continue. After all apps
-                         * will get clear errors if we don't pass such a missing credential on as they
-                         * themselves will get ENOENT when trying to read them, which should not be much
-                         * worse than when we handle the error here and make it fatal.
-                         *
-                         * Also, if the source file doesn't exist, but a fallback is set via SetCredentials=
-                         * we are fine, too. */
-                        log_debug_errno(r, "Couldn't read inherited credential '%s', skipping: %m", lc->path);
+                /* Skip over credentials with unspecified paths. These are received by the
+                 * service manager via the $CREDENTIALS_DIRECTORY environment variable. */
+                if (!is_path(lc->path) && streq(lc->id, lc->path))
                         continue;
-                }
-                if (r < 0)
-                        return log_debug_errno(r, "Failed to read credential '%s': %m", lc->path);
 
-                if (lc->encrypted) {
-                        _cleanup_free_ void *plaintext = NULL;
-                        size_t plaintext_size = 0;
+                sub_fd = open(lc->path, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
+                if (sub_fd < 0 && errno != ENOTDIR)
+                        return -errno;
 
-                        r = decrypt_credential_and_warn(lc->id, now(CLOCK_REALTIME), NULL, data, size, &plaintext, &plaintext_size);
+                if (sub_fd < 0) {
+                        r = set_put_strdup(&seen_creds, lc->id);
+                        if (r < 0)
+                                return r;
+                        r = load_credential(context, params, lc, unit, -1, dfd, uid, ownership_ok, &left);
                         if (r < 0)
                                 return r;
 
-                        free_and_replace(data, plaintext);
-                        size = plaintext_size;
+                } else {
+                        r = recurse_dir(
+                                        sub_fd,
+                                        /* path= */ "",
+                                        /* statx_mask= */ 0,
+                                        /* n_depth_max= */ UINT_MAX,
+                                        RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE,
+                                        load_cred_recurse_dir_cb,
+                                        &(struct load_cred_args) {
+                                                .seen_creds = seen_creds,
+                                                .context = context,
+                                                .params = params,
+                                                .parent_local_credential = lc,
+                                                .unit = unit,
+                                                .dfd = dfd,
+                                                .uid = uid,
+                                                .ownership_ok = ownership_ok,
+                                                .left = &left,
+                                        });
+                        if (r < 0)
+                                return r;
                 }
-
-                add = strlen(lc->id) + size;
-                if (add > left)
-                        return -E2BIG;
-
-                r = write_credential(dfd, lc->id, data, size, uid, ownership_ok);
-                if (r < 0)
-                        return r;
-
-                left -= add;
         }
 
         /* First we use the literally specified credentials. Note that they might be overridden again below,
index 6fc32b0a5b5574551bf498d08b2a21143dd413fb..c8b685b675c4268a0e3597e6afd5c4de97d9428a 100755 (executable)
@@ -28,8 +28,25 @@ systemd-run -p LoadCredential=passwd:/etc/passwd \
             rm '${CREDENTIALS_DIRECTORY}/passwd' \
     && { echo 'unexpected success'; exit 1; }
 
-# Now test encrypted credentials (only supported when built with OpenSSL though)
+# Check directory-based loading
+mkdir -p /tmp/ts54-creds/sub
+echo -n a >/tmp/ts54-creds/foo
+echo -n b >/tmp/ts54-creds/bar
+echo -n c >/tmp/ts54-creds/baz
+echo -n d >/tmp/ts54-creds/sub/qux
+systemd-run -p LoadCredential=cred:/tmp/ts54-creds \
+            -p DynamicUser=1 \
+            --wait \
+            --pipe \
+            cat '${CREDENTIALS_DIRECTORY}/cred_foo' \
+                '${CREDENTIALS_DIRECTORY}/cred_bar' \
+                '${CREDENTIALS_DIRECTORY}/cred_baz' \
+                '${CREDENTIALS_DIRECTORY}/cred_sub_qux' >/tmp/ts54-concat
+( echo -n abcd ) | cmp /tmp/ts54-concat
+rm /tmp/ts54-concat
+rm -rf /tmp/ts54-creds
 
+# Now test encrypted credentials (only supported when built with OpenSSL though)
 if systemctl --version | grep -q -- +OPENSSL ; then
     echo -n $RANDOM >/tmp/test-54-plaintext
     systemd-creds encrypt --name=test-54 /tmp/test-54-plaintext /tmp/test-54-ciphertext