컨테이너는 VM과 달리 Host의 OS를 공유한다.
VM은 커널을 포함해 운영체제 전체의 복사본을 실행하는 반면, 컨테이너는 호스트 컴퓨터의 커널을 공유한다.
격리 측면에서 VM이 컨테이너보다 유리하지만 많은 사람들이 컨테이너를 사용하는 이유는 아래와 같은 점들 때문이다.
- VM은 시동하는 데 컨테이너보다 시간이 훨씬 많이 걸린다. 컨테이너를 실행하는 것은 그냥 새 리눅스 프로세스를 띄우는 것일 뿐이지만, VM을 실행하려면 모든 부팅 및 초기화 과정을 거쳐야 한다.
- 각 VM은 커널 전체를 실행해야 하므로 추가 부담이 존재한다. 반면 컨테이너들은 하나의 커널을 공유하기 때문에 자원 면에서나 성능 면에서 대단히 효율적이다.
1. Cgroups(control groups) 격리
cpu, memory, network 등 프로세스 그룹의 시스템 리소스 사용량을 관리한다. 어떤 리소스 사용량이 많다면 그 애플리케이션을 cgroup에 넣어서 cpu와 memory 등의 사용 제한을 가능하게 한다.
보통 /sys/fs/crouop 에서 여러 종류의 제어 그룹 위계구조들을 볼 수 있다.
$ snchoi@snchoi:/$ ls sys/fs/cgroup/
cgroup.controllers cgroup.threads dev-mqueue.mount io.stat sys-fs-fuse-connections.mount
cgroup.max.depth cpu.pressure init.scope memory.numa_stat sys-kernel-config.mount
cgroup.max.descendants cpuset.cpus.effective io.cost.model memory.pressure sys-kernel-debug.mount
cgroup.procs cpuset.mems.effective io.cost.qos memory.stat sys-kernel-tracing.mount
cgroup.stat cpu.stat io.pressure misc.capacity system.slice
cgroup.subtree_control dev-hugepages.mount io.prio.class proc-sys-fs-binfmt_misc.mount user.slice
- 제어 그룹들을 잘 조율하면 한 프로세스가 자원을 혼자 너무 많이 사용해서 다른 프로세스의 행동에 영향을 미치는 일을 방지할 수 있다.
- 하나의 제어 그룹에 속할 수 있는 프로세스의 수를 제한하는 pid 라는 제어 그룹도 있다.
- 위계구조마다 그것을 제어하는 제어 그룹 제어기 (cgroup controller)가 있다.
- 모든 리눅스 프로세스는 각 자원 종류의 위계구조에 있는 한 제어 그룹에 속한다.
- 한 프로세스가 처음 생성될 때 프로세스는 부모 프로세스의 제어 그룹들을 물려받는다.
- 제어 그룹들을 관리한다는 것은 결국 이 위계구조들에 있는 파일들과 디렉터리들을 읽고 쓰는 것에 해당한다.
- 컨테이너를 실행하면 런타임이 컨테이너를 위한 새 제어그룹들을 생성한다.
- 컨테이너 안에서는 컨테이너가 속한 제어 그룹들을 /proc 디렉터리의 특정 파일에서 볼 수 있다.
- 제어 그룹의 특정 매개변수를 변경하려면 해당 파일에 값을 기록하면 된다.
- 제어 그룹의 특정 파일에 프로세스 ID를 추가하면 해당 프로세스가 제어 그룹에 배정된다.
2. 리눅스 이름공간
-
- cgroups가 프로세스가 사용할 수 있는 자원을 제한한다면, namespace는 프로세스가 볼 수 있는 것들을 제한한다.
- 프로세스를 어떤 namespace에 넣으면 프로세스는 그 namespace가 허용하는 것들만 볼 수 있게 된다.
- 현재 리눅스는 다음과 같은 여러 종류의 namespace를 지원한다.
- 유닉스 시분할 시스템 (UTS) - 프로세스가 인식하는 시스템의 호스트 이름과 도메인 이름들에 관한 namespace
- 프로세스 ID
- 마운트 지점
- 네트워크
- 사용자 ID, 그룹 ID
- IPC (inter-process communication, 프로세스 간 통신)
- 제어 그룹
현재 존재하는 이름공간들은 lsns 라는 명령으로 확인할 수 있다.
$ snchoi@snchoi:~$ lsns
NS TYPE NPROCS PID USER COMMAND
4026531834 time 54 1264 snchoi /lib/systemd/systemd --user
4026531835 cgroup 54 1264 snchoi /lib/systemd/systemd --user
4026531836 pid 54 1264 snchoi /lib/systemd/systemd --user
4026531837 user 54 1264 snchoi /lib/systemd/systemd --user
4026531838 uts 54 1264 snchoi /lib/systemd/systemd --user
4026531839 ipc 54 1264 snchoi /lib/systemd/systemd --user
4026531840 net 54 1264 snchoi /lib/systemd/systemd --user
4026531841 mnt 54 1264 snchoi /lib/systemd/systemd --user
2.1. 호스트 이름 격리 (UTS)
- 호스트 이름(hostname)과 도메인 이름들에 관한 것이다.
- 프로세스를 독자적인 UTS namespace에 넣으면 그 프로세스가 인식하는 호스트 이름을 그 프로세스가 실행 중인 컴퓨터 또는 VM의 호스트 이름과는 무관하게 임의로 변경할 수 있다.
# (호스트)
$ snchoi@snchoi:~$ hostname
snchoi
# (컨테이너)
$ snchoi@snchoi:~$ sudo docker run --rm -it --name hello ubuntu bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
5af00eab9784: Pull complete
Digest: sha256:0bced47fffa3361afa981854fcabcd4577cd43cebbb808cea2b1f33a3dd7f508
Status: Downloaded newer image for ubuntu:latest
$ root@986688c2be44:/$ hostname
986688c2be44
- 컨테이너 시스템은 각 컨테이너 ID가 호스트 이름으로 쓰인다
- 컨테이너 이름(hello)을 지정했다고 해도 그 이름이 컨테이너의 이름으로 쓰이지는 않는다
- 컨테이너가 개별적인 호스트 이름을 가지는 이유는 도커가 개별적인 UTS namespace를 컨테이너에 적용했기 때문이다
# (격리)
# 개별적인 namespace를 가진 프로세스를 만드는 수단으로 unshare 명령이 있다
$ snchoi@snchoi:~$ sudo unshare --uts sh
$ hostname
snchoi
$ hostname experiment
$ hostname
experiment
$ exit
# (호스트)
$ snchoi@snchoi:~$ hostname
snchoi
- 개별적인 UTL 이름공간을 가진 새 프로세스에서 sh 셸을 실행한다.
- 호스트 이름을 변경해도 호스트 컴퓨터의 호스트 이름은 변하지 않는다.
2.2. 프로세스 ID 격리
도커 컨테이너 안에서 ps 명령을 실행하면 그 컨테이너 안에서 실행되는 프로세스들만 나올 뿐 호스트에서 실행되는 프로세스들은 나오지 않는다.
컨테이너에 개별적인 프로세스 ID namespace를 적용했기 때문이다.
$ snchoi@snchoi:~$ sudo docker run --rm -it --name hello ubuntu bash
$ root@5820b5a88ea6:/# ps -eaf
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 00:56 pts/0 00:00:00 bash
root 9 1 0 00:56 pts/0 00:00:00 ps -eaf
$ root@5820b5a88ea6:/# exit
프로세스 ID 이름공간 격리도 unshare 명령을 통해 진행해보자
# (격리)
$ snchoi@snchoi:~$ sudo unshare --pid sh
$ whoami
root
$ whoami
sh: 2: Cannot fork
$ whoami
sh: 3: Cannot fork
$ whoami
sh: 4: Cannot fork
$ exit
첫 프로세스는 잘 작동한 것으로 보이지만, 그 이후의 모든 명령은 포크가 불가능하다는 이유로 실패한다.
- 프로세스가 1씩 증가했다.
- 프로세스 ID 들을 보면 첫 whoami의 프로세스 ID 가 1 이었음을 짐작할 수 있다.
- 프로세스 ID 가 1로 시작했다는 점은 호스트와는 개별적인 새 PID namespace가 작용했음을 말해준다.
# (호스트)
$ snchoi@snchoi:~$ ps fa
PID TTY STAT TIME COMMAND
3527676 pts/0 Ss 0:00 bash
3666327 pts/0 S 0:00 \_ sudo unshare --pid sh
3666328 pts/0 S+ 0:00 \_ sh
unshare 매뉴얼 페이지의 —fork 플래그 항목에 대한 설명에 따르면, 이 플래그는 “지정된 프로그램을 직접 실행하는 대신 unshare의 자식 프로세스로 포크한다. 새 pid namespace를 만들 때 유용하다”
- sh 프로세스가 unshare의 자식이 아니라 sudo 의 자식인 것이 문제이다
왜 PID namespace에서만 이런 일이 발생?
구구절절 설명을 해보면 다음과 같다.
프로세스 ID를 격리했기 때문에 새로운 격리공간 안에서 프로세스는 독립된 PID 번호를 가지게 된다.
새로운 PID 네임스페이스 내에서 첫 번째 프로세스는 항상 PID 1이 되어야 한다.
PID 1은 init 프로세스처럼 동작하며 다른 모든 프로세스의 부모 프로세스로 작동한다.
그러나 PID 격리지 --fork 옵션을 주지 않으면 sudo 명령이 1을 가지게 되고 이후 명령부터는 프로세스가 sudo 를 부모로 하여 동작하기 때문에 unshare 명령 프로세스가 PID 1이 되도록 해야한다.
따라서 현재 프로세스를 새로운 네임스페이스 내에서 PID 1로 만들기 위해서는 --fork 옵션을 사용하여 새로운 프로세스를 생성하고 이 새로운 프로세스가 새로운 네임스페이스 내에서 PID 1로 동작하도록 해야한다.
—fork 플래그를 지정해서 다시 시도해보자
# (격리)
$ snchoi@snchoi:~$ sudo unshare --pid --fork sh
$ whoami
root
$ whoami
root
# (호스트)
$ snchoi@snchoi:~$ ps fa
PID TTY STAT TIME COMMAND
3527676 pts/0 Ss 0:00 bash
3673711 pts/0 S 0:00 \_ sudo unshare --pid --fork sh
3673712 pts/0 S 0:00 \_ unshare --pid --fork sh
3673713 pts/0 S+ 0:00 \_ sh
- —fork 플래그를 지정해서 unshare를 실행하면 sh 셸이 unshare 프로세스의 자식으로 실행되므로, 이 셸 안에서 얼마든지 많은 자식 명령을 실행할 수 있다.
- 셸이 자신만의 프로세스 ID namespace 안에 있으므로 ps 명령의 출력이 아주 간단할 것 같지만, 실제로는 그렇지 않다.
- ps는 /proc 의 가상 파일들을 읽어서 작동한다.
- ps가 새 namespace 안에 있는 프로세스들에 관한 정보만 출력하게 하려면 호스트의 것과는 개별적인 /proc 디렉터리에 프로세스들에 관한 정보를 기록하게 만들어야 한다.
- /proc 는 루트 디렉터리 바로 아래에 있는 디렉터리이므로, 그러려면 루트 디렉터리 자체를 변경해야 한다.
2.3. 루트 디렉터리 변경
- 컨테이너 안에서는 호스트의 파일 시스템 전체를 볼 수 없다.
- 컨테이너 안에서는 파일 시스템 전체의 한 부분집합을 볼 수 있을 뿐인데, 이는 컨테이너 실행 시 루트 디렉터리가 바뀌기 때문이다.
- 리눅스에서 루트 디렉터리는 chroot 라는 명령으로 변경할 수 있다.
- 이 명령은 현재 프로세스에 대한 루트 디렉터리가 파일 시스템 안의 다른 어떤 위치를 가리키게 만든다.
- chroot 명령을 성공적으로 실행하고 나면, 전체 파일 시스템 위계구조에서 새 루트 디렉터리보다 위쪽에 있는 것에는 전혀 접근할 수 없다.
$ snchoi@snchoi:~$ mkdir new_root
$ snchoi@snchoi:~$ sudo chroot new_root/
[sudo] password for snchoi:
chroot: failed to run command /bin/bash: No such file or directory
- 새 디렉터리를 생성하고 chroot 로 루트 디렉터리로 만들어보았지만, 제대로 동작하지 않는다.
- 문제의 원인은, 새 루트 디렉터리에 bin 디렉터리가 없다는 것이다. 따라서 /bin/bash 셸도 실행할 수 없다.
- 새 루트에서 어떤 명령을 실행하려면 적절한 디렉터리에 실행 파일이 있어야 한다.
- 컨테이너에 이런 문제가 없는 것은, 필요한 파일들이 담긴 파일 시스템을 미리 컨테이너 이미지에 담아 두고 그로부터 컨테이너 인스턴스를 만들어서 실행하기 때문이다.
- 알파인 리눅스를 마치 컨테이너처럼 실행해보자!
$ snchoi@snchoi:~$ mkdir alpine
$ snchoi@snchoi:~$ cd apline
$ snchoi@snchoi:~/alpine$ curl -o alpine.tar.gz https://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.0-x86_64.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2562k 100 2562k 0 0 7255k 0 --:--:-- --:--:-- --:--:-- 7259k
$ snchoi@snchoi:~/alpine$ ls
alpine.tar.gz
$ snchoi@snchoi:~/alpine$ tar xvf alpine.tar.gz
$ snchoi@snchoi:~/alpine$ rm alpine.tar.gz
$ snchoi@snchoi:~/alpine$ ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
- 알파인 배포판을 지역 디렉터리에 풀어 놓았으니, 이제 chroot로 alpine 디렉터리를 루트 디렉터리로 만들고 그 디렉터리 위계구조에 있는 명령을 실행해 볼 수 있다.
$ snchoi@snchoi:~$ sudo chroot alpine/ ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ snchoi@snchoi:~$ sudo chroot alpine/ sh
/ # ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ # whoami
root
/ # exit
- chroot는 이름 그대로 프로세스의 “루트(root)를 변경한다.”
- 루트가 바뀌면 프로세스는 파일 시스템 위계구조에서 새 루트 디렉터리보다 아래에 있는 파일들과 디렉터리들에만 접근할 수 있다.
2.4. 이름공간과 루트 변경의 조합
- namespace와 루트 변경을 조합할 수 있다.
- 루트 디렉터리 변경 기능을 이용하면 호스트의 /proc과는 독립적인 /proc 디렉터리를 컨테이너가 사용하게 만들 수 있다.
- 커널이 디렉터리에 프로세스 정보를 채우게 하려면 이 디렉터리를 proc 형식의 유사(pseudo) 파일 시스템으로 마운트해야 한다.
$ snchoi@snchoi:~$ sudo unshare --pid --fork chroot alpine sh
/ $ mount -t proc proc proc
# 드디어 자신의 프로세스들만 볼 수 있게 하는 프로세스 ID 격리에 성공하였다!
/ $ ps
PID USER TIME COMMAND
1 root 0:00 sh
4 root 0:00 ps
2.5. 마운트 이름공간
- 컨테이너가 호스트의 모든 파일 시스템 마운트에 접근하는 것은 바람직하지 않다.
- 컨테이너에 개별적인 마운트 namespace를 부여하면 특정 마운트들만 보이게 만들 수 있다.
# (호스트)
$ snchoi@snchoi:~$ sudo unshare --mount sh
$ mkdir source
$ touch source/HELLO
$ ls source
HELLO
$ mkdir target
# 독자적인 마운트 namespace를 가진 프로세스를 띄우고 간단한 바인드 마운트를 만들어보았다
$ mount --bind source target
$ ls target
HELLO
$ findmnt target
TARGET SOURCE FSTYPE OPTIONS
/home/snchoi/target /dev/mapper/ubuntu--vg-ubuntu--lv[/home/snchoi/source] ext4 rw,relatime
$ findmnt
... (엄청나게 많은 마운트 데이터 출력)
- 호스트에서 findmnt target 을 해보면 아무 데이터도 나오지 않지만 마운트 namespace 안에서 findmnt를 하면 꽤 긴 출력이 나온다
- 프로세스 ID 격리와 비슷하게 커널이 /proc/<프로세스ID>/mounts 파일에서 마운트 정보를 읽어서 프로세스에 제공하기 때문이다
- 컨테이너화된 프로세스의 마운트 집합을 완전히 격리하려면 새 루트 디렉터리와 새 마운트 namespace, 그리고 새 proc 마운트 디렉터리를 조합해야 한다
# (격리)
$ snchoi@snchoi:~$ sudo unshare --mount chroot alpine sh
/ $ mount -t proc proc proc
/ $ mount
proc on /proc type proc (rw,relatime)
/ $ mkdir source
/ $ touch source/HELLP
/ $ mkdir target
/ $ mount --bind source target
/ $ mount
proc on /proc type proc (rw,relatime)
/dev/mapper/ubuntu--vg-ubuntu--lv on /target type ext4 (rw,relatime)
2.6. 네트워크 이름공간
컨테이너가 볼 수 있는 네트워크 인터페이스들과 라우팅 테이블을 제한할 수 있다.
# (호스트)
# 네트워크 namespace의 이름들은 lsns 명령으로 확인할 수 있다
$ snchoi@snchoi:~$ sudo lsns -t net
NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
4026531840 net 181 1 root unassigned /sbin/init
4026532413 net 1 718 root unassigned /usr/libexec/accounts-daemon
# (격리)
$ snchoi@snchoi:~$ sudo unshare --net bash
$ root@snchoi:/home/snchoi$ echo $$
3552
$ root@snchoi:/home/snchoi$ lsns -t net | grep 3552
4026532476 net 2 3552 root unassigned bash
새 네트워크 namespace를 부여받은 프로세스는 LOOPBACK 인터페이스만 가지고 시작한다.
# (호스트)
$ snchoi@snchoi:~$ sudo unshare --net bash
# (격리)
$ root@snchoi:/home/snchoi$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
- LOOPBACK 인터페이스만으로는 다른 누군가와 통신할 수 없다
- 외부 세계로의 통로를 뚫으려면 가상 이더넷 인터페이스를 만들어야 한다. (한 쌍의 가상 이더넷 인터페이스가 필요하다)
# (격리) 가상 이더넷 쌍을 만든다
$ root@snchoi:/home/snchoi$ ip link add ve1 netns 3552 type veth peer name ve2 netns 1
# (격리)
$ root@snchoi:/home/snchoi$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ve1@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether f6:55:54:c7:b6:70 brd ff:ff:ff:ff:ff:ff link-netnsid 0
- 링크의 상태가 “DOWN” 이다. 따라서 지금은 이 링크로 통신을 할 수 없다. 이쪽 끝뿐만 아니라 다른 쪽 끝도 “UP”이 되게 해야한다.
# (호스트) ve2 활성화
$ snchoi@snchoi:~$ sudo ip link set up ve2
# (격리) ve1 활성화
$ root@snchoi:/home/snchoi$ ip link set ve1 up
$ root@snchoi:/home/snchoi$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ve1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether f6:55:54:c7:b6:70 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::f455:54ff:fec7:b670/64 scope link
valid_lft forever preferred_lft forever
- IP 데이터를 전송하려면 인터페이스에 IP 주소를 부여해야 한다.
# (격리) ve1에 IP 주소를 부여한다.
$ root@snchoi:/home/snchoi$ ip addr add 192.168.1.100/24 dev ve1
# (호스트) ve2에 IP 주소를 부여한다.
$ snchoi@snchoi:~$ sudo ip addr add 192.168.1.200/24 dev ve2
- 이렇게 하면 하나의 IP 경로가 컨테이너 라우팅 테이블에 추가되는 효과도 생긴다.
# (격리)
# 컨테이너는 IP 주소 192.168.1.100/24 에서(만) 데이터를 주고받을 수 있다
$ root@snchoi:/home/snchoi$ ip route
192.168.1.0/24 dev ve1 proto kernel scope link src 192.168.1.100
- 이 라우팅 정보는 호스트의 라우팅 테이블과는 독립적이다.
- 컨테이너는 IP 주소 192.168.1.100/24 에서(만) 데이터를 주고받을 수 있다.
- 이를 확인하기 위해, 호스트에서 컨테이너의 주소로 핑을 날려본다.
# (호스트)
# 통신 확인을 위해 호스트에서 컨테이너의 주소로 핑을 날려본다
$ snchoi@snchoi:~$ ping 192.168.1.100
PING 192.168.1.100 (192.168.1.100) 56(84) bytes of data.
64 bytes from 192.168.1.100: icmp_seq=1 ttl=64 time=0.157 ms
64 bytes from 192.168.1.100: icmp_seq=2 ttl=64 time=0.101 ms
64 bytes from 192.168.1.100: icmp_seq=3 ttl=64 time=0.132 ms
2.7. 사용자 이름공간
- 사용자, 그룹ID 격리의 주된 장점은 컨테이너 안에서 루트ID 0을 호스트의 비루트 계정에 부여할 수 있다는 것이다
- 컨테이너 안에서는 소프트웨어 자체는 루트 계정으로 실행되지만, 공격자가 컨테이너에서 탈출해서 호스트로 간다고 해도 거기에서는 권한 없는 비루트계정일 뿐이다
# (격리)
$ snchoi@snchoi:~$ unshare --user bash
$ nobody@snchoi:~$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
$ nobody@snchoi:~$ echo $$
2162
- 새 사용자 namespace에서 사용자의 계정은 nobody이다.
- 기존 사용자들에 접근하려면 namespace 바깥쪽과 안쪽의 사용자ID들을 연결해줘야 한다.
- 대응(연결) 관계에 관한 정보는 /proc/<프로세스ID>/uid_map 파일에 있다. (수정을 위해서 루트 계정이 필요하다)
# 호스트에서 uid_map 파일을 수정한다.
# (호스트)
$ snchoi@snchoi:~$ sudo echo '0 1000 1' > /proc/2162/uid_map
이렇게 하면 자식 프로세스의 사용자가 루트가 된다.
$ nobody@snchoi:~$ id
uid=0(root) gid=65534(nogroup) groups=65534(nogroup)
도커의 사용자 이름공간 제약
-
- 도커는 사용자 namespace를 지원하지만, 기본적으로는 사용자 namespace 기능이 비활성화되어 있다. → 기본적으로 도커 컨테이너는 root 사용자로 실행된다
- 쿠버네티스는 이 기능의 도입이 논의된 적은 있지만 아직 지원하지 않는다.
- 사용자 namespace가 도커 사용자들이 원할 만한 몇 가지 것과 호환되지 않기 때문이다.
- 사용자 namespace는 호스트와의 프로세스 ID 공간 또는 네트워크 namespace 공유와 호환되지 않는다. (호스트의 프로세스와 컨테이너의 프로세스 간의 ID 매핑의 복잡성, 네트워크 리소스 접근 권한 관리의 복잡성)
- 컨테이너 안에서 프로세스가 루트로 실행된다고 해도 전체 루트 권한을 가지지는 않는다. 예를 들어 CAP_NET_BIND_SERVICE 능력이 없어서 낮은 번호의 포트들에는 바인딩할 수 없다.
- 컨테이너화된 프로세스가 파일과 상호작용하려면 적절한 접근 권한들이 필요하다 (호스트에서 마운트한 파일의 경우 문제가 될 수 있다)
2.8. IPC 이름공간
- 리눅스는 서로 다른 두 프로세스가 메모리의 공유 영역에 접근해서, 또는 공유 메시지 대기열을 이용해서 통신하는 기능을 제공한다.
- 그런 통신이 가능해지려면 두 프로세스가 같은 IPC namespace에 있어야 한다.
- 일반적으로 컨테이너가 다른 컨테이너의 공유 메모리에 접근하게 하는 것은 바람직하지 않다.
- 이 때문에 컨테이너는 개별적인 IPC namespace에 있어야 한다.
# (호스트)
# ipcmk 명령으로 Shared Memery 1000 bytes를 생성한다
$ snchoi@snchoi:~$ ipcmk -M 1000
Shared memory id: 5
$ snchoi@snchoi:~$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x8cfc5454 4 snchoi 644 1000 0
0x7ed23259 5 snchoi 644 1000 0
------ Semaphore Arrays --------
key semid owner perms nsems
# (격리)
$ snchoi@snchoi:~$ sudo unshare --ipc sh
/ $ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
2.9. 제어 그룹 이름공간
- croups 파일 시스템에 chroot를 적용하는 것과 다소 비슷하다.
- 프로세스가 전체 cgroups 파일 시스템 위계구조에서 자신의 제어 그룹들에 관한 디렉터리 위쪽은 볼 수 없게 만든다.
# (호스트)
$ snchoi@snchoi:~$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-fb34540f-3917-4fe8-afe9-389f4e277d98.scope
# (격리)
$ snchoi@snchoi:~$ sudo unshare --cgroup bash
root@snchoi:/home/snchoi$ cat /proc/self/cgroup
0::/
3. 호스트의 관점에서 본 컨테이너 프로세스
- 컨테이너는 컨테이너화된 프로세스이다.
- 컨테이너는 여전히 호스트 컴퓨터에서 실행되는 하나의 리눅스 프로세스이지만, 호스트 컴퓨터의 일부만 볼 수 있고 전체 파일 시스템의 한 부분 트리에만 접근할 수 있다.
- 컨테이너는 하나의 프로세스일 뿐이므로 호스트 운영체제의 문맥 안에 존재하며, 호스트의 커널을 다른 프로세스들과 공유한다.
- 호스트에서 컨테이너 프로세스들을 볼 수 있다는 것은 컨테이너와 VM의 근본적인 차이점 중 하나이다.
- 호스트에 접근한 공격자는 호스트에서 실행되는 모든 컨테이너를 인식하고 영향을 미칠 수 있다.
4. 컨테이너 전용 호스트
- 컨테이너는 호스트와 다른 컨테이너들과 하나의 커널을 공유한다.
- 호스트가 침해되면 호스트의 모든 컨테이너가 잠재적 피해자가 된다.
- 컨테이너 이외의 응용 프로그램을 전혀 실행하지 않는다면 호스트 컴퓨터에는 최소한의 사용자 계정들만 있으면 된다.
- 호스트를 불변 객체로 취급하면 침입을 탐지하기 쉽다.
'리눅스(Linux) > 컨테이너(Container)' 카테고리의 다른 글
[컨테이너] Container Layer 구조 (OverlayFS, 컨테이너 이미지 구조) (0) | 2023.08.15 |
---|---|
VM과 컨테이너 (0) | 2023.07.29 |
[Docker] docker inspect (0) | 2021.09.11 |
[도커] 스웜모드 & 스웜모드의서비스 장애 복구 & 서비스 컨테이너에 설정 정보 전달(secret,config) & 도커 스웜 네트워크 & 서비스 디스커버리 (0) | 2020.09.19 |
[도커] 쉘스크립트에 도커 명령어 작성 & Docker compose & swarm (0) | 2020.09.16 |