사용자 네임스페이스: 특정 프로그램에 대해서만 폴더를 마운트하는 방법

사용자 네임스페이스: 특정 프로그램에 대해서만 폴더를 마운트하는 방법

루트 액세스 없이 FHS가 아닌 시스템(NixO)에서 FHS 시스템을 가짜로 만들고 싶습니다. 이렇게 하려면 usernamespace를 사용하여 루트에 일부 폴더(예: install /tmp/mylibto )를 마운트해야 합니다 /lib(다른 솔루션은 보이지 않습니다).

불행히도 작동시키는 방법을 찾을 수 없습니다. 따라 하려고 했습니다.이 튜토리얼, 그러나 코드를 복사하면 실패합니다(bash를 시작할 수도 없습니다).

$ gcc userns_child_exec.c -lcap -o userns_child_exec
$ id
uid=1000(myname) gid=100(users) groups=100(users),1(wheel),17(audio),20(lp),57(networkmanager),59(scanner),131(docker),998(vboxusers),999(adbusers)

$ ./userns_child_exec -U -M '0 1000 1' -G '0 100 1' bash
write /proc/535313/gid_map: Operation not permitted
bash: initialize_job_control: no job control in background: Bad file descriptor

[nix-shell:~/Documents/Logiciels/Nix_bidouille/2022_04_26_-_nix_fake_FHS_user_namespace/demo]$ 
[root@bestos:~/Documents/Logiciels/Nix_bidouille/2022_04_26_-_nix_fake_FHS_user_namespace/demo]# 
exit

(bash 프롬프트가 표시되지만 그 이후에는 아무 것도 입력하고 바로 종료할 수 없습니다.)

어떻게 작동하게 만들 수 있는지 아시나요?

암호:

/* userns_child_exec.c

   Copyright 2013, Michael Kerrisk
   Licensed under GNU General Public License v2 or later

   Create a child process that executes a shell command in new
   namespace(s); allow UID and GID mappings to be specified when
   creating a user namespace.
*/
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <errno.h>

/* A simple error-handling function: print an error message based
   on the value in 'errno' and terminate the calling process */

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)

struct child_args {
    char **argv;        /* Command to be executed by child, with arguments */
    int    pipe_fd[2];  /* Pipe used to synchronize parent and child */
};

static int verbose;

static void
usage(char *pname)
{
    fprintf(stderr, "Usage: %s [options] cmd [arg...]\n\n", pname);
    fprintf(stderr, "Create a child process that executes a shell command "
            "in a new user namespace,\n"
            "and possibly also other new namespace(s).\n\n");
    fprintf(stderr, "Options can be:\n\n");
#define fpe(str) fprintf(stderr, "    %s", str);
    fpe("-i          New IPC namespace\n");
    fpe("-m          New mount namespace\n");
    fpe("-n          New network namespace\n");
    fpe("-p          New PID namespace\n");
    fpe("-u          New UTS namespace\n");
    fpe("-U          New user namespace\n");
    fpe("-M uid_map  Specify UID map for user namespace\n");
    fpe("-G gid_map  Specify GID map for user namespace\n");
    fpe("            If -M or -G is specified, -U is required\n");
    fpe("-v          Display verbose messages\n");
    fpe("\n");
    fpe("Map strings for -M and -G consist of records of the form:\n");
    fpe("\n");
    fpe("    ID-inside-ns   ID-outside-ns   len\n");
    fpe("\n");
    fpe("A map string can contain multiple records, separated by commas;\n");
    fpe("the commas are replaced by newlines before writing to map files.\n");

    exit(EXIT_FAILURE);
}

/* Update the mapping file 'map_file', with the value provided in
   'mapping', a string that defines a UID or GID mapping. A UID or
   GID mapping consists of one or more newline-delimited records
   of the form:

       ID_inside-ns    ID-outside-ns   length

   Requiring the user to supply a string that contains newlines is
   of course inconvenient for command-line use. Thus, we permit the
   use of commas to delimit records in this string, and replace them
   with newlines before writing the string to the file. */

static void
update_map(char *mapping, char *map_file)
{
    int fd, j;
    size_t map_len;     /* Length of 'mapping' */

    /* Replace commas in mapping string with newlines */

    map_len = strlen(mapping);
    for (j = 0; j < map_len; j++)
        if (mapping[j] == ',')
            mapping[j] = '\n';

    fd = open(map_file, O_RDWR);
    if (fd == -1) {
        fprintf(stderr, "open %s: %s\n", map_file, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (write(fd, mapping, map_len) != map_len) {
        fprintf(stderr, "write %s: %s\n", map_file, strerror(errno));
        exit(EXIT_FAILURE);
    }

    close(fd);
}

static int              /* Start function for cloned child */
childFunc(void *arg)
{
    struct child_args *args = (struct child_args *) arg;
    char ch;

    /* Wait until the parent has updated the UID and GID mappings. See
       the comment in main(). We wait for end of file on a pipe that will
       be closed by the parent process once it has updated the mappings. */

    close(args->pipe_fd[1]);    /* Close our descriptor for the write end
                                   of the pipe so that we see EOF when
                                   parent closes its descriptor */
    if (read(args->pipe_fd[0], &ch, 1) != 0) {
        fprintf(stderr, "Failure in child: read from pipe returned != 0\n");
        exit(EXIT_FAILURE);
    }

    /* Execute a shell command */

    execvp(args->argv[0], args->argv);
    errExit("execvp");
}

#define STACK_SIZE (1024 * 1024)

static char child_stack[STACK_SIZE];    /* Space for child's stack */

int
main(int argc, char *argv[])
{
    int flags, opt;
    pid_t child_pid;
    struct child_args args;
    char *uid_map, *gid_map;
    char map_path[PATH_MAX];

    /* Parse command-line options. The initial '+' character in
       the final getopt() argument prevents GNU-style permutation
       of command-line options. That's useful, since sometimes
       the 'command' to be executed by this program itself
       has command-line options. We don't want getopt() to treat
       those as options to this program. */

    flags = 0;
    verbose = 0;
    gid_map = NULL;
    uid_map = NULL;
    while ((opt = getopt(argc, argv, "+imnpuUM:G:v")) != -1) {
        switch (opt) {
        case 'i': flags |= CLONE_NEWIPC;        break;
        case 'm': flags |= CLONE_NEWNS;         break;
        case 'n': flags |= CLONE_NEWNET;        break;
        case 'p': flags |= CLONE_NEWPID;        break;
        case 'u': flags |= CLONE_NEWUTS;        break;
        case 'v': verbose = 1;                  break;
        case 'M': uid_map = optarg;             break;
        case 'G': gid_map = optarg;             break;
        case 'U': flags |= CLONE_NEWUSER;       break;
        default:  usage(argv[0]);
        }
    }

    /* -M or -G without -U is nonsensical */

    if ((uid_map != NULL || gid_map != NULL) &&
            !(flags & CLONE_NEWUSER))
        usage(argv[0]);

    args.argv = &argv[optind];

    /* We use a pipe to synchronize the parent and child, in order to
       ensure that the parent sets the UID and GID maps before the child
       calls execve(). This ensures that the child maintains its
       capabilities during the execve() in the common case where we
       want to map the child's effective user ID to 0 in the new user
       namespace. Without this synchronization, the child would lose
       its capabilities if it performed an execve() with nonzero
       user IDs (see the capabilities(7) man page for details of the
       transformation of a process's capabilities during execve()). */

    if (pipe(args.pipe_fd) == -1)
        errExit("pipe");

    /* Create the child in new namespace(s) */

    child_pid = clone(childFunc, child_stack + STACK_SIZE,
                      flags | SIGCHLD, &args);
    if (child_pid == -1)
        errExit("clone");

    /* Parent falls through to here */

    if (verbose)
        printf("%s: PID of child created by clone() is %ld\n",
                argv[0], (long) child_pid);

    /* Update the UID and GID maps in the child */

    if (uid_map != NULL) {
        snprintf(map_path, PATH_MAX, "/proc/%ld/uid_map",
                (long) child_pid);
        update_map(uid_map, map_path);
    }
    if (gid_map != NULL) {
        snprintf(map_path, PATH_MAX, "/proc/%ld/gid_map",
                (long) child_pid);
        update_map(gid_map, map_path);
    }

    /* Close the write end of the pipe, to signal to the child that we
       have updated the UID and GID maps */

    close(args.pipe_fd[1]);

    if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */
        errExit("waitpid");

    if (verbose)
        printf("%s: terminating\n", argv[0]);

    exit(EXIT_SUCCESS);
}

편집하다

사실 이상합니다. 그룹을 작성할 때 오류가 발생하지만 uid에서는 작동합니다.

[leo@bestos:~]$ cat /proc/582197/gid_map 

[leo@bestos:~]$ cat /proc/582197/uid_map 
         0       1000          1

[leo@bestos:~]$ ll /proc/582197/gid_map 
-rw-r--r-- 1 leo users 0 mai   18 09:09 /proc/582197/gid_map

[leo@bestos:~]$ ll /proc/582197/uid_map 
-rw-r--r-- 1 leo users 0 mai   18 09:09 /proc/582197/uid_map

답변1

당신이 읽고 있는 튜토리얼은 2013년에 생성되었는데, 그 이전에는 처리할 수 있는 중요한 추가 제한 사항이 추가되었습니다.글로벌 ID2015 커널 3.19의 매핑.man user_namespaces:

"거부"라고 쓰세요도착하다/proc/[pid]/setgroups이전 파일 쓰기 /proc/[pid]/gid_map ~ 할 것이다사용자 네임스페이스에서 setgroups(2)를 영구적으로 비활성화하고상위 사용자 네임스페이스에서 CAP_SETGID 기능 없이 /proc/[pid]/gid_map에 쓰기를 허용합니다.

이것/proc/[pid]/setgroups이 파일은 Linux 3.19에 추가되었습니다.이지만 보안 문제를 해결했기 때문에 이전의 많은 안정적인 커널 시리즈로 백포트되었습니다. 문제는 "rwx---rwx"와 같은 권한을 가진 파일과 관련이 있습니다. 이러한 파일은 "기타"보다 "그룹"에 대한 권한이 더 적습니다. 이는 setgroups(2)를 사용하여 그룹을 제거하면 이전에 갖지 않았던 프로세스 파일 액세스를 허용할 수 있음을 의미합니다. 이는 사용자 네임스페이스가 존재하기 전에는 문제가 되지 않았습니다. [...] 이로 인해 이전에 권한이 없는 사용자가 그룹을 삭제하여 이전에 갖지 못한 파일 액세스 권한을 얻을 수 있었습니다. [...]

deny따라서 올바른 이름 snprintf(map_path, PATH_MAX, "/proc/%ld/setgroups", (long) child_pid);의 파일 에 단어를 쓰는 코드를 추가한 다음 gid_map.

전체 코드는 다음과 같은 유비쿼터스 명령으로 대체될 수 있습니다.

unshare --user --map-root-user --mount -- bash

(암묵적인 것이 있습니다 --setgroups=deny)

마찬가지로 권한이 없으면 하나의 uid/gid만 매핑될 수 있습니다. 따라서 마운트가 완료되면 원래 사용자를 가장할 수 있는 유일한 옵션(완전하지는 않지만)은 원래 사용자로 다시 매핑하는 것입니다. 이는 unshare최신 버전의 Too와 두 번째 계단식 사용자 네임스페이스를 사용하여 수행 할 수 있습니다. 공유되지 않음:

# unshare --user --map-user=1000 --map-group=100 -- bash

그러면 이 네임스페이스에 uid가 있게 됩니다. 루트도 더 이상 존재하지 않습니다( nobody매핑되지 않은 다른 UID처럼 매핑된 것으로 처리됩니다 ).


노트

다른 네임스페이스 및 함수와의 다른 상호 작용이 있습니다.이것은 예이다:

CAP_SYS_ADMIN프로세스의 PID 네임스페이스를 소유한 사용자 네임스페이스를 보존하면 (Linux 3.8부터) 프로세스를 마운트할 수 있습니다./프로세스 파일 시스템.

따라서 --pid --fork위의 제한 사항을 준수하기 위해 추가하면 /proc나중에 필요한 경우 기존 제한 사항에 설치할 수 있지만 일반적으로 --pid처음 사용할 때만 필요합니다(추가하여 편리하게 수행할 수도 있음 --mount-proc).

네트워크 네임스페이스와의 상호 작용으로 인해 --net마운트 도 필요합니다./sys


이 모든 것을 하나로 모아 다음 /lib의 내용 으로 대체합니다./tmp/oOP의 예:

unshare --user --map-root-user --mount -- \
    sh -c 'mount --bind /tmp/o /lib; exec unshare --user --map-user=1000 --map-group=100 -- bash'

참고: 첫 번째 매핑이 완료되면 더 이상 대부분의 권한 있는 명령을 올바르게 사용할 수 없습니다. 즉, 사용자 네임스페이스에 사용 가능한 단일 UID 0이 있거나 다음(중첩) 사용자 네임스페이스에 단일 UID 1000이 사용 가능합니다. . 권한 있는 명령은 두 UID(그 중 하나는 일반적으로 루트임)와 사용할 수 없는 UID 간의 변환을 처리하기 때문에 EINVAL을 사용한 특정 시스템 호출에서 실패하는 경우가 많습니다.

더 나은 결과를 얻으려면 먼저 다른 권한을 구성하기 위해 권한 있는 명령과 루트 액세스의 도움이 필요합니다. 예에는 setuid root 명령이 포함되며 newuidmap일반적 newgidmap으로 권한 없이 사용자로부터 전체 컨테이너를 부팅해야 합니다.

답변2

AB의 훌륭한 답변을 완성하고 AB가 작성한 주석을 더 명확하게 만들기 위해 폴더가 아직 존재하지 않는 폴더에 설치되어야 하는 경우 다음에서 chroot를 사용할 수 있습니다 unshare.

$ unshare --user --map-root-user --mount-proc --pid --fork
# cd /tmp/ && mkdir mychroot && cd mychroot
# for folder in $(ls / | grep -v sys); do echo "$folder"; mkdir "./$folder"; mount --rbind "/$folder" "./$folder"; done; mkdir sys; mount --rbind /sys sys/
# mkdir lib
# chroot .
# ls /
bin  boot  dev  etc  home  lib  mnt  nix  opt  proc  root  run  srv  sys  tmp  usr  var

여기서는 sudo를 사용할 수 없으며 일반 사용자로 모든 작업을 수행하게 됩니다. newuidmap제가 여기서 도움을 드릴 수 있는지 확인해 보겠습니다 .

/run/current-system/sw/bin/mount(참고 NixOs 사용자는 다음을 사용해야 할 수도 있습니다.mount https://github.com/NixOS/nixpkgs/issues/42117)

관련 정보