]>
Commit | Line | Data |
---|---|---|
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 | ||
2f893044 LP |
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 | ||
01bebba3 | 24 | r = chase_symlinks_and_fopen_unlocked("/etc/passwd", directory, CHASE_PREFIX_ROOT, "re", NULL, &f); |
2f893044 LP |
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 | ||
01bebba3 | 57 | r = chase_symlinks_and_fopen_unlocked("/etc/group", directory, CHASE_PREFIX_ROOT, "re", NULL, &f); |
2f893044 LP |
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)), | |
0cdf6b14 | 128 | JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")), |
2f893044 LP |
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))), | |
0cdf6b14 | 140 | JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")))); |
2f893044 LP |
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) ++) { | |
76ef5d04 | 157 | if (*current_uid > MAP_UID_MAX || *current_uid > max_uid) |
2f893044 LP |
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.", | |
76ef5d04 | 161 | MAP_UID_MIN, MAP_UID_MAX); |
2f893044 LP |
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); | |
2f092762 | 171 | if (r <= 0) |
2f893044 | 172 | return r; |
2f893044 LP |
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; | |
76ef5d04 | 202 | uid_t current_uid = MAP_UID_MIN; |
2f893044 LP |
203 | int r; |
204 | ||
205 | assert(custom_mounts); | |
206 | assert(n_custom_mounts); | |
207 | assert(ret); | |
208 | ||
209 | /* This resolves the users specified in 'bind_user', generates a minimalized JSON user + group record | |
210 | * for it to stick in the container, allocates a UID/GID for it, and updates the custom mount table, | |
211 | * to include an appropriate bind mount mapping. | |
212 | * | |
213 | * This extends the passed custom_mounts/n_custom_mounts with the home directories, and allocates a | |
214 | * new BindUserContext for the user records */ | |
215 | ||
216 | if (strv_isempty(bind_user)) { | |
217 | *ret = NULL; | |
218 | return 0; | |
219 | } | |
220 | ||
221 | c = new0(BindUserContext, 1); | |
222 | if (!c) | |
223 | return log_oom(); | |
224 | ||
225 | STRV_FOREACH(n, bind_user) { | |
226 | _cleanup_(user_record_unrefp) UserRecord *u = NULL, *cu = NULL; | |
227 | _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL; | |
228 | _cleanup_free_ char *sm = NULL, *sd = NULL; | |
229 | CustomMount *cm; | |
230 | ||
231 | r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE, &u); | |
232 | if (r < 0) | |
233 | return log_error_errno(r, "Failed to resolve user '%s': %m", *n); | |
234 | ||
235 | /* For now, let's refuse mapping the root/nobody users explicitly. The records we generate | |
236 | * are strictly additive, nss-systemd is typically placed last in /etc/nsswitch.conf. Thus | |
237 | * even if we wanted, we couldn't override the root or nobody user records. Note we also | |
238 | * check for name conflicts in /etc/passwd + /etc/group later on, which would usually filter | |
239 | * out root/nobody too, hence these checks might appear redundant — but they actually are | |
240 | * not, as we want to support environments where /etc/passwd and /etc/group are non-existent, | |
241 | * and the user/group databases fully synthesized at runtime. Moreover, the name of the | |
242 | * user/group name of the "nobody" account differs between distros, hence a check by numeric | |
243 | * UID is safer. */ | |
244 | if (u->uid == 0 || streq(u->user_name, "root")) | |
245 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'root' user not supported, sorry."); | |
246 | if (u->uid == UID_NOBODY || STR_IN_SET(u->user_name, NOBODY_USER_NAME, "nobody")) | |
247 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mapping 'nobody' user not supported, sorry."); | |
248 | ||
249 | if (u->uid >= uid_shift && u->uid < uid_shift + uid_range) | |
250 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID of user '%s' to map is already in container UID range, refusing.", u->user_name); | |
251 | ||
252 | r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE, &g); | |
253 | if (r < 0) | |
254 | return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name); | |
255 | ||
256 | if (g->gid >= uid_shift && g->gid < uid_shift + uid_range) | |
257 | return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "GID of group '%s' to map is already in container GID range, refusing.", g->group_name); | |
258 | ||
259 | /* We want to synthesize exactly one user + group from the host into the container. This only | |
260 | * makes sense if the user on the host has its own private group. We can't reasonably check | |
261 | * this, so we just check of the name of user and group match. | |
262 | * | |
263 | * One of these days we might want to support users in a shared/common group too, but it's | |
264 | * not clear to me how this would have to be mapped, precisely given that the common group | |
265 | * probably already exists in the container. */ | |
266 | if (!streq(u->user_name, g->group_name)) | |
267 | return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), | |
268 | "Sorry, mapping users without private groups is currently not supported."); | |
269 | ||
270 | r = find_free_uid(directory, uid_range, ¤t_uid); | |
271 | if (r < 0) | |
272 | return r; | |
273 | ||
274 | r = convert_user(directory, u, g, current_uid, &cu, &cg); | |
275 | if (r < 0) | |
276 | return r; | |
277 | ||
354dadb3 | 278 | if (!GREEDY_REALLOC(c->data, c->n_data + 1)) |
2f893044 LP |
279 | return log_oom(); |
280 | ||
281 | sm = strdup(u->home_directory); | |
282 | if (!sm) | |
283 | return log_oom(); | |
284 | ||
285 | sd = strdup(cu->home_directory); | |
286 | if (!sd) | |
287 | return log_oom(); | |
288 | ||
289 | cm = reallocarray(*custom_mounts, sizeof(CustomMount), *n_custom_mounts + 1); | |
290 | if (!cm) | |
291 | return log_oom(); | |
292 | ||
293 | *custom_mounts = cm; | |
294 | ||
295 | (*custom_mounts)[(*n_custom_mounts)++] = (CustomMount) { | |
296 | .type = CUSTOM_MOUNT_BIND, | |
297 | .source = TAKE_PTR(sm), | |
298 | .destination = TAKE_PTR(sd), | |
299 | }; | |
300 | ||
301 | c->data[c->n_data++] = (BindUserData) { | |
302 | .host_user = TAKE_PTR(u), | |
303 | .host_group = TAKE_PTR(g), | |
304 | .payload_user = TAKE_PTR(cu), | |
305 | .payload_group = TAKE_PTR(cg), | |
306 | }; | |
307 | ||
308 | current_uid++; | |
309 | } | |
310 | ||
311 | *ret = TAKE_PTR(c); | |
312 | return 1; | |
313 | } | |
314 | ||
315 | static int write_and_symlink( | |
316 | const char *root, | |
317 | JsonVariant *v, | |
318 | const char *name, | |
319 | uid_t uid, | |
320 | const char *suffix, | |
321 | WriteStringFileFlags extra_flags) { | |
322 | ||
323 | _cleanup_free_ char *j = NULL, *f = NULL, *p = NULL, *q = NULL; | |
324 | int r; | |
325 | ||
326 | assert(root); | |
327 | assert(v); | |
328 | assert(name); | |
329 | assert(uid_is_valid(uid)); | |
330 | assert(suffix); | |
331 | ||
332 | r = json_variant_format(v, JSON_FORMAT_NEWLINE, &j); | |
333 | if (r < 0) | |
334 | return log_error_errno(r, "Failed to format user record JSON: %m"); | |
335 | ||
336 | f = strjoin(name, suffix); | |
337 | if (!f) | |
338 | return log_oom(); | |
339 | ||
340 | p = path_join(root, "/run/host/userdb/", f); | |
341 | if (!p) | |
342 | return log_oom(); | |
343 | ||
344 | if (asprintf(&q, "%s/run/host/userdb/" UID_FMT "%s", root, uid, suffix) < 0) | |
345 | return log_oom(); | |
346 | ||
347 | if (symlink(f, q) < 0) | |
348 | return log_error_errno(errno, "Failed to create symlink '%s': %m", q); | |
349 | ||
350 | r = userns_lchown(q, 0, 0); | |
351 | if (r < 0) | |
352 | return log_error_errno(r, "Failed to adjust access mode of '%s': %m", q); | |
353 | ||
354 | r = write_string_file(p, j, WRITE_STRING_FILE_CREATE|extra_flags); | |
355 | if (r < 0) | |
356 | return log_error_errno(r, "Failed to write %s: %m", p); | |
357 | ||
358 | r = userns_lchown(p, 0, 0); | |
359 | if (r < 0) | |
360 | return log_error_errno(r, "Failed to adjust access mode of '%s': %m", p); | |
361 | ||
362 | return 0; | |
363 | } | |
364 | ||
365 | int bind_user_setup( | |
366 | const BindUserContext *c, | |
367 | const char *root) { | |
368 | ||
369 | static const UserRecordLoadFlags strip_flags = /* Removes privileged info */ | |
370 | USER_RECORD_REQUIRE_REGULAR| | |
371 | USER_RECORD_STRIP_PRIVILEGED| | |
372 | USER_RECORD_ALLOW_PER_MACHINE| | |
373 | USER_RECORD_ALLOW_BINDING| | |
bfc0cc1a LP |
374 | USER_RECORD_ALLOW_SIGNATURE| |
375 | USER_RECORD_PERMISSIVE; | |
2f893044 LP |
376 | static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */ |
377 | USER_RECORD_STRIP_REGULAR| | |
378 | USER_RECORD_ALLOW_PRIVILEGED| | |
379 | USER_RECORD_STRIP_PER_MACHINE| | |
380 | USER_RECORD_STRIP_BINDING| | |
381 | USER_RECORD_STRIP_SIGNATURE| | |
bfc0cc1a LP |
382 | USER_RECORD_EMPTY_OK| |
383 | USER_RECORD_PERMISSIVE; | |
2f893044 LP |
384 | int r; |
385 | ||
386 | assert(root); | |
387 | ||
388 | if (!c || c->n_data == 0) | |
389 | return 0; | |
390 | ||
391 | r = userns_mkdir(root, "/run/host", 0755, 0, 0); | |
392 | if (r < 0) | |
393 | return log_error_errno(r, "Failed to create /run/host: %m"); | |
394 | ||
395 | r = userns_mkdir(root, "/run/host/home", 0755, 0, 0); | |
396 | if (r < 0) | |
397 | return log_error_errno(r, "Failed to create /run/host/userdb: %m"); | |
398 | ||
399 | r = userns_mkdir(root, "/run/host/userdb", 0755, 0, 0); | |
400 | if (r < 0) | |
401 | return log_error_errno(r, "Failed to create /run/host/userdb: %m"); | |
402 | ||
403 | for (size_t i = 0; i < c->n_data; i++) { | |
404 | _cleanup_(group_record_unrefp) GroupRecord *stripped_group = NULL, *shadow_group = NULL; | |
405 | _cleanup_(user_record_unrefp) UserRecord *stripped_user = NULL, *shadow_user = NULL; | |
406 | const BindUserData *d = c->data + i; | |
407 | ||
408 | /* First, write shadow (i.e. privileged) data for group record */ | |
409 | r = group_record_clone(d->payload_group, shadow_flags, &shadow_group); | |
410 | if (r < 0) | |
411 | return log_error_errno(r, "Failed to extract privileged information from group record: %m"); | |
412 | ||
413 | if (!json_variant_is_blank_object(shadow_group->json)) { | |
414 | r = write_and_symlink( | |
415 | root, | |
416 | shadow_group->json, | |
417 | d->payload_group->group_name, | |
418 | d->payload_group->gid, | |
419 | ".group-privileged", | |
420 | WRITE_STRING_FILE_MODE_0600); | |
421 | if (r < 0) | |
422 | return r; | |
423 | } | |
424 | ||
425 | /* Second, write main part of group record. */ | |
426 | r = group_record_clone(d->payload_group, strip_flags, &stripped_group); | |
427 | if (r < 0) | |
428 | return log_error_errno(r, "Failed to strip privileged information from group record: %m"); | |
429 | ||
430 | r = write_and_symlink( | |
431 | root, | |
432 | stripped_group->json, | |
433 | d->payload_group->group_name, | |
434 | d->payload_group->gid, | |
435 | ".group", | |
436 | 0); | |
437 | if (r < 0) | |
438 | return r; | |
439 | ||
440 | /* Third, write out user shadow data. i.e. extract privileged info from user record */ | |
441 | r = user_record_clone(d->payload_user, shadow_flags, &shadow_user); | |
442 | if (r < 0) | |
443 | return log_error_errno(r, "Failed to extract privileged information from user record: %m"); | |
444 | ||
445 | if (!json_variant_is_blank_object(shadow_user->json)) { | |
446 | r = write_and_symlink( | |
447 | root, | |
448 | shadow_user->json, | |
449 | d->payload_user->user_name, | |
450 | d->payload_user->uid, | |
451 | ".user-privileged", | |
452 | WRITE_STRING_FILE_MODE_0600); | |
453 | if (r < 0) | |
454 | return r; | |
455 | } | |
456 | ||
457 | /* Finally write out the main part of the user record */ | |
458 | r = user_record_clone(d->payload_user, strip_flags, &stripped_user); | |
459 | if (r < 0) | |
460 | return log_error_errno(r, "Failed to strip privileged information from user record: %m"); | |
461 | ||
462 | r = write_and_symlink( | |
463 | root, | |
464 | stripped_user->json, | |
465 | d->payload_user->user_name, | |
466 | d->payload_user->uid, | |
467 | ".user", | |
468 | 0); | |
469 | if (r < 0) | |
470 | return r; | |
471 | } | |
472 | ||
473 | return 1; | |
474 | } |