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