리눅스 컨테이너 (2) - chroot

chroot

 

이전에 리눅스 컨테이너 (1)편에서는 여러가지 프로세스 격리 기능들에 대해 간단히 살펴보았습니다. 이번 글에서는 격리 기능들 중 chroot의 동작 원리를 들여다보고, 실제로 어떻게 사용하는지 실습을 통해 알아보겠습니다.

 

chroot를 실습하려면 리눅스 환경이 필요한데, 저는 이를 위해 Multipass를 사용했습니다. Multipass는 Ubuntu를 개발한 Canonical에서 제공하는 경량 가상 머신 관리자로, 로컬 환경에서 빠르고 간편하게 Ubuntu 인스턴스를 실행할 수 있도록 도와주는 도구입니다. 개발 및 테스트용 리눅스 환경을 구축할 때 매우 유용합니다.

 

동작 원리

 

chroot는 Change Root Directory 의 줄임말로 루트 디렉터리를 바꾼다는 뜻을 갖고 있습니다. 먼저 루트 디렉터리를 바꾼다는 의미에 대해 알아보겠습니다. 

 

루트 디렉터리란 파일 시스템의 최상위 디렉터리를 의미하며 컴퓨터에서 모든 파일과 디렉터리는 루트 디렉터리를 기준으로 계층적으로 구성됩니다. 리눅스 운영체제에서는 / 라고 표현하고, 윈도우 운영체제에서는 C:\ 와 같이 표현합니다.

 

 

위와 같은 파일 구조를 갖는 시스템 위에서 어떤 프로세스 H를 실행했다고 생각해보겠습니다. 이 프로세스 H는 기본적으로 루트 디렉터리( / )를 기준으로 다른 파일들을 탐색할 수 있습니다. 만약 chroot-env 라는 폴더에 접근하고자 한다면 /home/user/chroot-env 와 같이 접근할 수 있습니다.

 

chroot는 바로 이 루트 디렉터리를 다른 위치로 지정해서 프로세스를 실행할 수 있는 명령어입니다. 아래 그림처럼 chroot-env 폴더를 루트 디렉터리로하는 프로세스 A가 있다고 생각해보겠습니다. 

 

chroot를 사용해 실행된 프로세스 A는 chroot-env가 새로운 루트 디렉터리( / )가 되기 때문에 chroot-env를 기준으로 다른 파일들을 탐색하게 됩니다. 예를 들어 프로세스 H에서는 /home/user/chroot-env/bin으로 접근했다면 프로세스 A에서는 /bin으로 접근하게 됩니다.

또한 프로세스 A는 chroot-env가 최상위 디렉터리이기 때문에 그 위에 있는 /home, /home/user 와 같은 경로에는 접근할 수 없습니다.

 

이처럼 루트 디렉터리를 변경하여 특정 프로세스(A)가 상위 디렉터리에 접근할 수 없도록 격리시키는 역할을 하는 명령어가 chroot입니다.

다만, 루트 권한을 가진 프로세스라면 chroot 환경을 탈출할 수 있는 방법이 존재합니다.

 

 

chroot 실행 1

 

chroot를 사용해보기 앞서 매뉴얼을 먼저 확인해보겠습니다. 리눅스 시스템에서 매뉴얼은 man 명령어를 통해 사용할 수 있습니다.

 

SYNOPSIS에 나온 방법대로 다음과 같이 간단한 명령어를 사용해보겠습니다.

$ chroot / /bin/bash

$ chroot /

 

COMMAND에 아무런 값을 주지 않는다면 기본값으로 /bin/sh -i 가 실행되기 때문에 위의 두 명령어는 같은 명령어입니다.

/bin/bash 는 Linux 및 Unix 계열 시스템에서 사용하는 셸(Shell) 프로그램 중 하나인 Bash의 실행 파일입니다.

 

 

명령어를 쳐보면 위와 같은 에러가 나오는데, 이는 chroot는 루트 권한이 필요하기 때문에 일반 사용자(ubuntu)는 작업을 수행할 수 없다는 뜻입니다. 이를 위해 일시적으로 관리자 권한을 부여할 수 있는 sudo 명령어를 사용해보겠습니다.

$ sudo chroot / /bin/bash

$ sudo chroot /

 

sudo 명령어를 사용하면 위와 같이 chroot 명령어가 정상적으로 실행된 것을 확인해볼 수 있습니다. 이렇게 실행된 chroot 환경은 exit 명령어를 사용해 종료할 수 있습니다.

 

chroot 실행 2

 

chroot 실행 1에서는 루트 디렉터리( / )를 새로운 루트 디렉터리로 지정해 chroot 환경을 시작했기 때문에 결과적으로는 아무런 효과가 없습니다. 이번에는 /tmp/testroot 라는 경로는 새로운 루트 디렉터리로 지정해보겠습니다. 그 전에 다음과 같이 /tmp/testroot 경로를 생성해보겠습니다.

mkdir -p /tmp/testroot

sudo chroot /tmp/testroot

 

그 다음 실행해보면 다음과 같은 /bin/bash 를 찾을 수 없다는 에러가 발생합니다.

 

이는 /bin/bash를 기존 루트 디렉터리가 아닌, 새로 설정한 루트 디렉터리 기준으로 찾기 때문입니다. 즉, /bin/bash가 아니라 /tmp/testroot/bin/bash 경로에 실제 실행 파일이 존재해야 합니다.

 

그러면 /bin/bash를 /tmp/testroot/bin/bash로 복사해보겠습니다.

mkdir -p /tmp/testroot/bin/
cp /bin/bash /tmp/testroot/bin/

 

복사 후 실행해보면 여전히 /bin/bash 를 찾을 수 없다는 에러가 발생합니다.

 

이는 단순히 파일이 없어서가 아니라 라이브러리 의존성이 충족되지 않았기 때문입니다.

 

/bin/bash는 정적 바이너리(static binary)가 아닌 동적 라이브러리를 사용하는 실행 파일입니다. 이 말은, 단순히 bash 파일 하나만 복사한다고 실행이 가능한 것이 아니라, 실행에 필요한 공유 라이브러리(.so) 파일들도 함께 있어야 한다는 뜻입니다.

 

list dynamic dependencies 의 약자로 실행 파일이 동적으로 참조하는 라이브러리(.so)들을 보여주는 ldd 라는 명령어를 사용해 bash가 어떤 라이브러리를 사용하는지 확인해보겠습니다.

ldd /bin/bash

이 중에서 첫 번째 라이브러리인 linux-vdso.so.1 는 Virtual Dynamic Shared Object(VDSO) 라는 커널이 사용자 공간에 동적으로 제공하는 가상 라이브러리로 실제 디스크 상에 존재하는 파일이 아니고 실행 시점에 커널이 메모리에 매핑하여 제공하는 특별한 라이브러리입니다.

 

실제 파일이 아닌 첫 번째 라이브러리를 제외한 나머지 3가지 의존 라이브러리들은 새로운 루트 디렉터리(/tmp/testroot)를 기준으로 복사해주겠습니다.

mkdir -p /tmp/testroot/lib/aarch64-linux-gnu/

cp /lib/aarch64-linux-gnu/libtinfo.so.6 /tmp/testroot/lib/aarch64-linux-gnu/
cp /lib/aarch64-linux-gnu/libc.so.6 /tmp/testroot/lib/aarch64-linux-gnu/
cp /lib/ld-linux-aarch64.so.1 /tmp/testroot/lib/

 

제대로 복사가 되었는지 디렉터리 구조를 트리(tree) 형태로 시각적으로 출력해주는 tree 명령어를 사용해 확인해보겠습니다.

sudo apt install tree

cd /tmp/testroot

tree

 

제대로 복사가 되었으니 다시 chroot를 실행해보면 bash가 잘 실행되는 것을 볼 수 있습니다.

 

마지막으로 작업 중인 디렉터리(현재 디렉터리)의 전체 경로를 출력하는 명령어 pwd(print working directory)를 사용하여 기존 루트 디렉터리와 격리가 제대로 이루어졌는지 확인해보면 다음과 같이 현재 디렉터리가 새로운 루트 디렉터리가 된 것을 확인해볼 수 있습니다.

 

 

마치며

 

이번 글에서는 chroot를 활용한 간단한 격리 환경 구성과 그 원리를 살펴보았습니다. 하지만 chroot는 완전한 보안 격리를 제공하지는 않기 때문에, 실제 컨테이너 환경에서는 더 강력한 격리 메커니즘이 필요합니다. 다음 글에서는 리눅스 컨테이너 기술의 핵심인 namespace에 대해 알아보겠습니다.