diff --git a/src/nspawn/meson.build b/src/nspawn/meson.build index c049ac6754..ae3d72faca 100644 --- a/src/nspawn/meson.build +++ b/src/nspawn/meson.build @@ -3,6 +3,8 @@ libnspawn_core_sources = files(''' nspawn-cgroup.c nspawn-cgroup.h + nspawn-creds.c + nspawn-creds.h nspawn-def.h nspawn-expose-ports.c nspawn-expose-ports.h diff --git a/src/nspawn/nspawn-creds.c b/src/nspawn/nspawn-creds.c new file mode 100644 index 0000000000..41a38d37ea --- /dev/null +++ b/src/nspawn/nspawn-creds.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "alloc-util.h" +#include "macro.h" +#include "memory-util.h" +#include "nspawn-creds.h" + +static void credential_free(Credential *cred) { + assert(cred); + + cred->id = mfree(cred->id); + cred->data = erase_and_free(cred->data); + cred->size = 0; +} + +void credential_free_all(Credential *creds, size_t n) { + size_t i; + + assert(creds || n == 0); + + for (i = 0; i < n; i++) + credential_free(creds + i); + + free(creds); +} diff --git a/src/nspawn/nspawn-creds.h b/src/nspawn/nspawn-creds.h new file mode 100644 index 0000000000..b3c90bb17a --- /dev/null +++ b/src/nspawn/nspawn-creds.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +typedef struct Credential { + char *id; + void *data; + size_t size; +} Credential; + +void credential_free_all(Credential *creds, size_t n); diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index ab31c05a9e..b8fa145f77 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -116,9 +116,10 @@ typedef enum SettingsMask { SETTING_USE_CGNS = UINT64_C(1) << 27, SETTING_CLONE_NS_FLAGS = UINT64_C(1) << 28, SETTING_CONSOLE_MODE = UINT64_C(1) << 29, - SETTING_RLIMIT_FIRST = UINT64_C(1) << 30, /* we define one bit per resource limit here */ - SETTING_RLIMIT_LAST = UINT64_C(1) << (30 + _RLIMIT_MAX - 1), - _SETTINGS_MASK_ALL = (UINT64_C(1) << (30 + _RLIMIT_MAX)) -1, + SETTING_CREDENTIALS = UINT64_C(1) << 30, + SETTING_RLIMIT_FIRST = UINT64_C(1) << 31, /* we define one bit per resource limit here */ + SETTING_RLIMIT_LAST = UINT64_C(1) << (31 + _RLIMIT_MAX - 1), + _SETTINGS_MASK_ALL = (UINT64_C(1) << (31 + _RLIMIT_MAX)) -1, _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX } SettingsMask; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3ad8829855..26469759d9 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -36,6 +36,7 @@ #include "dev-setup.h" #include "dissect-image.h" #include "env-util.h" +#include "escape.h" #include "fd-util.h" #include "fdset.h" #include "fileio.h" @@ -45,6 +46,7 @@ #include "hexdecoct.h" #include "hostname-util.h" #include "id128-util.h" +#include "io-util.h" #include "log.h" #include "loop-util.h" #include "loopback-setup.h" @@ -58,6 +60,7 @@ #include "namespace-util.h" #include "netlink-util.h" #include "nspawn-cgroup.h" +#include "nspawn-creds.h" #include "nspawn-def.h" #include "nspawn-expose-ports.h" #include "nspawn-mount.h" @@ -219,6 +222,8 @@ static DeviceNode* arg_extra_nodes = NULL; static size_t arg_n_extra_nodes = 0; static char **arg_sysctl = NULL; static ConsoleMode arg_console_mode = _CONSOLE_MODE_INVALID; +static Credential *arg_credentials = NULL; +static size_t arg_n_credentials = 0; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_template, freep); @@ -406,7 +411,13 @@ static int help(void) { "%3$sInput/Output:%4$s\n" " --console=MODE Select how stdin/stdout/stderr and /dev/console are\n" " set up for the container.\n" - " -P --pipe Equivalent to --console=pipe\n" + " -P --pipe Equivalent to --console=pipe\n\n" + "%3$sCredentials:%4$s\n" + " --set-credential=ID:VALUE\n" + " Pass a credential with literal value to container.\n" + " --load-credential=ID:PATH\n" + " Load credential to pass to container from file or\n" + " AF_UNIX stream socket.\n" "\nSee the %2$s for details.\n" , program_invocation_short_name , link @@ -675,6 +686,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_PAGER, ARG_VERITY_DATA, ARG_ROOT_HASH_SIG, + ARG_SET_CREDENTIAL, + ARG_LOAD_CREDENTIAL, }; static const struct option options[] = { @@ -742,6 +755,8 @@ static int parse_argv(int argc, char *argv[]) { { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "verity-data", required_argument, NULL, ARG_VERITY_DATA }, { "root-hash-sig", required_argument, NULL, ARG_ROOT_HASH_SIG }, + { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, + { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, {} }; @@ -1496,6 +1511,105 @@ static int parse_argv(int argc, char *argv[]) { arg_pager_flags |= PAGER_DISABLE; break; + case ARG_SET_CREDENTIAL: { + _cleanup_free_ char *word = NULL, *data = NULL; + const char *p = optarg; + Credential *a; + size_t i; + int l; + + r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Failed to parse --set-credential= parameter: %m"); + if (r == 0 || !p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing value for --set-credential=: %s", optarg); + + if (!credential_name_valid(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name is not valid: %s", word); + + for (i = 0; i < arg_n_credentials; i++) + if (streq(arg_credentials[i].id, word)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate credential '%s', refusing.", word); + + l = cunescape(p, UNESCAPE_ACCEPT_NUL, &data); + if (l < 0) + return log_error_errno(l, "Failed to unescape credential data: %s", p); + + a = reallocarray(arg_credentials, arg_n_credentials + 1, sizeof(Credential)); + if (!a) + return log_oom(); + + a[arg_n_credentials++] = (Credential) { + .id = TAKE_PTR(word), + .data = TAKE_PTR(data), + .size = l, + }; + + arg_credentials = a; + + arg_settings_mask |= SETTING_CREDENTIALS; + break; + } + + case ARG_LOAD_CREDENTIAL: { + ReadFullFileFlags flags = READ_FULL_FILE_SECURE; + _cleanup_(erase_and_freep) char *data = NULL; + _cleanup_free_ char *word = NULL, *j = NULL; + const char *p = optarg; + Credential *a; + size_t size, i; + + r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Failed to parse --set-credential= parameter: %m"); + if (r == 0 || !p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing value for --set-credential=: %s", optarg); + + if (!credential_name_valid(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name is not valid: %s", word); + + for (i = 0; i < arg_n_credentials; i++) + if (streq(arg_credentials[i].id, word)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Duplicate credential '%s', refusing.", word); + + if (path_is_absolute(p)) + flags |= READ_FULL_FILE_CONNECT_SOCKET; + else { + const char *e; + + e = getenv("CREDENTIALS_DIRECTORY"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential not available (no credentials passed at all): %s", word); + + j = path_join(e, p); + if (!j) + return log_oom(); + } + + r = read_full_file_full(AT_FDCWD, j ?: p, flags, &data, &size); + if (r < 0) + return log_error_errno(r, "Failed to read credential '%s': %m", j ?: p); + + a = reallocarray(arg_credentials, arg_n_credentials + 1, sizeof(Credential)); + if (!a) + return log_oom(); + + a[arg_n_credentials++] = (Credential) { + .id = TAKE_PTR(word), + .data = TAKE_PTR(data), + .size = size, + }; + + arg_credentials = a; + + arg_settings_mask |= SETTING_CREDENTIALS; + break; + } + case '?': return -EINVAL; @@ -2228,6 +2342,66 @@ static int setup_keyring(void) { return 0; } +static int setup_credentials(const char *root) { + const char *q; + int r; + + if (arg_n_credentials <= 0) + return 0; + + r = userns_mkdir(root, "/run/host", 0755, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to create /run/host: %m"); + + r = userns_mkdir(root, "/run/host/credentials", 0700, 0, 0); + if (r < 0) + return log_error_errno(r, "Failed to create /run/host/credentials: %m"); + + q = prefix_roota(root, "/run/host/credentials"); + r = mount_verbose(LOG_ERR, NULL, q, "ramfs", MS_NOSUID|MS_NOEXEC|MS_NODEV, "mode=0700"); + if (r < 0) + return r; + + for (size_t i = 0; i < arg_n_credentials; i++) { + _cleanup_free_ char *j = NULL; + _cleanup_close_ int fd = -1; + + j = path_join(q, arg_credentials[i].id); + if (!j) + return log_oom(); + + fd = open(j, O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC|O_NOFOLLOW, 0600); + if (fd < 0) + return log_error_errno(errno, "Failed to create credential file %s: %m", j); + + r = loop_write(fd, arg_credentials[i].data, arg_credentials[i].size, /* do_poll= */ false); + if (r < 0) + return log_error_errno(r, "Failed to write credential to file %s: %m", j); + + if (fchmod(fd, 0400) < 0) + return log_error_errno(errno, "Failed to adjust access mode of %s: %m", j); + + if (arg_userns_mode != USER_NAMESPACE_NO) { + if (fchown(fd, arg_uid_shift, arg_uid_shift) < 0) + return log_error_errno(errno, "Failed to adjust ownership of %s: %m", j); + } + } + + if (chmod(q, 0500) < 0) + return log_error_errno(errno, "Failed to adjust access mode of %s: %m", q); + + r = userns_lchown(q, 0, 0); + if (r < 0) + return r; + + /* Make both mount and superblock read-only now */ + r = mount_verbose(LOG_ERR, NULL, q, NULL, MS_REMOUNT|MS_BIND|MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL); + if (r < 0) + return r; + + return mount_verbose(LOG_ERR, NULL, q, NULL, MS_REMOUNT|MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, "mode=0500"); +} + static int setup_kmsg(int kmsg_socket) { _cleanup_(unlink_and_freep) char *from = NULL; _cleanup_free_ char *fifo = NULL; @@ -2941,6 +3115,7 @@ static int inner_child( NULL, /* LISTEN_FDS */ NULL, /* LISTEN_PID */ NULL, /* NOTIFY_SOCKET */ + NULL, /* CREDENTIALS_DIRECTORY */ NULL }; const char *exec_target; @@ -3191,6 +3366,13 @@ static int inner_child( if (asprintf((char **)(envp + n_env++), "NOTIFY_SOCKET=%s", NSPAWN_NOTIFY_SOCKET_PATH) < 0) return log_oom(); + if (arg_n_credentials > 0) { + envp[n_env] = strdup("CREDENTIALS_DIRECTORY=/run/host/credentials"); + if (!envp[n_env]) + return log_oom(); + n_env++; + } + env_use = strv_env_merge(3, envp, os_release_pairs, arg_setenv); if (!env_use) return log_oom(); @@ -3538,6 +3720,10 @@ static int outer_child( if (r < 0) return r; + r = setup_credentials(directory); + if (r < 0) + return r; + r = mount_custom( directory, arg_custom_mounts, @@ -5339,6 +5525,7 @@ finish: expose_port_free_all(arg_expose_ports); rlimit_free_all(arg_rlimit); device_node_array_free(arg_extra_nodes, arg_n_extra_nodes); + credential_free_all(arg_credentials, arg_n_credentials); if (r < 0) return r;