Docker없이 컨테이너 만들기 Part2
Docker없이 컨테이너 만들기 Part 2: 네임스페이스
아래의 내용은 Kakao 발표자료와 영상을 기반으로 작성했습니다.
들어가며
컨테이너는 논리적으로 잘 격리된 프로세스이다. 여기서는 컨테이너 상태를 만들기 위해 필요한 필요한 프로세스 격리 기술을 하나씩 알아보고 실습을 진행해보고자 한다. 앞선 포스팅에서 디렉터리를 격리하는 방법에 대해 살펴봤다. 이번편에서는 네임스페이스를 통해 가능한 격리 옵션을 살펴보려고 한다.
네임스페이스
리눅스에서는 네임스페이스를 통해 여러 상태를 격리할 수 있다.
1
2
3
4
# /proc 경로를 통해 확인
ls -al /proc/{pid}/ns
# 명령어를 통해 확인 (출력이 깔끔)
lsns -p {pid}
PID
PID는 Processs ID의 약자로, 리눅스에서 프로세스를 식별하는 ID이다. 아래의 그림처럼 네임스페이스 별로 본연의 ID와 해당 네임스페이스 기준으로 ID가 나뉘며, 이런 네임스페이스 구분을 통해 관리가 편해진다.
- 자식 PID 네임스페이스는 부모 프로세스 ID와 자신의 네임스페이스 ID 값을 가진다.
출처: https://dev4devs.com/2019/10/19/understanding-containers-in-15-minutes/
추가로 컨테이너 실행할 때 명령어 끝나고 죽는데 그것은 프로세스가 격리되어서 Init 프로세스 자체가 컨테이너 실행시 진행되는 명령어만 수행하고 죽어, 컨테이너의 모든 프로세스가 죽기 때문이다.
자세히 알아보면 “unshare할 때 fork 하여, 자식 PID 네임스페이스의 pid 1로 실행”할 때 pid 1 (init) 이 종료되면 pid namespace 도 종료된다. Container 실행 시 바로 종료되는 명령어만 실행하면 컨테이너가 끝나는 이유도 동일하다. (init process가 echo와 같은 명령어라면, 실행 후 종료되므로 Init 프로세스가 죽어 컨테이너 자체도 끝난다.)
실습
[터미널 1에서는 프로세스 네임스페이스를 실행한다.]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo unshare -fp --mount-proc /bin/sh
# echo $$
1
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 12:09 pts/2 00:00:00 /bin/sh
root 2 1 0 12:10 pts/2 00:00:00 ps -ef
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2892 1664 pts/2 S 12:09 0:00 /bin/sh
root 3 0.0 0.1 10464 3328 pts/2 R+ 12:10 0:00 ps aux
# lsns -t pid -p 1
NS TYPE NPROCS PID USER COMMAND
4026532207 pid 2 1 root /bin/sh
# sleep 10000
[터미널 2는 호스트로, 실제 터미널1에서 격리된 프로세스를 죽여본다.]
1
2
echo $$
12566
- 프로세스 확인
1
2
3
4
5
6
ps aux | grep '/bin/sh'
root 14092 0.0 0.2 11904 5632 pts/0 S+ 12:09 0:00 sudo unshare -fp --mount-proc /bin/sh
root 14093 0.0 0.1 11904 2300 pts/2 Ss 12:09 0:00 sudo unshare -fp --mount-proc /bin/sh
root 14094 0.0 0.0 6192 1792 pts/2 S 12:09 0:00 unshare -fp --mount-proc /bin/sh
root 14095 0.0 0.0 2892 1664 pts/2 S+ 12:09 0:00 /bin/sh
ubuntu 14314 0.0 0.1 7008 2432 pts/1 S+ 12:10 0:00 grep --color=auto /bin/sh
- 격리된 프로세스 확인(아래의 출력 중 NS를 네임스페이스가 다른 것을 확인할 수 있다.)
1
2
3
sudo lsns -t pid -p 14095
NS TYPE NPROCS PID USER COMMAND
4026532207 pid 1 14095 root /bin/sh
- sleep 프로세스를 죽여본다. (격리된 프로세스 환경 속에 있는)
1
sudo kill -SIGKILL $(pgrep sleep)
위의 명령어를 실행하면, 터미널1에서 “killed”로 표시되며 격리 환경이 exit되는 것을 확인할 수 있다.
mount
pivot_root에서 설명한 마운트 네임스페이스를 실습한다.
실습
- 터미널1
1
2
3
sudo unshare -m
# 격리된 환경에서 아래의 명령어를 실행한다.
lsns -p $$
- 터미널2
1
sudo lsns -p 1 | grep mnt
아래의 사진을 확인해보면 mnt의 네임스페이스의 값이 다른 것을 확인할 수 있다.
UTS 네임스페이스
Unix Time Sharing (여러 사용자 작업 환경 제공하고자 서버 시분할 나눠쓰기) 특히 호스트명, 도메인명을 격리할 수 있다.
실습
- 터미널1:
unshare -u
옵션을 통해 격리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sudo unshare -u
root@MyServer:/tmp# lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 106 1 root /sbin/init
4026531835 cgroup 106 1 root /sbin/init
4026531836 pid 106 1 root /sbin/init
4026531837 user 106 1 root /sbin/init
4026531839 ipc 106 1 root /sbin/init
4026531840 net 106 1 root /sbin/init
4026531841 mnt 98 1 root /sbin/init
4026532206 uts 2 10048 root -bash
root@MyServer:/tmp# hostname
MyServer
root@MyServer:/tmp# hostname KANS
root@MyServer:/tmp# hostname
KANS
root@MyServer:/tmp#
- 터미널2: 호스트에서 변경된 내용이 있는지 확인한다.(호스트명의 변화가 없음을 확인할 수 있다.)
1
2
3
4
5
hostname
MyServer
sudo lsns -p 1 | grep uts
4026531838 uts 104 1 root /sbin/init
USER
USER 네임스페이스 : 2012년, UID/GID 넘버스페이스 격리한다. User 네임스페이스는 비교적 최근에 나온 기술이다. 이를 통해 컨테이너의 root 권한 문제를 해결할 수 있다 우선 컨테이너의 root 권한을 아래의 실습을 통해 확인해보자.
실습
- 컨테이너를 실행한다.
1
docker run -it ubuntu /bin/sh
[docker 내부 환경]
1
2
3
lsns -p $$ -t user
NS TYPE NPROCS PID USER COMMAND
4026531837 user 2 1 root /bin/sh
[Host 환경]
1
2
3
4
ps -ef |grep "/bin/sh"
ubuntu 16140 15353 0 12:17 pts/0 00:00:00 docker run -it ubuntu /bin/sh
root 16286 16242 0 12:18 pts/0 00:00:00 /bin/sh
ubuntu 16584 12566 0 12:21 pts/1 00:00:00 grep --color=auto /bin/sh
네임스페이가 같은지 호스트에서 확인해보자
1
2
3
lsns -p $$ -t user
NS TYPE NPROCS PID USER COMMAND
**4026531837** user 5 12285 ubuntu /lib/systemd/systemd --user
“4026531837” = “4026531837”으로 유저 네임스페이스가 같다.
“컨테이너는 호스트와 네임스페이스도 같고, 권한도 Root를 가져간다.” 이는 보안상 취약하다.
우리는 아래에서 User 네임스페이스를 통해 격리하는 방식을 진행해본다.
(docker도 유저 네임스페이스는 지원하나, 기본값은 아니다.)
- 격리된 상태
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unshare -U --map-root-user /bin/sh
# whoami
root
#
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 2 16856 root /bin/sh
4026531835 cgroup 2 16856 root /bin/sh
4026531836 pid 2 16856 root /bin/sh
4026531838 uts 2 16856 root /bin/sh
4026531839 ipc 2 16856 root /bin/sh
4026531840 net 2 16856 root /bin/sh
4026531841 mnt 2 16856 root /bin/sh
4026532206 user 2 16856 root /bin/sh
- 호스트
1
2
3
4
ps -ef |grep "/bin/sh"
ubuntu 6874 5348 0 15:42 pts/0 00:00:00 /bin/sh
ubuntu 16909 15353 0 12:24 pts/0 00:00:00 /bin/sh
ubuntu 16927 12566 0 12:24 pts/1 00:00:00 grep --color=auto /bin/sh
호스트에서 확인했을 때, root권한이 아닌 ubuntu 유저로 확인된다.
💡 k8s, docker 환경에서
k8s, docker에서는 usernamespace가 기본값이 아님.
docker가 User Namespace를 지원. root 권한에 대한 취약점을 해결하기 위해
Pod에서도 User Namespace를 지원함. (v1.30 beta)
Time
프로세스가 볼 수 있는 시스템 시간을 격리하여, 특정 프로세스에 대해 다른 시간대를 설정할 수 있다. 별도로 실습은 진행하지 않는다.
마치며
리눅스에서 기본적으로 제공하는 네임스페이스 형태에 대해 알아봤다. PID, 마운트, UTS, Time 등 다양한 상태에 대해 격리를 제공한다. 다음 편에서는 자원 할당과 관련된 내용을 살펴볼 예정이다.