summaryrefslogtreecommitdiffstats
path: root/src/nspawn/nspawn-bind-user.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nspawn/nspawn-bind-user.c')
-rw-r--r--src/nspawn/nspawn-bind-user.c474
1 files changed, 474 insertions, 0 deletions
diff --git a/src/nspawn/nspawn-bind-user.c b/src/nspawn/nspawn-bind-user.c
new file mode 100644
index 0000000..61d8d30
--- /dev/null
+++ b/src/nspawn/nspawn-bind-user.c
@@ -0,0 +1,474 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "chase.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "nspawn-bind-user.h"
+#include "nspawn.h"
+#include "path-util.h"
+#include "user-util.h"
+#include "userdb.h"
+
+static int check_etc_passwd_collisions(
+ const char *directory,
+ const char *name,
+ uid_t uid) {
+
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(directory);
+ assert(name || uid_is_valid(uid));
+
+ r = chase_and_fopen_unlocked("/etc/passwd", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
+ if (r == -ENOENT)
+ return 0; /* no user database? then no user, hence no collision */
+ if (r < 0)
+ return log_error_errno(r, "Failed to open /etc/passwd of container: %m");
+
+ for (;;) {
+ struct passwd *pw;
+
+ r = fgetpwent_sane(f, &pw);
+ if (r < 0)
+ return log_error_errno(r, "Failed to iterate through /etc/passwd of container: %m");
+ if (r == 0) /* EOF */
+ return 0; /* no collision */
+
+ if (name && streq_ptr(pw->pw_name, name))
+ return 1; /* name collision */
+ if (uid_is_valid(uid) && pw->pw_uid == uid)
+ return 1; /* UID collision */
+ }
+}
+
+static int check_etc_group_collisions(
+ const char *directory,
+ const char *name,
+ gid_t gid) {
+
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert(directory);
+ assert(name || gid_is_valid(gid));
+
+ r = chase_and_fopen_unlocked("/etc/group", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
+ if (r == -ENOENT)
+ return 0; /* no group database? then no group, hence no collision */
+ if (r < 0)
+ return log_error_errno(r, "Failed to open /etc/group of container: %m");
+
+ for (;;) {
+ struct group *gr;
+
+ r = fgetgrent_sane(f, &gr);
+ if (r < 0)
+ return log_error_errno(r, "Failed to iterate through /etc/group of container: %m");
+ if (r == 0)
+ return 0; /* no collision */
+
+ if (name && streq_ptr(gr->gr_name, name))
+ return 1; /* name collision */
+ if (gid_is_valid(gid) && gr->gr_gid == gid)
+ return 1; /* gid collision */
+ }
+}
+
+static int convert_user(
+ const char *directory,
+ UserRecord *u,
+ GroupRecord *g,
+ uid_t allocate_uid,
+ UserRecord **ret_converted_user,
+ GroupRecord **ret_converted_group) {
+
+ _cleanup_(group_record_unrefp) GroupRecord *converted_group = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *converted_user = NULL;
+ _cleanup_free_ char *h = NULL;
+ JsonVariant *p, *hp = NULL;
+ int r;
+
+ assert(u);
+ assert(g);
+ assert(u->gid == g->gid);
+
+ r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
+ "Sorry, the user '%s' already exists in the container.", u->user_name);
+
+ r = check_etc_group_collisions(directory, g->group_name, GID_INVALID);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
+ "Sorry, the group '%s' already exists in the container.", g->group_name);
+
+ h = path_join("/run/host/home/", u->user_name);
+ if (!h)
+ return log_oom();
+
+ /* Acquire the source hashed password array as-is, so that it retains the JSON_VARIANT_SENSITIVE flag */
+ p = json_variant_by_key(u->json, "privileged");
+ if (p)
+ hp = json_variant_by_key(p, "hashedPassword");
+
+ r = user_record_build(
+ &converted_user,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(u->user_name)),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(allocate_uid)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid)),
+ JSON_BUILD_PAIR_CONDITION(u->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(u->disposition))),
+ JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(h)),
+ JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
+ JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "privileged", JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_VARIANT(hp))))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build container user record: %m");
+
+ r = group_record_build(
+ &converted_group,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid)),
+ JSON_BUILD_PAIR_CONDITION(g->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(g->disposition))),
+ JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn"))));
+ if (r < 0)
+ return log_error_errno(r, "Failed to build container group record: %m");
+
+ *ret_converted_user = TAKE_PTR(converted_user);
+ *ret_converted_group = TAKE_PTR(converted_group);
+
+ return 0;
+}
+
+static int find_free_uid(const char *directory, uid_t max_uid, uid_t *current_uid) {
+ int r;
+
+ assert(directory);
+ assert(current_uid);
+
+ for (;; (*current_uid) ++) {
+ if (*current_uid > MAP_UID_MAX || *current_uid > max_uid)
+ return log_error_errno(
+ SYNTHETIC_ERRNO(EBUSY),
+ "No suitable available UID in range " UID_FMT "…" UID_FMT " in container detected, can't map user.",
+ MAP_UID_MIN, MAP_UID_MAX);
+
+ r = check_etc_passwd_collisions(directory, NULL, *current_uid);
+ if (r < 0)
+ return r;
+ if (r > 0) /* already used */
+ continue;
+
+ /* We want to use the UID also as GID, hence check for it in /etc/group too */
+ r = check_etc_group_collisions(directory, NULL, (gid_t) *current_uid);
+ if (r <= 0)
+ return r;
+ }
+}
+
+BindUserContext* bind_user_context_free(BindUserContext *c) {
+ if (!c)
+ return NULL;
+
+ assert(c->n_data == 0 || c->data);
+
+ for (size_t i = 0; i < c->n_data; i++) {
+ user_record_unref(c->data[i].host_user);
+ group_record_unref(c->data[i].host_group);
+ user_record_unref(c->data[i].payload_user);
+ group_record_unref(c->data[i].payload_group);
+ }
+
+ return mfree(c);
+}
+
+int bind_user_prepare(
+ const char *directory,
+ char **bind_user,
+ uid_t uid_shift,
+ uid_t uid_range,
+ CustomMount **custom_mounts,
+ size_t *n_custom_mounts,
+ BindUserContext **ret) {
+
+ _cleanup_(bind_user_context_freep) BindUserContext *c = NULL;
+ uid_t current_uid = MAP_UID_MIN;
+ int r;
+
+ assert(custom_mounts);
+ assert(n_custom_mounts);
+ assert(ret);
+
+ /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
+ * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
+ * to include an appropriate bind mount mapping.
+ *
+ * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
+ * new BindUserContext for the user records */
+
+ if (strv_isempty(bind_user)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ c = new0(BindUserContext, 1);
+ if (!c)
+ return log_oom();
+
+ STRV_FOREACH(n, bind_user) {
+ _cleanup_(user_record_unrefp) UserRecord *u = NULL, *cu = NULL;
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL;
+ _cleanup_free_ char *sm = NULL, *sd = NULL;
+ CustomMount *cm;
+
+ r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE, &u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve user '%s': %m", *n);
+
+ /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate
+ * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus
+ * even if we wanted, we couldn't override the root or nobody user records. Note we also
+ * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter
+ * out root/nobody too, hence these checks might appear redundant — but they actually are
+ * not, as we want to support environments where /etc/passwd and /etc/group are non-existent,
+ * and the user/group databases fully synthesized at runtime. Moreover, the name of the
+ * user/group name of the "nobody" account differs between distros, hence a check by numeric
+ * UID is safer. */
+ if (u->uid == 0 || streq(u->user_name, "root"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'root' user not supported, sorry.");
+ if (u->uid == UID_NOBODY || STR_IN_SET(u->user_name, NOBODY_USER_NAME, "nobody"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'nobody' user not supported, sorry.");
+
+ if (u->uid >= uid_shift && u->uid < uid_shift + uid_range)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID of user '%s' to map is already in container UID range, refusing.", u->user_name);
+
+ r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE, &g);
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name);
+
+ if (g->gid >= uid_shift && g->gid < uid_shift + uid_range)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "GID of group '%s' to map is already in container GID range, refusing.", g->group_name);
+
+ /* We want to synthesize exactly one user + group from the host into the container. This only
+ * makes sense if the user on the host has its own private group. We can't reasonably check
+ * this, so we just check of the name of user and group match.
+ *
+ * One of these days we might want to support users in a shared/common group too, but it's
+ * not clear to me how this would have to be mapped, precisely given that the common group
+ * probably already exists in the container. */
+ if (!streq(u->user_name, g->group_name))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Sorry, mapping users without private groups is currently not supported.");
+
+ r = find_free_uid(directory, uid_range, &current_uid);
+ if (r < 0)
+ return r;
+
+ r = convert_user(directory, u, g, current_uid, &cu, &cg);
+ if (r < 0)
+ return r;
+
+ if (!GREEDY_REALLOC(c->data, c->n_data + 1))
+ return log_oom();
+
+ sm = strdup(u->home_directory);
+ if (!sm)
+ return log_oom();
+
+ sd = strdup(cu->home_directory);
+ if (!sd)
+ return log_oom();
+
+ cm = reallocarray(*custom_mounts, *n_custom_mounts + 1, sizeof(CustomMount));
+ if (!cm)
+ return log_oom();
+
+ *custom_mounts = cm;
+
+ (*custom_mounts)[(*n_custom_mounts)++] = (CustomMount) {
+ .type = CUSTOM_MOUNT_BIND,
+ .source = TAKE_PTR(sm),
+ .destination = TAKE_PTR(sd),
+ };
+
+ c->data[c->n_data++] = (BindUserData) {
+ .host_user = TAKE_PTR(u),
+ .host_group = TAKE_PTR(g),
+ .payload_user = TAKE_PTR(cu),
+ .payload_group = TAKE_PTR(cg),
+ };
+
+ current_uid++;
+ }
+
+ *ret = TAKE_PTR(c);
+ return 1;
+}
+
+static int write_and_symlink(
+ const char *root,
+ JsonVariant *v,
+ const char *name,
+ uid_t uid,
+ const char *suffix,
+ WriteStringFileFlags extra_flags) {
+
+ _cleanup_free_ char *j = NULL, *f = NULL, *p = NULL, *q = NULL;
+ int r;
+
+ assert(root);
+ assert(v);
+ assert(name);
+ assert(uid_is_valid(uid));
+ assert(suffix);
+
+ r = json_variant_format(v, JSON_FORMAT_NEWLINE, &j);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format user record JSON: %m");
+
+ f = strjoin(name, suffix);
+ if (!f)
+ return log_oom();
+
+ p = path_join(root, "/run/host/userdb/", f);
+ if (!p)
+ return log_oom();
+
+ if (asprintf(&q, "%s/run/host/userdb/" UID_FMT "%s", root, uid, suffix) < 0)
+ return log_oom();
+
+ if (symlink(f, q) < 0)
+ return log_error_errno(errno, "Failed to create symlink '%s': %m", q);
+
+ r = userns_lchown(q, 0, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to adjust access mode of '%s': %m", q);
+
+ r = write_string_file(p, j, WRITE_STRING_FILE_CREATE|extra_flags);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write %s: %m", p);
+
+ r = userns_lchown(p, 0, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
+
+ return 0;
+}
+
+int bind_user_setup(
+ const BindUserContext *c,
+ const char *root) {
+
+ static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
+ USER_RECORD_REQUIRE_REGULAR|
+ USER_RECORD_STRIP_PRIVILEGED|
+ USER_RECORD_ALLOW_PER_MACHINE|
+ USER_RECORD_ALLOW_BINDING|
+ USER_RECORD_ALLOW_SIGNATURE|
+ USER_RECORD_PERMISSIVE;
+ static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */
+ USER_RECORD_STRIP_REGULAR|
+ USER_RECORD_ALLOW_PRIVILEGED|
+ USER_RECORD_STRIP_PER_MACHINE|
+ USER_RECORD_STRIP_BINDING|
+ USER_RECORD_STRIP_SIGNATURE|
+ USER_RECORD_EMPTY_OK|
+ USER_RECORD_PERMISSIVE;
+ int r;
+
+ assert(root);
+
+ if (!c || c->n_data == 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/home", 0755, 0, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create /run/host/home: %m");
+
+ r = userns_mkdir(root, "/run/host/userdb", 0755, 0, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create /run/host/userdb: %m");
+
+ for (size_t i = 0; i < c->n_data; i++) {
+ _cleanup_(group_record_unrefp) GroupRecord *stripped_group = NULL, *shadow_group = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *stripped_user = NULL, *shadow_user = NULL;
+ const BindUserData *d = c->data + i;
+
+ /* First, write shadow (i.e. privileged) data for group record */
+ r = group_record_clone(d->payload_group, shadow_flags, &shadow_group);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract privileged information from group record: %m");
+
+ if (!json_variant_is_blank_object(shadow_group->json)) {
+ r = write_and_symlink(
+ root,
+ shadow_group->json,
+ d->payload_group->group_name,
+ d->payload_group->gid,
+ ".group-privileged",
+ WRITE_STRING_FILE_MODE_0600);
+ if (r < 0)
+ return r;
+ }
+
+ /* Second, write main part of group record. */
+ r = group_record_clone(d->payload_group, strip_flags, &stripped_group);
+ if (r < 0)
+ return log_error_errno(r, "Failed to strip privileged information from group record: %m");
+
+ r = write_and_symlink(
+ root,
+ stripped_group->json,
+ d->payload_group->group_name,
+ d->payload_group->gid,
+ ".group",
+ 0);
+ if (r < 0)
+ return r;
+
+ /* Third, write out user shadow data. i.e. extract privileged info from user record */
+ r = user_record_clone(d->payload_user, shadow_flags, &shadow_user);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract privileged information from user record: %m");
+
+ if (!json_variant_is_blank_object(shadow_user->json)) {
+ r = write_and_symlink(
+ root,
+ shadow_user->json,
+ d->payload_user->user_name,
+ d->payload_user->uid,
+ ".user-privileged",
+ WRITE_STRING_FILE_MODE_0600);
+ if (r < 0)
+ return r;
+ }
+
+ /* Finally write out the main part of the user record */
+ r = user_record_clone(d->payload_user, strip_flags, &stripped_user);
+ if (r < 0)
+ return log_error_errno(r, "Failed to strip privileged information from user record: %m");
+
+ r = write_and_symlink(
+ root,
+ stripped_user->json,
+ d->payload_user->user_name,
+ d->payload_user->uid,
+ ".user",
+ 0);
+ if (r < 0)
+ return r;
+ }
+
+ return 1;
+}