]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/nspawn/nspawn-bind-user.c
97761cb6f0b913045be5d453da22d1bbbc975c3c
[thirdparty/systemd.git] / src / nspawn / nspawn-bind-user.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include "chase-symlinks.h"
4 #include "fd-util.h"
5 #include "fileio.h"
6 #include "format-util.h"
7 #include "nspawn-bind-user.h"
8 #include "nspawn.h"
9 #include "path-util.h"
10 #include "user-util.h"
11 #include "userdb.h"
12
13 static int check_etc_passwd_collisions(
14 const char *directory,
15 const char *name,
16 uid_t uid) {
17
18 _cleanup_fclose_ FILE *f = NULL;
19 int r;
20
21 assert(directory);
22 assert(name || uid_is_valid(uid));
23
24 r = chase_symlinks_and_fopen_unlocked("/etc/passwd", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
25 if (r == -ENOENT)
26 return 0; /* no user database? then no user, hence no collision */
27 if (r < 0)
28 return log_error_errno(r, "Failed to open /etc/passwd of container: %m");
29
30 for (;;) {
31 struct passwd *pw;
32
33 r = fgetpwent_sane(f, &pw);
34 if (r < 0)
35 return log_error_errno(r, "Failed to iterate through /etc/passwd of container: %m");
36 if (r == 0) /* EOF */
37 return 0; /* no collision */
38
39 if (name && streq_ptr(pw->pw_name, name))
40 return 1; /* name collision */
41 if (uid_is_valid(uid) && pw->pw_uid == uid)
42 return 1; /* UID collision */
43 }
44 }
45
46 static int check_etc_group_collisions(
47 const char *directory,
48 const char *name,
49 gid_t gid) {
50
51 _cleanup_fclose_ FILE *f = NULL;
52 int r;
53
54 assert(directory);
55 assert(name || gid_is_valid(gid));
56
57 r = chase_symlinks_and_fopen_unlocked("/etc/group", directory, CHASE_PREFIX_ROOT, "re", NULL, &f);
58 if (r == -ENOENT)
59 return 0; /* no group database? then no group, hence no collision */
60 if (r < 0)
61 return log_error_errno(r, "Failed to open /etc/group of container: %m");
62
63 for (;;) {
64 struct group *gr;
65
66 r = fgetgrent_sane(f, &gr);
67 if (r < 0)
68 return log_error_errno(r, "Failed to iterate through /etc/group of container: %m");
69 if (r == 0)
70 return 0; /* no collision */
71
72 if (name && streq_ptr(gr->gr_name, name))
73 return 1; /* name collision */
74 if (gid_is_valid(gid) && gr->gr_gid == gid)
75 return 1; /* gid collision */
76 }
77 }
78
79 static int convert_user(
80 const char *directory,
81 UserRecord *u,
82 GroupRecord *g,
83 uid_t allocate_uid,
84 UserRecord **ret_converted_user,
85 GroupRecord **ret_converted_group) {
86
87 _cleanup_(group_record_unrefp) GroupRecord *converted_group = NULL;
88 _cleanup_(user_record_unrefp) UserRecord *converted_user = NULL;
89 _cleanup_free_ char *h = NULL;
90 JsonVariant *p, *hp = NULL;
91 int r;
92
93 assert(u);
94 assert(g);
95 assert(u->gid == g->gid);
96
97 r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID);
98 if (r < 0)
99 return r;
100 if (r > 0)
101 return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
102 "Sorry, the user '%s' already exists in the container.", u->user_name);
103
104 r = check_etc_group_collisions(directory, g->group_name, GID_INVALID);
105 if (r < 0)
106 return r;
107 if (r > 0)
108 return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
109 "Sorry, the group '%s' already exists in the container.", g->group_name);
110
111 h = path_join("/run/host/home/", u->user_name);
112 if (!h)
113 return log_oom();
114
115 /* Acquire the source hashed password array as-is, so that it retains the JSON_VARIANT_SENSITIVE flag */
116 p = json_variant_by_key(u->json, "privileged");
117 if (p)
118 hp = json_variant_by_key(p, "hashedPassword");
119
120 r = user_record_build(
121 &converted_user,
122 JSON_BUILD_OBJECT(
123 JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(u->user_name)),
124 JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(allocate_uid)),
125 JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid)),
126 JSON_BUILD_PAIR_CONDITION(u->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(u->disposition))),
127 JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(h)),
128 JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
129 JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "privileged", JSON_BUILD_OBJECT(
130 JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_VARIANT(hp))))));
131 if (r < 0)
132 return log_error_errno(r, "Failed to build container user record: %m");
133
134 r = group_record_build(
135 &converted_group,
136 JSON_BUILD_OBJECT(
137 JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)),
138 JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(allocate_uid)),
139 JSON_BUILD_PAIR_CONDITION(g->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(g->disposition))),
140 JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn"))));
141 if (r < 0)
142 return log_error_errno(r, "Failed to build container group record: %m");
143
144 *ret_converted_user = TAKE_PTR(converted_user);
145 *ret_converted_group = TAKE_PTR(converted_group);
146
147 return 0;
148 }
149
150 static int find_free_uid(const char *directory, uid_t max_uid, uid_t *current_uid) {
151 int r;
152
153 assert(directory);
154 assert(current_uid);
155
156 for (;; (*current_uid) ++) {
157 if (*current_uid > MAP_UID_MAX || *current_uid > max_uid)
158 return log_error_errno(
159 SYNTHETIC_ERRNO(EBUSY),
160 "No suitable available UID in range " UID_FMT "…" UID_FMT " in container detected, can't map user.",
161 MAP_UID_MIN, MAP_UID_MAX);
162
163 r = check_etc_passwd_collisions(directory, NULL, *current_uid);
164 if (r < 0)
165 return r;
166 if (r > 0) /* already used */
167 continue;
168
169 /* We want to use the UID also as GID, hence check for it in /etc/group too */
170 r = check_etc_group_collisions(directory, NULL, (gid_t) *current_uid);
171 if (r <= 0)
172 return r;
173 }
174 }
175
176 BindUserContext* bind_user_context_free(BindUserContext *c) {
177 if (!c)
178 return NULL;
179
180 assert(c->n_data == 0 || c->data);
181
182 for (size_t i = 0; i < c->n_data; i++) {
183 user_record_unref(c->data[i].host_user);
184 group_record_unref(c->data[i].host_group);
185 user_record_unref(c->data[i].payload_user);
186 group_record_unref(c->data[i].payload_group);
187 }
188
189 return mfree(c);
190 }
191
192 int bind_user_prepare(
193 const char *directory,
194 char **bind_user,
195 uid_t uid_shift,
196 uid_t uid_range,
197 CustomMount **custom_mounts,
198 size_t *n_custom_mounts,
199 BindUserContext **ret) {
200
201 _cleanup_(bind_user_context_freep) BindUserContext *c = NULL;
202 uid_t current_uid = MAP_UID_MIN;
203 char **n;
204 int r;
205
206 assert(custom_mounts);
207 assert(n_custom_mounts);
208 assert(ret);
209
210 /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record
211 * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table,
212 * to include an appropriate bind mount mapping.
213 *
214 * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a
215 * new BindUserContext for the user records */
216
217 if (strv_isempty(bind_user)) {
218 *ret = NULL;
219 return 0;
220 }
221
222 c = new0(BindUserContext, 1);
223 if (!c)
224 return log_oom();
225
226 STRV_FOREACH(n, bind_user) {
227 _cleanup_(user_record_unrefp) UserRecord *u = NULL, *cu = NULL;
228 _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL;
229 _cleanup_free_ char *sm = NULL, *sd = NULL;
230 CustomMount *cm;
231
232 r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE, &u);
233 if (r < 0)
234 return log_error_errno(r, "Failed to resolve user '%s': %m", *n);
235
236 /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate
237 * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus
238 * even if we wanted, we couldn't override the root or nobody user records. Note we also
239 * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter
240 * out root/nobody too, hence these checks might appear redundant — but they actually are
241 * not, as we want to support environments where /etc/passwd and /etc/group are non-existent,
242 * and the user/group databases fully synthesized at runtime. Moreover, the name of the
243 * user/group name of the "nobody" account differs between distros, hence a check by numeric
244 * UID is safer. */
245 if (u->uid == 0 || streq(u->user_name, "root"))
246 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'root' user not supported, sorry.");
247 if (u->uid == UID_NOBODY || STR_IN_SET(u->user_name, NOBODY_USER_NAME, "nobody"))
248 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'nobody' user not supported, sorry.");
249
250 if (u->uid >= uid_shift && u->uid < uid_shift + uid_range)
251 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID of user '%s' to map is already in container UID range, refusing.", u->user_name);
252
253 r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE, &g);
254 if (r < 0)
255 return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name);
256
257 if (g->gid >= uid_shift && g->gid < uid_shift + uid_range)
258 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "GID of group '%s' to map is already in container GID range, refusing.", g->group_name);
259
260 /* We want to synthesize exactly one user + group from the host into the container. This only
261 * makes sense if the user on the host has its own private group. We can't reasonably check
262 * this, so we just check of the name of user and group match.
263 *
264 * One of these days we might want to support users in a shared/common group too, but it's
265 * not clear to me how this would have to be mapped, precisely given that the common group
266 * probably already exists in the container. */
267 if (!streq(u->user_name, g->group_name))
268 return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
269 "Sorry, mapping users without private groups is currently not supported.");
270
271 r = find_free_uid(directory, uid_range, &current_uid);
272 if (r < 0)
273 return r;
274
275 r = convert_user(directory, u, g, current_uid, &cu, &cg);
276 if (r < 0)
277 return r;
278
279 if (!GREEDY_REALLOC(c->data, c->n_data + 1))
280 return log_oom();
281
282 sm = strdup(u->home_directory);
283 if (!sm)
284 return log_oom();
285
286 sd = strdup(cu->home_directory);
287 if (!sd)
288 return log_oom();
289
290 cm = reallocarray(*custom_mounts, sizeof(CustomMount), *n_custom_mounts + 1);
291 if (!cm)
292 return log_oom();
293
294 *custom_mounts = cm;
295
296 (*custom_mounts)[(*n_custom_mounts)++] = (CustomMount) {
297 .type = CUSTOM_MOUNT_BIND,
298 .source = TAKE_PTR(sm),
299 .destination = TAKE_PTR(sd),
300 };
301
302 c->data[c->n_data++] = (BindUserData) {
303 .host_user = TAKE_PTR(u),
304 .host_group = TAKE_PTR(g),
305 .payload_user = TAKE_PTR(cu),
306 .payload_group = TAKE_PTR(cg),
307 };
308
309 current_uid++;
310 }
311
312 *ret = TAKE_PTR(c);
313 return 1;
314 }
315
316 static int write_and_symlink(
317 const char *root,
318 JsonVariant *v,
319 const char *name,
320 uid_t uid,
321 const char *suffix,
322 WriteStringFileFlags extra_flags) {
323
324 _cleanup_free_ char *j = NULL, *f = NULL, *p = NULL, *q = NULL;
325 int r;
326
327 assert(root);
328 assert(v);
329 assert(name);
330 assert(uid_is_valid(uid));
331 assert(suffix);
332
333 r = json_variant_format(v, JSON_FORMAT_NEWLINE, &j);
334 if (r < 0)
335 return log_error_errno(r, "Failed to format user record JSON: %m");
336
337 f = strjoin(name, suffix);
338 if (!f)
339 return log_oom();
340
341 p = path_join(root, "/run/host/userdb/", f);
342 if (!p)
343 return log_oom();
344
345 if (asprintf(&q, "%s/run/host/userdb/" UID_FMT "%s", root, uid, suffix) < 0)
346 return log_oom();
347
348 if (symlink(f, q) < 0)
349 return log_error_errno(errno, "Failed to create symlink '%s': %m", q);
350
351 r = userns_lchown(q, 0, 0);
352 if (r < 0)
353 return log_error_errno(r, "Failed to adjust access mode of '%s': %m", q);
354
355 r = write_string_file(p, j, WRITE_STRING_FILE_CREATE|extra_flags);
356 if (r < 0)
357 return log_error_errno(r, "Failed to write %s: %m", p);
358
359 r = userns_lchown(p, 0, 0);
360 if (r < 0)
361 return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p);
362
363 return 0;
364 }
365
366 int bind_user_setup(
367 const BindUserContext *c,
368 const char *root) {
369
370 static const UserRecordLoadFlags strip_flags = /* Removes privileged info */
371 USER_RECORD_REQUIRE_REGULAR|
372 USER_RECORD_STRIP_PRIVILEGED|
373 USER_RECORD_ALLOW_PER_MACHINE|
374 USER_RECORD_ALLOW_BINDING|
375 USER_RECORD_ALLOW_SIGNATURE|
376 USER_RECORD_PERMISSIVE;
377 static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */
378 USER_RECORD_STRIP_REGULAR|
379 USER_RECORD_ALLOW_PRIVILEGED|
380 USER_RECORD_STRIP_PER_MACHINE|
381 USER_RECORD_STRIP_BINDING|
382 USER_RECORD_STRIP_SIGNATURE|
383 USER_RECORD_EMPTY_OK|
384 USER_RECORD_PERMISSIVE;
385 int r;
386
387 assert(root);
388
389 if (!c || c->n_data == 0)
390 return 0;
391
392 r = userns_mkdir(root, "/run/host", 0755, 0, 0);
393 if (r < 0)
394 return log_error_errno(r, "Failed to create /run/host: %m");
395
396 r = userns_mkdir(root, "/run/host/home", 0755, 0, 0);
397 if (r < 0)
398 return log_error_errno(r, "Failed to create /run/host/userdb: %m");
399
400 r = userns_mkdir(root, "/run/host/userdb", 0755, 0, 0);
401 if (r < 0)
402 return log_error_errno(r, "Failed to create /run/host/userdb: %m");
403
404 for (size_t i = 0; i < c->n_data; i++) {
405 _cleanup_(group_record_unrefp) GroupRecord *stripped_group = NULL, *shadow_group = NULL;
406 _cleanup_(user_record_unrefp) UserRecord *stripped_user = NULL, *shadow_user = NULL;
407 const BindUserData *d = c->data + i;
408
409 /* First, write shadow (i.e. privileged) data for group record */
410 r = group_record_clone(d->payload_group, shadow_flags, &shadow_group);
411 if (r < 0)
412 return log_error_errno(r, "Failed to extract privileged information from group record: %m");
413
414 if (!json_variant_is_blank_object(shadow_group->json)) {
415 r = write_and_symlink(
416 root,
417 shadow_group->json,
418 d->payload_group->group_name,
419 d->payload_group->gid,
420 ".group-privileged",
421 WRITE_STRING_FILE_MODE_0600);
422 if (r < 0)
423 return r;
424 }
425
426 /* Second, write main part of group record. */
427 r = group_record_clone(d->payload_group, strip_flags, &stripped_group);
428 if (r < 0)
429 return log_error_errno(r, "Failed to strip privileged information from group record: %m");
430
431 r = write_and_symlink(
432 root,
433 stripped_group->json,
434 d->payload_group->group_name,
435 d->payload_group->gid,
436 ".group",
437 0);
438 if (r < 0)
439 return r;
440
441 /* Third, write out user shadow data. i.e. extract privileged info from user record */
442 r = user_record_clone(d->payload_user, shadow_flags, &shadow_user);
443 if (r < 0)
444 return log_error_errno(r, "Failed to extract privileged information from user record: %m");
445
446 if (!json_variant_is_blank_object(shadow_user->json)) {
447 r = write_and_symlink(
448 root,
449 shadow_user->json,
450 d->payload_user->user_name,
451 d->payload_user->uid,
452 ".user-privileged",
453 WRITE_STRING_FILE_MODE_0600);
454 if (r < 0)
455 return r;
456 }
457
458 /* Finally write out the main part of the user record */
459 r = user_record_clone(d->payload_user, strip_flags, &stripped_user);
460 if (r < 0)
461 return log_error_errno(r, "Failed to strip privileged information from user record: %m");
462
463 r = write_and_symlink(
464 root,
465 stripped_user->json,
466 d->payload_user->user_name,
467 d->payload_user->uid,
468 ".user",
469 0);
470 if (r < 0)
471 return r;
472 }
473
474 return 1;
475 }