0. 개요
이 글은 아래 글과 이어집니다.
https://ksb-dev.tistory.com/377
chroot와 pivot root는 전용루트시스템입니다.
하지만, 아래와 같은 문제점이 있습니다.
- 컨테이너에서 host의 다른 컨테이너가 보임
- 컨테이너에서 host 포트 사용
- 컨테이너에 루트 권한 존재
pivot root에서 mount namespace가 처음 사용 되면서 네임스페이스가 등장했습니다.
네임스페이스는 프로세스에 격리된 환경을 제공합니다.
또한, 모든 프로세스는 타입별로 네임스페이스에 존재하며, 자식 프로세스는 부모의 네임스페이스에 속하는 특징이 있습니다.
2002년 mount namespace가 등장한 뒤 현재 다양한 네임스페이스가 존재합니다.
각 년도별로 등장한 네임스페이스를 정리하면 아래와 같습니다.
연도
|
네임스페이스
|
unshare 옵션
|
2002
|
MOUNT
|
-m, —mount
|
2006
|
UTS, IPC
|
-u, —uts | -i, —ipc
|
2008
|
PID
|
-p, —pid
|
2009
|
NET
|
-n, —net
|
2012
|
USER
|
-U, —user
|
💡 참고로 2008년에 cgroup이 릴리즈 되었으며, 2013년에 docker가 릴리즈 되었습니다.
1. MOUNT Namespace
마운트 네임스페이스 격리할 때 사용합니다.
unshare -m
lsns -p $$
💡 unshare의 옵션 뒤에 아무것도 안붙이면 ‘/bin/sh’로 동작함
2. UTS Namespace
호스트 이름을 바꾸는 네임스페이스로 호스트 이름을 바꿀 때 사용합니다.
unshare -u
lsns -p $$
3. IPC Namespace
IPC(Inter-Process Communication) : 프로세스간 통신 매커니즘
unshare -i
lsns -p $$
4. PID Namespace
4.1 설명
PID(Process ID) 넘버스페이스 격리할 때 사용합니다.
PID 1은 특별한 프로세스입니다.
0번은 커널 프로세스이고, 해당 0번에서 유저의 최상위 1번 프로세스를 생성합니다.
1번은 /sbin/init으로 초기화를 진행합니다.
1번은 자식에게 시그널을 전파하고 좀비 및 고아 프로세스가 있으면 정리하는 역할을 합니다.
부모, 자식 중첩구조로서 부모에서는 자식이 전부 보입니다.
또한, 자식은 본인의 PID 및 부모의 PID 두 개를 가지고 있습니다.
위 그림에서 처럼 자식은 부모의 PID를 fork하고 PID 1로 실행합니다.
시그널 처리 및 좀비, 고아 프로세스 처리는 개발자가 직접 구현해야 동작합니다.
4.2 실습
PID Namespace를 만드는 명령어는 아래와 같습니다.
# -p : pid namesapce
# -f : fork
# --mount-proc : proc 파일시스템 마운트
unshare -fp --mount-proc /bin/sh
위 명령어에 나온 proc란, 커널이 생성하는 메모리 기반의 가상파일 시스템입니다.
ps, mount, unmount 등 proc를 통해 실행되기 때문에 커널이 관리하는 정보를 조회할 수 있습니다.
생성된 네임스페이스와 host의 PID를 비교하겠습니다.
ps -ef | more
각 PID 1이 /bin/sh, /sbin/init인 것을 알 수 있습니다.
host에서 /bin/sh을 조회하겠습니다.
ps -ef | grep /bin/sh
위 그림에서 왼쪽에서 두 번째와 세 번째가 PID를 의미합니다.
두 번째가 현재 PID 이고, 세 번째가 부모 PID입니다.
/bin/sh의 경우 현재 PID가 1509, 부모 PID가 1508입니다.
또, 1508은 unshare로 생성한 프로세스의 PID이기도 합니다.
PID로 더 자세히 비교해보겠습니다.
lsns -t pid -p 1
lsns -t pid -p 1509
두 네임스페이스의 값을 보시면 4026532281로 같은 것을 보실 수 있습니다.
host에서 PID를 kill 해보겠습니다.
kill -SIGKILL 1509
host에서 kill을 하니 자식 프로세스도 자동적으로 kill 되는 것을 확인하실 수 있습니다.
5. NETwork Namespace
5.1 설명
컨테이너의 네트워크를 host의 네트워크와 격리할 때 사용합니다.
네트워크 가상화로 가상 네트워크 장치 및 인터페이스(veth)를 생성해 격리된 네임스페이스 안에서 사용하도록 합니다.
네트워크 네임스페이스 삭제시 가상 네트워크 장치 같이 삭제됩니다.
5.2 실습
서로 다른 두 개의 컨테이너를 생성하여 통신하도록 합니다.
우선 peer 통신할 가상 네트워크 장치를 생성합니다.
ip link add veth0 type veth peer name veth1
ip link
맨 아래 두 개를 보시면 생성된 가상 네트워크 장치를 볼 수 있습니다.
RED와 BLUE network namespace를 만들어 가상 네트워크 장치를 연결해보겠습니다.
ip netns add RED # 컨테이너 생성
ip netns add BLUE
ip link set veth0 netns RED # 가상 네트워크 장치 연결
ip link set veth1 netns BLUE
ip netns exec RED ip link set veth0 up # 동작 시작
ip netns exec BLUE ip link set veth1 up
ip netns exec RED ip addr add 11.11.11.2/24 dev veth0 # ip 할당
ip netns exec BLUE ip addr add 11.11.11.3/24 dev veth1
생성된 network namespace는 아래 위치에서 확인할 수 있습니다.
ls /var/run/netns
터미널 두 개로 각각 네임스페이스에 접속합니다.
# 1번 터미널
nsenter --net=/var/run/netns/RED
# 2번 터미널
root@ubuntu2004:~#
nsenter --net=/var/run/netns/BLUE
각 터미널에서 서로에게 ping으로 통신하면 정상으로 동작합니다.
접속 종료 후 network namespace를 삭제하면 정상적으로 삭제되는 것을 볼 수 있습니다.
ip netns del RED
ip netns del BLUE
ls /var/run/netns
6. USER Namespace
6.1 설명
UID(User ID), GID(Group ID)를 기준으로 격리하는 네임스페이스입니다.
컨테이너의 루트 권한문제를 해결할 수 있습니다.
PID와 마찬가지로 부모, 자식의 중첩 구조입니다.
시스템은 프로세스의 UID, GID와 파일의 UID, GID를 비교하여 사용 여부를 결정합니다.
실습에서는 일반 계정으로 도커와 user namespace를 사용하여 차이점을 알아보겠습니다.
6.2 실습 1 - 도커
먼저 일반계정으로 접속하고, 일반계정에 도커 권한을 할당 후 재접속합니다.
이전 실습으로 루트권한으로 접속되어 있으면 exit로 나오시면 됩니다.
sudo usermod -aG docker vagrant
docker 컨테이너와 host에서 id 명령어로 UID와 GID를 비교하겠습니다.
# docker
docker run -it ubuntu /bin/sh
id
# host
id
docker 컨테이너에서는 root이고, host에서는 일반 계정을 나타내는 1000을 확인할 수 있습니다.
이제 docker 컨테이너서의 root가 host의 root 권한을 가지는지, 일반 계정의 권한을 가지는지 확인해보겠습니다.
일반 계정의 host에서 아래 명령어로 확인하니 host의 root 권한을 가진 것으로 확인되었습니다.
ps -ef | grep "/bin/sh"
inode 값을 확인해보겠습니다.
readlink /proc/$$/ns/user
inode의 값이 같다는 의미는 컨테이너의 UID, GID, 네임스페이스가 host의 UID, 네임스페이스와 같다는 것을 의미합니다.
이는 같은 계정을 의미하므로 컨테이너의 root가 host의 root와 같은 계정임을 의미합니다.
💡 따라서, 일반 계정에 도커 권한을 주는 것은 root 권한을 주는 것을 의미합니다!!
6.3 실습 2 - user namespace
user namespace로 격리해 id 값을 확인하겠습니다.
# namespace
# --map-root-user : UID를 리맵해서 user namespace 생성 -> 리맵은 아래에서 설명!
unshare -U --map-root-user /bin/sh
id
#host
id
네임스페이스 내부는 root이고, host에서는 일반 계정을 나타내는 1000을 확인할 수 있습니다.
프로세스의 상태를 확인하는 ps 명령어를 host에서 사용해보겠습니다.
ps -ef | grep "/bin/sh"
docker container와 달리 일반 계정인 것을 확인할 수 있습니다.
inode 값을 비교해보겠습니다.
readlink /proc/$$/ns/user
inode의 값이 다르니 host와 격리된 환경은 서로 다른 계정임을 알 수 있습니다.
또한, inode의 값은 host나 일반계정이나 같다는 것을 알 수 있습니다.
해당 실습에서 리맵을 사용했습니다.
리맵이란, 부모의 UID를 컨테이너 내부에서 루트로 변경하여 사용할 수 있게 하는 것입니다.
컨테이너 내부에서는 루트처럼 동작하지만, 실제 외부 자원을 사용할 때는 부모의 UID에 따라 권한을 가진다는 것을 의미합니다.
도커 1.10 부터 user namespace 기능을 제공하지만 default는 아닙니다.
도커에서 user namespace를 사용하는 방법은 까다롭습니다.
'삽질' 카테고리의 다른 글
도커 없이 컨테이너 만들기 - 9 (0) | 2024.08.23 |
---|---|
도커 없이 컨테이너 만들기 - 8 (0) | 2024.08.23 |
도커 없이 컨테이너 만들기 - 6 (0) | 2024.08.23 |
도커 없이 컨테이너 만들기 - 5 (0) | 2024.08.23 |
도커 없이 컨테이너 만들기 - 4 (0) | 2024.08.23 |