컨테이너를 실행할 때 루트 사용자 또는 일반 사용자로 실행하는 것은 보안 및 권한 관리에 중요한 영향을 미칩니다. 이 질문은 간단해 보이지만, 실제로는 깊은 이해가 필요한 주제입니다. 컨테이너 실행 방식은 여러 요인에 따라 달라질 수 있으며, 사용자의 목적과 보안 모델에 따라 다른 선택을 할 수 있습니다. 명확한 이해를 돕기 위해 이 블로그 포스트를 작성했습니다.

위의 질문에 답하기 전에, 컨테이너 엔진(Podman, Docker, CRI-O, containerd 등), 컨테이너 내부 프로세스(apache, postgresql, mysql 등) 또는 컨테이너가 매핑되는 프로세스 ID에 대해 이야기하는 것인지 결정해야 합니다(세 가지 모두 다를 수 있음). 언뜻 보기에는 명확하지 않을 수 있습니다. 컨테이너 엔진 또는 컨테이너의 하위 프로세스는 사실상 모든 사용자로 실행할 수 있습니다.

Containers illustrationRed Hat의 UBI(Universal Base Image)를 활용해서 작업 효율성을 개선하세요

루트 이해

Podman의 등장으로 루트리스(rootless) 컨테이너가 실현 가능해졌습니다. Podman은 컨테이너를 직접적인 하위 프로세스로 생성하기 때문에 네 가지 가능한 옵션을 생각할 수 있습니다. 하지만 루트리스란 무엇일까요? 루트리스를 이해하려면 컨테이너 내부의 루트를 이해해야 합니다. 컨테이너 내부의 루트를 이해하려면 컨테이너 외부의 루트를 이해해야 합니다. 그렇습니다.

다음 표는 컨테이너 내부와 외부의 루트를 보여줍니다(DevConf.us 2019에서 이러한 개념을 명확히 정리한 Vincent Batt에게 감사드립니다). 이 프레임워크를 사용하면 루트리스에 대해 이해하는 데 조금 도움이 될 것입니다.

 

Table 1: Root inside and outside the container showing how users appear to the system

위의 예시에서 몇 가지 흥미로운 점을 짚어 보겠습니다.  먼저 커맨드라인 옵션인 -i(대화형), -t(터미널), -u(사용자)가 있습니다. 이 옵션을 결합하면 컨테이너 내부에서 인터랙티브 터미널을 사용할 수 있으며, 컨테이너화된 프로세스가 사용자 sync로 실행되도록 지정합니다. 'man podman-run' 커맨드를 입력하여 더 자세히 알아볼 수 있습니다. 

둘째, 셸이 일반 사용자로 실행되고 있음을 나타내는 $와, 셸이 루트로 실행되고 있음을 나타내는 #에 주목해야 합니다. 이러한 기호는 컨테이너 내부와 외부의 루트를 설명하는 데 도움이 되는 UNIX 전통입니다. 

셋째, 위의 예시에서 Podman은 정의에 따라 컨테이너 외부에 있으며 루트 또는 일반 사용자(fatherlinux)로 실행되는 반면, 컨테이너 내부에서는 bash가 루트 또는 일반 사용자(sync)로 실행됩니다. 컨테이너 호스트의 /etc/passwd 파일에 있는 사용자는 Podman에 사용되는 반면, 컨테이너 이미지의 /etc/passwd 파일에 있는 사용자는 실행 중인 컨테이너에서 사용됩니다. 다시 말해, 이 두 개의 passwd 파일은 컨테이너 외부와 내부에 하나씩, 두 개의 사용자 집합을 가질 수 있음을 의미합니다. 이것이 바로 사용자 네임스페이스를 가능하게 하는 것입니다.

사용자 네임스페이스

커널이 컨테이너 내부에서 실행 중인 프로세스를 생성할 때 사용자 네임스페이스는 컨테이너화된 프로세스의 사용자 ID를 컨테이너 외부의 다른 사용자 ID에 매핑합니다. 자세히 살펴보겠습니다.

 

Table 2: Showing user IDs and username inside and outside container

커맨드라인 옵션은 -i(인터랙티브), -d(분리), -u(사용자)이며, 이러한 옵션을 결합하여 컨테이너를 백그라운드에서 실행하고 컨테이너화된 프로세스를 사용자 sync로 실행하도록 지정합니다. 이전과 마찬가지로 man podman-run 커맨드를 입력하여 자세히 알아볼 수 있습니다. 

Podman은 또한 컨테이너 호스트의 사용자를 실행 중인 컨테이너의 사용자에 매핑할 수 있는 top이라는 매우 유용한 하위 명령을 제공합니다. 위의 예시에서는 컨테이너를 루트로 실행할 때 컨테이너의 sync 사용자(uid 5)를 기본 컨테이너 호스트의 sync 사용자(uid 5)에 매핑하는 것을 보여줍니다. 즉, 프로세스가 해당 컨테이너에서 탈출한 경우 실제 sync 사용자의 권한으로 실행할 수 있다는 것을 의미합니다. 

반면, 일반 사용자(fatherlinux)와 정확히 동일한 컨테이너를 실행하면 실행 중인 컨테이너 내부의 sync 사용자(uid 5)를 기본 컨테이너 호스트의 uid 100004에 매핑합니다. 잠깐! sync 사용자(uid 5)를 fatherlinux(uid 1000)에 매핑하지 않는 이유는 무엇일까요?

간단히 답하자면, 최신 커널과 최신 shadow-utils 패키지(useradd, passwd )를 사용하는 경우 새로운 각 사용자에게 사용 가능한 다양한 사용자 ID가 할당됩니다. 일반적으로 UNIX 시스템에서는 각 사용자가 하나의 ID만 가지고 있었지만, 이제는 컨테이너 내부에서 사용할 수 있도록 수천 개의 UID를 각 사용자에게 할당할 수 있습니다. 

이는 컨테이너에서 여러 사용자를 사용하는 경우에 유용합니다. 예를 들어, 단일 컨테이너 또는 포드에서 Apache와 MySQL을 함께 실행하거나 다른 사용자로 실행되는 에이전트로 사이드카 컨테이너를 실행하는 경우가 있습니다. 그러나 이 매핑은 어디에서 왔을까요? /etc/subuid/etc/subgid라는 두 파일에서 비롯됩니다. usermod 커맨드를 통해 또는 시스템 관리자가 수동으로 사용자를 추가할 때 이러한 파일에 항목이 생성됩니다.

사용자 식별자에 대한 심층 분석(선택 사항)

다음은 내 시스템에 있는 항목의 예시입니다. 다음 항목을 사용하면 fatherlinux 사용자는 컨테이너에 있는 최대 65,535개의 사용자 ID를 100,000부터 시작하는 시스템의 실제 사용자 ID에 매핑할 수 있습니다. 기본적으로 shadow-utils(useradd, passwd 등)는 이러한 사용자 ID 범위를 하나의 사용자에게만 예약합니다. useradd 커맨드는 다음 사용자를 위해 다음 범위를 예약합니다. 이 예시에서 사용자는 fred이고, 사용자 ID는 165536부터 시작합니다.

cat /etc/subuid fatherlinux:100000:65536 fred:165536:65536 cat /etc/subgid fatherlinux:100000:65536 fred:165536:65536

컨테이너 내부에서 이 맵을 볼 수도 있습니다.

 

Table 3: More on users inside and outside the container

Podman이 루트로 실행되면 컨테이너에서 전체 사용자 ID 범위(4294967295 == 32비트)를 사용할 수 있습니다. 그러나 Podman을 fatherlinux로 실행하면 컨테이너 내부의 루트를 fatherlinux 사용자(1000)에 매핑하고 sync 사용자(uid 5)를 100,000부터 165,535까지의 범위에 있는 UID에 매핑합니다. 

이는 컨테이너 엔진과 실행 중인 컨테이너 내부의 컨테이너화된 프로세스가 권한이 없는 서로 다른 사용자로 실행되기 때문에 뛰어난 보안 기능입니다. 100,000부터 165,535까지의 사용자 ID 집합에는 시스템에 대한 특별한 권한이 없으며, 사용자 fatherlinux(1000)도 마찬가지입니다. 즉, 컨테이너 내의 프로세스가 탈출하면 해당 프로세스가 컨테이너 호스트에서 심각하게 제한됩니다. 

또 다른 질문으로, 여러 사용자를 추가할 때 시스템에 UID가 부족해질 수 있는지를 물어보면 짧은 대답은 '예'입니다. 그러나 UID가 40억 개의 UID가 있는 32비트 숫자로 표시된다는 점을 고려할 때 이는 거의 불가능합니다. 즉, 최대 65,535명의 사용자를 시스템에 추가할 수 있습니다(4294967295를 65535로 나눈 값). 이 정도면 대부분의 활용 사례에 충분합니다.

루트리스 컨테이너의 마지막 뉘앙스를 자세히 살펴보겠습니다. /etc/subuid 파일은 컨테이너 내부의 사용자를 컨테이너 외부의 사용자에게 매핑하는 데 사용되지만, 사용자(아래 예시에서는 fatherlinux)가 컨테이너 이미지에 정의되어 있지 않으면 Podman에서 컨테이너를 시작할 수 없습니다.

podman run --user fatherlinux -it ubi8 bash

결과는 다음과 같습니다.

unable to find user fatherlinux: no matching entries in passwd file

컨테이너 이미지 내의 /etc/passwd 파일에 있는 컨테이너에 사용자 ID를 지정해야 합니다. 이는 컨테이너가 컨테이너 내의 운영 체제에 본질적으로 연결되어 있으며 컨테이너 호스트 운영 체제와 분리되어 있는 방식을 보여주는 또 다른 예입니다. 컨테이너는 Linux입니다.

컨테이너 심층 방어

이 개념은 클라이언트 서버 모델 때문에 docker 데몬과 함께 이해하기가 쉽지 않습니다. docker 클라이언트 서버 모델을 사용하면 일반 사용자로 명령을 실행하는 경우에도 컨테이너를 루트로 실행할 수 있습니다. 이는 docker 데몬이 루트로 실행되기 때문에 루트의 모든 권한을 갖기 때문입니다. 이제 훨씬 더 명확하게 이해할 수 있을 것입니다. 다음 명령을 실행하여 확인해 봅니다.

 

Table 4: Commands to check user IDs

컨테이너를 실행하는 사용자가 호스트에 대한 루트 액세스 권한을 얻지 못하도록 하려면 컨테이너 엔진과 컨테이너화된 프로세스를 루트가 아닌 사용자로 실행해야 합니다. 이렇게 하면 서비스(httpd, MySQL )와 운영 체제의 권한 있는 리소스 간에 여러 계층의 보안이 제공됩니다. 컨테이너 엔진을 루트가 아닌 사용자로 실행하는 것은 하나의 방어 계층이지만, 컨테이너에서 다른 루트가 아닌 사용자로 프로세스를 실행하면 또 다른 방어 계층이 제공됩니다. 

Dan Walsh는 루트가 아닌 사용자로 루트리스 Podman 실행(Running Rootless Podman as a non-root User)라는 글에서 이에 대해 자세히 설명합니다. 상위 수준에서 Podman과 같은 루트리스 컨테이너 엔진을 사용하면 사용자 계정으로 실행할 수 있습니다. 그런 다음 컨테이너 내에서는 컨테이너화된 프로세스에 대한 계정으로만 제어되는 사용자 ID 집합에 매핑된 가상 사용자 집합을 사용할 수 있습니다. 

이제 컨테이너 내부와 외부의 루트 권한에 대해 더 잘 이해하게 되셨기를 바랍니다. 


About the author

At Red Hat, Scott McCarty is Senior Principal Product Manager for RHEL Server, arguably the largest open source software business in the world. Focus areas include cloud, containers, workload expansion, and automation. Working closely with customers, partners, engineering teams, sales, marketing, other product teams, and even in the community, he combines personal experience with customer and partner feedback to enhance and tailor strategic capabilities in Red Hat Enterprise Linux.

McCarty is a social media start-up veteran, an e-commerce old timer, and a weathered government research technologist, with experience across a variety of companies and organizations, from seven person startups to 20,000 employee technology companies. This has culminated in a unique perspective on open source software development, delivery, and maintenance.

Read full bio