반응형
- 사용자 네임스페이스를 사용하면 유저 및 그룹 ID의 네임스페이스 별 맵핑이 가능하다.
- 이는 사용자 네임스페이스 내부의 프로세스 사용자 및 그룹 ID가 네임스페이스 외부의 ID와 다를 수 있음을 의미한다.
- 특히, 프로세스는 네임스페이스 외부에서 0이 아닌 사용자 ID를 가질 수 있는 동시에 네임스페이스 내부에 0인 사용자 ID를 가질 수 있다. 즉, 프로세스는 유저 네임스페이스 외부의 작업에 대해 권한이 없지만 네임스페이스 내부에서는 루트 권한이 있다.
Creating User namespaces
- `clone()` 또는 `unshare()`를 호출 시 `CLONE_NEWUSER`플래그를 지정하여 사용자 네임스페이스가 만들어진다. 리눅스 3.8부터(다른 유형의 네임스페이스를 만드는 데 사용되는 플래그와는 달리), 사용자 네임스페이스를 만드는데 권한이 필요하지 않다. 아래 예제에서 모든 사용자 네임스페이스는 권한이 없는 사용자 ID 1000을 사용하여 생성된다.
- 사용자 네임스페이스를 조사하기 위해 새로운 사용자 네임스페이스에 자식을 만드는 작은 프로그램인 `demo_userns.c`를 사용한다. 자식은 단순히 유효(effective) 사용자 및 그룹 ID와 `capabilities`을 표시한다. 권한이 없는 사용자로 프로그램을 실행하면 다음과 같은 결과가 생성된다.
$ id -u # Display effective user ID of shell process
1000
$ id -g # Effective group ID of shell
1000
$ ./demo_userns
eUID = 65534; eGID = 65534; capabilities: =ep
[demo_userns.c]: https://lwn.net/Articles/539941/
[capabilities]: http://man7.org/linux/man-pages/man7/capabilities.7.html
- 이 프로그램의 출력은 몇가지 흥미로운 세부사항을 보여준다. 이 중 하나는 자식 프로세스에 지정된 `capabilities`이다. 문자열 `"=ep"`(`capabilities`집합을 텍스트 표현으로 변환하는 라이브러리 함수 cap_to_text()에 의해 생성됨)는 프로그램이 권한 없는 계정에서 실행되었다 할지라도, 자식은 (permitted and effective)`capabilities`의 전체 세트를 가지고 있다는 것을 나타낸다. 사용자 네임스페이스가 생성되면, 네임스페이스의 첫 번째 프로세스는 해당 네임스페이스 안에서 전체 `capabilities`의 전체 세트가 주어진다. 이를 통해 해당 프로세스는 다른 프로세스가 네임스페이스에 생성되기 전에 네임스페이스에 필요한 초기화를 수행할 수 있다.
- 두 번째 흥미로운 점은 자식 프로세스의 사용자 및 그룹 ID이다. 위에서 언급한 것처럼, 사용자 네임스페이스 내부와 외부에서의 프로세스의 사용자 및 그룹 ID는 다를 수 있다. 그러나 사용자 네임스페이스 내부의 사용자 ID에서 네임스페이스 외부의 해당 사용자 ID의 세트로 맵핑해야 한다. 그룹 ID도 마찬가지다. 이를 통해 시스템은 사용자 네임스페이스의 프로세스가 더 넓은 시스템에 영향을 미치는 작업(예 - 네임스페이스 외부의 프로세스에 신호를 보내거나 파일에 접근)을 수행할 때 적절한 권한 검사를 수행할 수 있다.
- 프로세스 사용자 및 그룹 ID를 리턴하는 시스템 호출(예 - `getuid()`, `getgid()`)은 호출 프로세스가 상주하는 사용자 네임스페이스에 나타나는 대로 항상 `credentials`정보를 리턴한다. 사용자 ID가 네임스페이스 내부에 맵핑이 없다면, 사용자 ID를 반환하는 시스템 호출은 `/proc/sys/kernel/overflowuid `파일에 정의된 값을 리턴한다. 표준 시스템에서는 기본 값이 `65534`이다. 초기에 사용자 네임스페이스는 사용자 ID 맵핑이 없으므로, 네임스페이스 내의 모든 사용자 ID가 이 값에 맵핑된다. 마찬가지로 새로운 사용자 네임스페이스에는 그룹 ID에 대한 맵핑이 없으며 맵핑되지 않은 모든 그룹 ID는 `/proc/sys/kernel/overflowgid`(overflowuid와 동일한 기본 값을 가짐)에 맵핑된다.
- 위의 출력에서 얻을 수 없는 또 하나의 중요한 점이 있다. 비록 새로운 프로세스는 새로운 사용자 네임스페이스 안에서 `capabilities`의 전체 세트를 가지고 있더라고, 부모 네임스페이스에서는 `capabilities`를 안가지고 있다. 이는 `clone()`을 호출하는 프로세스의 `credentials` 및 `capabilities` 에 관계없이 적용된다. 특히 루트(root)가 `clone(CLONE_NEWUSER)`을 사용하더라도, 만들어진 자식 프로세스는 부모 네임스페이스에서 `capabilities`를 갖지 못한다.
- 마지막으로 사용자 네임스페이스 생성에 대한 흥미로운 점은 네임스페이스를 중첩할 수 있다는 것이다. 즉, (초기 사용자 네임스페이스 이외의) 각 사용자 네임스페이스는 부모 네임스페이스를 가지며 0개 이상의 하위 사용자 네임스페이스를 가질 수 있다. 사용자 네임스페이스의 부모는 `CLONE_NEWUSER` 플래그를 사용하여 `clone() `또는 `unshare()` 를 호출하여 사용자 네임스페이스를 만드는 프로세스의 사용자 네임스페이스다. 이 글의 마지막 부분에서 사용자 네임스페이스 간의 부모-자식 관계의 중요성이 더 명확해질 것이다.
Mapping user and group IDs
- 일반적으로 새로운 사용자 네임스페이스를 만든 후 첫번째 단계 중 하나는 해당 네임스페이스에서 생성될 프로세스의 사용자 및 그룹 ID에 사용되는 맵핑을 정의하는 것이다. 이는 사용자 네임스페이스의 프로세스 중 하나에 해당하는 `/proc/PID/uid_map` 및 `/proc/PID/gid_map` 파일(처음에 이 두 파일은 비어있다)에 맵핑 정보를 작성하여 수행된다. 이 정보는 하나 이상의 행으로 구성되며, 각 행에는 공백으로 구분된 세 개의 값이 포함된다.
ID-inside-ns ID-outside-ns length
- `ID-inside-ns` 및 `length` 값은 네임스페이스 외부의 동일한 길이의 ID 범위에 맵핑될 네임스페이스 내의 ID 범위를 함께 정의한다. `ID-outside-ns`값은 외부 범위의 시작점을 지정한다. `ID-outside-ns`가 해석되는 방법은 `/proc/PID/uid_map`(또는 `/proc/PID/gid_map`)파일을 여는 프로세스가 프로세스 PID와 동일한 사용자 네임스페이스에 있는지 여부에 따라 달라진다.
- 만일 두 프로세스가 동일한 네임스페이스에 있으면, `ID-outside-ns`는 프로세스 PID의 부모 사용자 네임스페이스에서 사용자 ID(그룹 ID)로 해석된다. 일반적인 경우는 프로세스가 자체 맵핑 파일(`/proc/self/uid_map` 또는 `/proc/self/gid_map`)에 쓰는 것이다,
- 만일 두 프로세스가 서로 다른 네임스페이스에 있으면, `ID-outside-ns`는 `/proc/PID/uid_map`(`/proc/PID/gid_map`)을 여는 프로세스의 사용자 네임스페이스에서의 사용자 ID(그룹 ID)로 해석된다. 그런 다음 쓰기(writing) 프로세스는 자체 사용자 네임스페이스를 기준으로 매핑을 정의한다.
- `demo_userns`프로그램을 다시 한번 호출하지만, 이번에는 단일 명령 행 인자(single command-line argument(모든 문자열))을 사용한다고 가정하자. 이로 인해 프로그램이 반복되어 몇 초마다 `credentials` 및 `capabilities`가 지속적으로 표시된다.
$ ./demo_userns x
eUID = 65534; eGID = 65534; capabilities: =ep
eUID = 65534; eGID = 65534; capabilities: =ep
- 이제 다른 터미널 창, 즉 다른 네임스페이스(즉, `demo_userns`를 실행하는 프로세스의 부모 사용자 네임스페이스)에서 실행되는 셸(shell) 프로세스로 전환하고 `demo_userns`가 만든 새 사용자 네임스페이스에서 자식 프로세스에 대한 사용자 ID 맵핑을 만든다.
$ ps -C demo_userns -o 'pid uid comm' # Determine PID of clone child
PID UID COMMAND
4712 1000 demo_userns # This is the parent
4713 1000 demo_userns # Child in a new user namespace
$ echo '0 1000 1' > /proc/4713/uid_map
- `demo_userns`를 실행하는 창으로 돌아가면 다음 내용을 볼 수 있다.
eUID = 0; eGID = 65534; capabilities: =ep
- 다시 말해, 부모 사용자 네임스페이스(이전에 65534로 맵핑됨)의 사용자 ID는 `demo_userns`에 의해 생성된 사용자 네임스페이스의 사용자 ID `0`에 맵핑되었다. 이 시점에서 이 사용자 ID를 처리하는 새 사용자 네임스페이스 내의 모든 작업에는 숫자 `0`이 표시되고 부모 사용자 네임스페이스의 해당 작업에는 사용자 ID `1000`과 동일한 프로세스가 표시된다.
- 마찬가지로, 새 사용자 네임스페이스에서 그룹 ID에 대한 매핑을 만들 수 있다. 다른 터미널 창으로 전환하여 부모 사용자 네임스페이스의 단일 그룹 ID `1000`에 대한 새 사용자 네임스페이스의 그룹 ID `0`에 대한 맵핑을 만든다.
$ echo '0 1000 1' > /proc/4713/gid_map
- `demo_userns`를 실행하는 창으로 다시 전환하면 변경 사항이 유효(effective) 그룹 ID 표시에 반영된다. eUID = 0; eGID = 0; capabilities: =ep
eUID = 0; eGID = 0; capabilities: =ep
Rules for writing to mapping file
- `uid_map`파일에 쓰는 것을 관리하는 여러 규칙이 있다. `gid_map`파일에 쓰는 데에도 이와 유사한 규칙이 적용된다. 이러한 규칙 중 가장 중요한 것은 다음과 같다.
- 맵핑 정의는 네임스페이스당 한 번만 수행된다. 사용자 네임스페이스에서 정확히 하나의 프로세스에 대한 `uid_map`파일에 단일쓰기만(여러 줄 바꿈으로 구분된 레코드를 포함할 수 있음) 수행할 수 있다. 또한 파일에 기록될 수 있는 라인의 수는 현재 5개로 제한된다(5개 - 향후에 증가될 수 있는 임의의 한계 값).
- `/proc/PID/uid_map`파일은 네임스페이스를 만든 사용자 ID가 소유하며 해당 사용자(또는 권한 있는 사용자)만 쓸 수 있다. 또한 다음 요구사항이 모두 충족되어야 한다.
- 쓰기(writing) 프로세스는 프로세스 PID의 사용자 네임스페이스에 `CAP_SETUID`(`gid_map`의 경우 `CAP_SETGID`) capability가 있어야 한다.
- `capabilities`와 관계없이, 쓰기 프로세스는 프로세스 PID의 사용자 네임스페이스 또는 프로세스 PID의 (바로 위) 부모 사용자 네임스페이스 내에 있어야 한다
- 다음 중 하나에 해당해야 한다.
- `uid_map`에 기록된 데이터는 부모 사용자 네임스페이스 안에 쓰기 프로세스의 유효(effective) 사용자 ID(그룹 ID)를 사용자 네임스페이스의 사용자 ID(그룹 ID)에 맵핑하는 단일 행으로 구성된다. 이 규칙을 사용하면 사용자 네임스페이스(예 - `clone()`으로 만든 자식)의 초기 프로세스가 자체 사용자 ID(그룹 ID)에 대한 맵핑을 작성할 수 있다.
- 프로세스는 부모 사용자 네임스페이스에 `CAP_SETUID`(`gid_map`의 경우 `CAP_SETGID`) capability가 있다. 이러한 프로세스는 부모 네임스페이스에서 임의의 사용자 ID(그룹 ID)에 대한 맵핑을 정의할 수 있다. 앞에서 언급했듯이, 새로운 사용자 네임스페이스의 초기 프로세스는 부모 네임스페이스에선 `capabilities`가 없다. 따라서, 부모 네임스페이스안의 프로세스만 부모 사용자 네임스페이스에 임의의 ID를 맵핑하는 맵핑을 작성할 수 있다.
Capabilities, execve() and user ID 0
- 이 시리즈의 이전 기사에서, `ns_child_exec`이라는 프로그램을 개발했었다. 이 프로그램을 `clone()`을 사용하여 명령 행 옵션으로 지정된 새 네임스페이스에서 자식 프로세스를 만든 다음, 자식 프로세스에서 셸 명령을 실행한다.
- 이 프로그램을 사용하여 새로운 사용자 네임스페이스에서 셸을 실행한 다음, 해당 셸 내에서 새로운 사용자 네임스페이스에 대한 사용자 ID 맵핑을 정의하려고 한다. 그렇게 하면 문제가 발생한다.
$ ./ns_child_exec -U bash
$ echo '0 1000 1' > /proc/$$/uid_map # $$ is the PID of the shell
bash: echo: write error: Operation not permitted
[ns_child_exec.c]: https://lwn.net/Articles/533492/
[userns_child_exec.c]: https://lwn.net/Articles/539940/
- 이 에러는 셸에 새로운 사용자 네임스페이스 내에 `capabilities`가 없이 때문에 발생한다. 다음 명령에서 확인할 수 있다.
$ id -u # Verify that user ID and group ID are not mapped
65534
$ id -g
65534
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
- bash 셸을 싱행한 `execve()``execve()`호출 헤서 문제가 발생했다. 사용자 ID가 `0`이 아닌 프로세스가 `execve()`를 수행하면 프로세스의 `capabilities`세트가 지워진다.
- 이 문제를 피하려면, `execve()`를 수행하기 전에 사용자 네임스페이스 내에 사용자 ID 맵핑을 작성해야 한다. `ns_child_exec`프로그램으로는 불가능하다. 이를 가능케하는 향상된 버전의 프로그램 (`userns_child_exec.c`)이 필요하다.
- `userns_child_exec.c`프로그램은 `ns_child_exec`프로그램과 동일한 작업을 수행하며 `-M`, `-G` 두가지 추가 명령 줄 옵션을 허용한다는 점을 제외하고 동일한 명령 줄 인터페이스를 갖는다. 이 옵션은 새로운 사용자 네임스페이스에 대한 사용자 및 그룹 ID 맵을 정의 하는 데 사용되는 문자열 인자를 허용한다. 예를 들어, 다음 명령은 새로운 사용자 네임스페이스에서 사용자 ID `1000`과 그룹 ID `1000`을 모두 `0`에 맵핑한다.
$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
- 이번에는 맵핑 파일을 업데이트하면 셸에 예상되는 사용자 ID, 그룹 ID 그리고 `capabilities`가 있음을 알 수 있다.
$ id -u
0
$ id -g
0
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff
- `userns_child_exec` 프로그램의 구현에는 미묘한 부분이 있다. 먼저 부모 프로세스 (즉, `clone ()`의 호출자) 또는 새로운 자식 프로세스가 새로운 사용자 네임 스페이스의 사용자 ID 및 그룹 ID 맵을 업데이트 할 수 있다. 그러나 위의 규칙에 따라 자식 프로세스가 정의 할 수 있는 유일한 맵핑 유형은 자신의 유효(effective) 사용자 ID만 맵핑하는 것이다. 자식에서 임의의 사용자 및 그룹 ID 맵핑을 정의하려면 부모 프로세스에서 수행해야 한다. 또한 부모 프로세스에는 적절한 `capabilities`, 즉 `CAP_SETUID`, `CAP_SETGID` 및 (부모가 맵핑 파일을 여는 데 필요한 권한이 있는지 확인) `CAP_DAC_OVERRIDE`가 있어야 한다.
Viewing user and group ID mappings
- 지금까지의 예제는 매핑 정의를 위해 `/proc/PID/uid_map` 및 `/proc/PID/gid_map` 파일을 사용하는 것을 보여준다. 이러한 파일을 사용하여 프로세스를 관리하는 맵핑을 볼 수도 있다. 이러한 파일에 쓸 때와 같이 두 번째 (`ID-outside-ns`) 값은 파일을 여는 프로세스에 따라 해석된다. 파일을 여는 프로세스가 프로세스 PID와 동일한 사용자 네임스페이스에 있는 경우, 부모 사용자 네임 스페이스와 관련하여 `ID-outside-ns`가 정의된다. 파일을 여는 프로세스가 다른 사용자 네임스페이스에있는 경우, 파일을 여는 프로세스의 사용자 네임스페이스와 관련하여 `ID-outside-ns`가 정의된다.
- 셸을 실행하는 두 개의 사용자 네임스페이스를 작성하고, 네임스페이스에서 프로세스의 `uid_map`파일을 검사하여 이를 설명할 수 있다. 셸을 실행하는 프로세스를 가진 새로운 사용자 네임스페이스를 만드는 것으로 시작하자.
$ id -u # Display effective user ID
1000
$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' bash
$ echo $$ # Show shell's PID for later reference
2465
$ cat /proc/2465/uid_map
0 1000 1
$ id -u # Mapping gives this process an effective user ID of 0
0
- 이제 다른 터미널 창으로 전환하고 다른 사용자 및 그룹 ID 맵핑을 사용하는 형제(sibling) 사용자 네임스페이스를 생성한다고 가정하자.
$ ./userns_child_exec -U -M '200 1000 1' -G '200 1000 1' bash
$ cat /proc/self/uid_map
200 1000 1
$ id -u # Mapping gives this process an effective user ID of 200
200
$ echo $$ # Show shell's PID for later reference
2535
- 두 번째 사용자 네임스페이스에서 실행 중인 두 번째 터미널 창에서 계속해서 다른 사용자 네임 스페이스에서 프로세스의 사용자 ID 맵핑을 보자.
$ cat /proc/2465/uid_map
0 200 1
- 이 명령의 출력은 다른 사용자 네임스페이스의 사용자 ID `0`이 해당(this) 네임스페이스의 사용자 ID `200`에 맵핑됨을 보여준다. 커널은 파일로부터 읽어 들이는 프로세스의 사용자 네임스페이스에 따라 `ID-outside-ns`값을 생성하므로, 다른 사용자 네임스페이스에서 실행될 때 동일한 명령이 다른 출력을 생성했다.
- 첫 번째 터미널 창으로 전환하고 프로세스의 사용자 ID 맵핑 파일을 두 번째 사용자 네임스페이스에 표시하면 역방향(converse) 맵핑을 볼 수 있다.
$ cat /proc/2535/uid_map
200 0 1
- 여기서도 출력은 두번째 사용자 네임스페이스에서 실행될 때 동일한 명령과 다르다. 파일로부터 읽어 들이는 프로세스의 사용자 네임스페이스에 따라 `ID-outside-ns` 값이 생성되기 때문이다. 물론 초기 네임스페이스에서 첫번째 네임스페이스의 사용자 ID `0`과 두번째 네임스페이스의 사용자 ID `200`은 모두 사용자 ID `1000`에 매핑된다. 초기 사용자 네임스페이스 내의 세 번째 셸 창에서 다음 명령을 실행하여 이를 확인할 수 있다.
$ cat /proc/2465/uid_map
0 1000 1
$ cat /proc/2535/uid_map
200 1000 1
반응형
'운영체제 > LINUX' 카테고리의 다른 글
[Linux] Linux OS Booting Process 정리 (0) | 2021.05.11 |
---|---|
[Linux] Capabilities (0) | 2019.12.17 |
[Linux] PID 네임스페이스 (0) | 2019.12.14 |
[Linux] IPC 네임스페이스 (0) | 2019.12.13 |
[Linux] UTS 네임스페이스 (0) | 2019.12.13 |