Docker없이 컨테이너 만들기 Part1
Docker없이 컨테이너 만들기 Part 1: 디렉터리 격리
💡 KANS 스터디
CloudNet에서 주관하는 KANS(Kubernetes Advanced Networking Study)으로 쿠버네티스 네트워킹 스터디입니다. 아래의 글은 스터디의 내용을 기반으로 작성했습니다.스터디에 관심이 있으신 분은 CloudNet Blog를 참고해주세요.
아래의 내용은 Kakao 발표자료와 영상을 기반으로 작성했습니다.
들어가며
컨테이너는 논리적으로 잘 격리된 프로세스이다. 이번 시리즈에서는 컨테이너 상태를 만들기 위해 필요한 필요한 프로세스 격리 기술을 하나씩 알아보고 실습을 진행해보며 컨테이너를 이해한다.
우선 컨테이너가 되기 위해서는 디렉터리 격리, 자원 할당량 제한, 프로세스 격리가 필수적으로 필요하다. 순서대로 루트 디렉터리를 격리하는 방법, 네임스페이스를 통해 프로세스를 격리하는 방법, 자원할당량을 조절하는 방법을 살펴볼 예정이다.
실습 시나리오는 다음과 같다.
- 루트 디렉터리 격리
- chroot 격리 기술
- 탈옥 가능여부 확인
- pivot_root + mount namespace를 통한 격리
- 탈옥 가능여부 확인
- chroot 격리 기술
- 네임스페이스
- Process
- User
- etc
- 자원 격리
- cgroup
- docker 옵션
격리 기술 History
출처: https://speakerdeck.com/kakao/ige-dwaeyo-dokeo-eobsi-keonteineo-mandeulgi?slide=200
루트 디렉터리 격리
디렉터리를 격리하는 방법은 chroot가 있다. 위의 history를 확인해보면 1979년에 나온 오래된 기능이다. 하지만, 탈옥 이슈가 있어 현재는 pivot_root와 Mount namespace 기술을 이용해 작업 환경을 격리한다고 한다.
chroot
아래에서 chroot를 실습을 진행해보며 자세히 알아본다.
루트 디렉터리 격리 확인
- 실습 환경 구성(bin/sh, bin/ls 등 필요한 라이브러리를 사전에 가져온다.)
1
2
3
4
5
6
7
8
9
10
cd /tmp
# 필요한 실행파일 가져오기
mkdir -p myroot/bin
cp /usr/bin/sh myroot/bin/
cp /usr/bin/ls myroot/bin/
# 필요한 라이브러리 가져오기
mkdir -p myroot/{lib64,lib/x86_64-linux-gnu}
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64
- 실습 환경 확인(
ls
,sh
실행파일이 들어와있다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@MyServer:/tmp# tree myroot/
myroot/
├── bin
│ ├── ls
│ └── sh
├── lib
│ └── x86_64-linux-gnu
│ ├── libc.so.6
│ ├── libpcre2-8.so.0
│ └── libselinux.so.1
└── lib64
└── ld-linux-x86-64.so.2
4 directories, 6 files
- chroot 진행. 루트디렉터리 격리 확인
1
2
3
4
5
6
root@MyServer:/tmp# chroot myroot /bin/sh
# ls
bin lib lib64
# cd ../../../
# ls
bin lib lib64
위의 실습을 통해 호스트의 루트 디렉터리로 빠져나가지 못하고 /tmp/myroot로 격리된 것을 확인할 수 있다.
이제 탈옥 실습에 필요한 정보를 넣어둔다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
root@MyServer:/tmp# tree myroot
myroot
├── bin
│ ├── ls
│ ├── mkdir
│ ├── mount
│ ├── ps
│ └── sh
├── lib
│ └── x86_64-linux-gnu
│ ├── libblkid.so.1
│ ├── libc.so.6
│ ├── libcap.so.2
│ ├── libgcrypt.so.20
│ ├── libgpg-error.so.0
│ ├── liblzma.so.5
│ ├── libmount.so.1
│ ├── libpcre2-8.so.0
│ ├── libprocps.so.8
│ ├── libselinux.so.1
│ ├── libsystemd.so.0
│ └── libzstd.so.1
├── lib64
│ └── ld-linux-x86-64.so.2
└── usr
└── lib
└── x86_64-linux-gnu
└── liblz4.so.1
7 directories, 19 files
- ps 명령어는 /proc 가 마운트되어야 한다.(위에서
/proc
디렉터리를 통해 커널이 실시간으로 프로세스 정보를 업데이트한다는 것을 확인할 수 있다.)
1
2
3
4
root@MyServer:/tmp# chroot myroot /bin/sh
# ps
Error, do this: mount -t proc proc /proc
- 마운트를 진행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# mount -t proc proc /proc
mount: /proc: mount point does not exist.
# mkdir /proc
# mount -t proc proc /proc
# ps
PID TTY TIME CMD
6263 ? 00:00:00 sudo
6264 ? 00:00:00 su
6265 ? 00:00:00 bash
6360 ? 00:00:00 sh
6365 ? 00:00:00 ps
# ps auf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
1000 6054 0.0 0.3 9924 5888 ? Ss 16:11 0:00 bash --rcfile /dev/fd/63
0 6262 0.0 0.2 11904 5632 ? S+ 16:12 0:00 \_ sudo su -
0 6263 0.0 0.1 11904 2428 ? Ss 16:12 0:00 \_ sudo su -
0 6264 0.0 0.2 10636 4736 ? S 16:12 0:00 \_ su -
0 6265 0.0 0.2 9228 5376 ? S 16:12 0:00 \_ -bash
0 6360 0.0 0.0 2892 1664 ? S 16:17 0:00 \_ /bin/sh
0 6366 0.0 0.1 7064 3072 ? R+ 16:17 0:00 \_ ps auf
1000 5859 0.0 0.3 9924 5888 ? Ss+ 16:11 0:00 bash --rcfile /dev/fd/63
0 571 0.0 0.1 6176 2048 ? Ss+ 15:34 0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux
0 557 0.0 0.1 6220 2304 ? Ss+ 15:34 0:00 /sbin/agetty -o -p -- \u --keep-baud 115200,576
탈옥 확인
탈옥 코드를 통해 탈옥을 진행한다.
- 탈옥 코드
코드에 대해 간단하게 설명하면, “.out” 디렉터리를 만들고, 해당 폴더를 기준으로 chroot를 실행한다.
경로를 최상위(../../../../../
)로 바꾸고, 해당 경로를 기준으로 chroot를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
- 컴파일
1
gcc -o myroot/escape_chroot escape_chroot.c
- 탈옥코드가 있는 상태에서 chroot를 실행한다.
1
2
3
4
5
6
7
8
tree -L 1 myroot
myroot
├── bin
├── escape_chroot
├── lib
├── lib64
├── proc
└── usr
- 아래와 같이 탈옥이 가능하다.
탈옥 코드를 실행시킨 뒤 ls를 보면 호스트의 최상위 루트 디렉터리를 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
sudo chroot myroot /bin/sh
# ls
bin escape_chroot lib lib64 proc usr
# cd ../../../
# ls
bin escape_chroot lib lib64 proc usr
# ./escape_chroot
# ls
bin dev home lib32 libx32 media opt root sbin srv tmp var
boot etc lib lib64 lost+found mnt proc run snap sys usr
이렇듯 탈옥 이슈로 인해 컨테이너에서는 chroot를 사용하지 않고, 아래의 pivot_root + mount namespace
방법을 사용한다.
Pivot_root + mount namespace(host 영향 격리)
Pivot_root 명령어를 통해, 루트 파일시스템과 마운트 파일시스템의 논리적인 위치를 바꿀 수 있다.
Pivot_root는 실제로 root와 mount 파일시스템의 위치를 바꾸는 개념으로 가상으로만 작동하는 chroot와 비교된다. 당연하겠지만, 실제 파일시스템 위치를 변경하므로 호스트에도 영향이 간다.
네임스페이스를 통해 마운트 환경을 격리하여 호스트에 영향도를 제거할 수 있다.
- 마운트 네임스페이스를 진행하여, 부모 프로세스의 마운트 내용을 복사한다.
- 자식 네임스페이스 안에서 컨테이너 파일시스템을 마운트한다.
- 자식 네임스페이스 안에서 pivot_root를 진행하면, 루트 디렉터리가 변경된다.
- pivot root는 가상으로 변경하는 게 아니기에 탈옥이 불가능하다.
이제 실습을 진행해보자. 실습에 사용할 명령어를 미리 살펴보자.
unshare
명령어를 통해 격리를 진행할 수 있다. ex)unshare --mount /bin/sh
pivot_root
new-root와 old-root가 pivot된다. ex) pivot_root [new-root] [old-root]
실습
- 별도의 mount를 하나 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir new_root
mount -t tmpfs none new_root
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 29G 3.1G 26G 11% /
tmpfs 951M 0 951M 0% /dev/shm
efivarfs 128K 3.6K 120K 3% /sys/firmware/efi/efivars
tmpfs 381M 884K 380M 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 191M 4.0K 191M 1% /run/user/1000
/dev/nvme0n1p15 105M 6.1M 99M 6% /boot/efi
none 951M 0 951M 0% /new_root
- new_root/put_old를 생성한 뒤, pivot을 진행한다.
1
$ pivot_root . put_old
파일시스템이 pivot된 것을 확인할 수 있다.
1
2
3
4
5
6
# cd /
# ls
bin escape_chroot lib lib64 proc put_old
# ls put_old
bin dev home lib32 libx32 media new_root proc run snap sys usr
boot etc lib lib64 lost+found mnt opt root sbin srv tmp var
- 탈옥 테스트
가상이 아닌 실제 파일시스템이 pivot이 된 것이기에, 탈옥에 실패하는 것을 확인할 수 있다.
1
2
3
4
5
# ./escape_chroot
# cd ../../../
# ls /
bin escape_chroot lib lib64 proc put_old
# exit
마치며
루트 디렉터리를 격리하는 방법으로 사용된 방법을 알아봤다. 왜 chroot는 격리기술로 적합하지 않은지 탈옥 실습을 진행해보며 알 수 있었다. 이제 다음 편에서는 리눅스 네임스페이스를 살펴볼 예정이다.