From: Albert Brox Date: Sat, 24 Jul 2021 16:38:22 +0000 (-0400) Subject: core: teach LoadCredential= to load from a directory X-Git-Tag: v251-rc1~569 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3989bdc1ad7cca4d75c06cdf601fea2cb37ba337;p=thirdparty%2Fsystemd.git core: teach LoadCredential= to load from a directory --- diff --git a/TODO b/TODO index 47ae975b3ed..d00e56eca49 100644 --- 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. diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 3378689b759..079ff14aeaf 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -3007,7 +3007,10 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy newline characters and NUL 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. + 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 _$FILENAME (e.g., Key_file1). When + loading from a directory, symlinks will be ignored. The LoadCredentialEncrypted= setting is identical to LoadCredential=, except that the credential data is decrypted before being passed diff --git a/src/core/execute.c b/src/core/execute.c index 4c96c30cf47..fec4f65884d 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -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, diff --git a/test/units/testsuite-54.sh b/test/units/testsuite-54.sh index 6fc32b0a5b5..c8b685b675c 100755 --- a/test/units/testsuite-54.sh +++ b/test/units/testsuite-54.sh @@ -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