]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/nspawn/nspawn-bind-user.c
strv: make iterator in STRV_FOREACH() declaread in the loop
[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
2f893044
LP
13static 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
46static 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
79static 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
150static 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
176BindUserContext* 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
192int 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, &current_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
315static 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
365int 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}