블로그

모든 테넌트가 자체 VM을 가져야 할까요?

테넌트별 컨테이너, VM, 하이브리드 구성 중에서 위험 기반 의사 결정 프레임워크와 각 옵션을 방어 가능하게 만드는 강화 단계를 통해 선택하세요.

요약

멀티 테넌트 호스팅은 테넌트 간에 얼마나 많은 접근을 허용할지 선택하게 합니다. 컨테이너는 Linux 네임스페이스와 cgroups를 사용하여 프로세스와 리소스를 격리하지만 호스트 커널을 공유합니다. 가상 머신은 속도와 운영 부하를 대가로 하드웨어 수준의 경계를 추가합니다. 하이브리드 접근 방식(VM 내부의 컨테이너)은 둘 다 제공할 수 있지만 패치해야 할 표면이 두 배가 됩니다. 이 글에서는 위험 기반 의사 결정, 나란히 비교, 그리고 VM 내부에서도 중요한 Docker 강화 단계를 안내합니다. 결국 어떤 격리 모델이 테넌트에 적합한지, 출시 전에 무엇을 구성해야 하는지 알게 됩니다.

멀티 테넌트 앱이 거의 준비되었습니다. 고객별로 스택을 생성하는 Docker Compose 파일이 있고 매우 빠릅니다. 그런데 호스팅 회사를 운영하는 친구가 '각 테넌트에게 자체 VM을 할당하고 있나요?'라고 묻습니다. 당신은 얼어붙습니다. 그 질문에 대비하지 않았기 때문입니다. 이 글은 보안 팀 없이도 오늘 그 질문에 답할 수 있는 방법을 제공합니다. 혼자서 이 일을 하기 때문에 결정은 새벽 2시에도 방어할 수 있을 만큼 단순해야 합니다.

'최고'의 모델을 찾으려고 하지 마세요. 테넌트의 코드가 호스트를 장악하면 어떤 일이 발생하는지 기록하는 것부터 시작하세요. 어떤 도구를 선택하기 전에 폭발 반경(blast radius)을 정의하세요. 그 연습은 어떤 벤치마크보다 더 많은 것을 알려줄 것입니다.

커널은 쫓아낼 수 없는 룸메이트

컨테이너는 호스트 커널을 공유하기 때문에 효율적입니다. 그 공유가 바로 전체 비결이자 전체 위험입니다. Linux 네임스페이스는 각 컨테이너에 프로세스, 네트워킹, 파일시스템에 대한 자체 뷰를 제공합니다. 제어 그룹(cgroups)은 CPU, 메모리, 디스크 I/O를 제한하여 한 테넌트가 다른 테넌트를 굶기지 않게 합니다. 하지만 둘 다 하드웨어 벽을 만들지 않습니다.

컨테이너를 아주 그럴듯한 가짜 신분증을 가진 프로세스로 생각하세요. 그것은 자신이 독립된 머신에 있다고 믿습니다. 그러나 커널은 호스트에서 실행되는 단일 Linux 사본입니다. 테넌트가 커널 취약점을 악용하면 네임스페이스는 그저 메타데이터일 뿐이 됩니다. 커널 함수를 호출할 수 있는 공격자는 동일한 커널의 다른 네임스페이스에 도달할 수 있습니다. 그것이 바로 여러분이 계속 듣는 컨테이너 탈출(container escape)입니다.

클라이언트당 하나의 컨테이너로 작은 B2B 도구를 호스팅한다고 가정해 보세요. 한 클라이언트가 원격 코드 실행 버그가 있는 수상한 플러그인을 설치합니다. 기본 Docker 설정에서 그 프로세스는 컨테이너 내부에서 root로 실행됩니다. 컨테이너의 root는 여전히 UID 0이며, 명시적으로 사용자를 매핑하지 않는 한 커널은 그 UID를 호스트 root와 구분하지 않습니다. 공격자는 탈출을 시도할 수 있으며 공유 커널이 그들의 표적이 됩니다.

실패는 극적일 필요가 없습니다. 단일 테넌트의 메모리 누수는 호스트를 스왑으로 밀어 넣어 다른 모든 테넌트를 느리게 만들 수 있습니다. cgroup 제한이 없으면 하나의 오작동 루프는 가용성 공격입니다. 제한이 있으면 프로세스가 차단되고 알림이 발생합니다.

이것이 컨테이너가 안전하지 않다는 뜻인가요? 아닙니다. 커널을 공유 신뢰 영역으로 취급해야 한다는 뜻입니다. 선택하기 전에 한 문단 분량의 위험 성명서를 작성하세요: '테넌트의 컨테이너가 손상되면 공격자가 접근할 수 있는 것: [목록]. 비즈니스 비용은: [금액 또는 영향].' 그 문단이 두렵게 만든다면 당신은 편집증이 아니라 정직한 것입니다.

공유 컨테이너에서 완전히 분리된 스택까지 격리 스펙트럼을 더 자세히 보려면 멀티 테넌트 Docker 아키텍처 설계 가이드를 참조하세요.

세 가지 분할 방법 (배포 전에 하나 선택)

멀티 테넌트 격리를 위한 아키텍처는 실제로 세 가지입니다. 모든 '모범 사례'는 이들의 조합입니다.

접근 방식격리 장벽가장 적합한 경우가장 어려운 문제
테넌트별 컨테이너커널 네임스페이스 + cgroups테넌트가 많고 작으며 테넌트당 위험이 낮고 밀도가 필요할 때하나의 커널 익스플로잇이 해당 호스트의 모든 테넌트를 무너뜨릴 수 있음
테넌트당 VM 하나하이퍼바이저/하드웨어 가상화규제 대상 데이터, 적대적 테넌트, 테넌트당 높은 가치더 무겁고 프로비저닝이 느리며 테넌트별로 OS를 패치해야 함
VM 내부의 컨테이너컨테이너화된 워크로드 주변의 VM 경계밀도와 그룹 간 단단한 쉘비용과 운영 오버헤드가 거의 두 배

테넌트별 컨테이너. 대부분의 SaaS 창업자에게 기본 선택입니다. 각 테넌트는 자체 컨테이너 또는 소규모 Compose 스택을 받습니다. 프로비저닝은 즉각적이고 이미지는 작으며 CI/CD는 간단합니다. 리소스 제한은 시끄러운 이웃이 서버를 잡아먹지 못하게 합니다. 트레이드오프는 공유 커널입니다. 워크로드를 비권한으로 유지하고 호스트를 정기적으로 패치할 수 있다면 이것이 종종 올바른 첫 단계입니다.

두 테넌트를 같은 컨테이너에 넣지 마세요. 그것은 공유 커널 + 공유 런타임 + 공유 파일시스템입니다. 한 테넌트가 프로세스를 생성하는 파일을 업로드하면 다른 테넌트는 이미 같은 프로세스 테이블에 있습니다. 컨테이너는 격리의 단위입니다. 컨테이너당 하나의 테넌트로 만드세요.

데이터베이스는 어떨까요? 모든 테넌트가 동일한 자격 증명으로 하나의 MongoDB 또는 PostgreSQL 인스턴스에 연결한다면 이미 거대한 공유 구성 요소를 추가한 것입니다. 각 테넌트에게 별도의 자격 증명을 제공하고 이상적으로는 별도의 데이터베이스나 스키마를 제공하세요. 컨테이너는 앱을 격리합니다. 데이터베이스는 공격자가 가장 먼저 테스트할 누출 지점인 경우가 많습니다.

테넌트당 하나의 VM. 각 테넌트에게 전체 가상 머신을 제공하세요. 하이퍼바이저는 하드웨어 수준의 경계를 추가하며, 이는 커널 익스플로잇이 호스트에 도달하기 위해 반드시 넘어야 하는 경계입니다. 이는 규제 환경이나 테넌트를 신뢰할 수 없을 때 중요합니다. 비용은 밀도와 시간입니다. 이제 컨테이너뿐만 아니라 운영 체제 군단을 관리하게 됩니다. 각 VM에는 업데이트, 보안 에이전트, 모니터링이 필요합니다. 솔로 창업자에게는 실제 작업입니다.

이 계층에서 작동하는 패턴: 동일한 기본 이미지에서 VM을 생성하기 위해 코드형 인프라를 사용하고, 라이브 시스템을 패치하는 대신 새 이미지에 업데이트를 포함하며, 인식하지 못하는 워크로드를 종료하세요. VM의 관리 포트는 인터넷에 닫아 두세요.

VM 내부의 컨테이너. 이 하이브리드는 초보자 튜토리얼에서 거의 논의되지 않습니다. 각 테넌트(또는 소규모 테넌트 그룹) 주위에 작은 VM을 두고 그 VM 내부에서 컨테이너를 실행합니다. VM은 폭발 반경을 담는 용기이고, 컨테이너는 단지 배포 가능한 단위입니다. 이를 통해 가상화의 확실한 경계와 이미지의 재현성을 얻을 수 있습니다. 가상화 오버헤드와 컨테이너 유연성 비용을 지불해야 하므로 비용이 더 들지만, 테넌트를 완전히 신뢰할 수 없을 때 가장 합리적인 장기 모델이 될 수 있습니다.

일반적인 미세 예: 테넌트가 Node API와 백그라운드 워커를 실행합니다. 두 프로세스를 모두 포함하는 하나의 거대한 컨테이너 대신 하나의 VM을 사용한 다음, 서로 다른 리소스 제한, 공유 네트워크, 워커에 대한 직접 인터넷 노출이 없는 두 개의 컨테이너를 사용하세요. VM은 확실한 경계를 제공하고 컨테이너는 구조를 제공합니다.

어떤 것을 선택해야 할까요? 표가 후보 목록입니다. 다음 섹션에서 결정을 구체화합니다.

컨테이너를 선택했다면 이 여섯 가지를 하지 않으면 시작하지 마세요

모든 컨테이너를 잠재적 공격자로 취급한다면 테넌트별 컨테이너는 괜찮습니다. 그것은 희망적인 생각이 아니라 구성에서 시작됩니다.

0. 누구도 신뢰하기 전에 리소스를 제한하세요. cgroups는 공정성 메커니즘이자 가용성 방어입니다. 컨테이너별로 --memory--cpus를 설정하세요. 메모리를 누수하는 테넌트는 서버의 한계가 아닌 자체 한계에 도달해야 합니다. 이것은 보안 경계는 아니지만, 시끄러운 이웃은 단 한 줄의 코드 없이도 공격이 됩니다. 실용적인 시작: --memory 512m --cpus 0.5. 워커 프로세스의 경우 더 낮게 시작하고 확장하세요.

1. 비루트 사용자로 실행하세요. 절대적으로 필요한 경우가 아니면 컨테이너 프로세스가 UID 0을 사용하지 못하게 하세요. Dockerfile에서 사용자를 설정하고 --user를 추가 가드로 전달하세요. 비권한 사용자로 실행되는 익스플로잇은 커널에 도달하는 경로가 훨씬 적습니다. Dockerfile에서 사용자를 생성하세요: RUN useradd -u 10001 appUSER app. 시간을 아끼기 위해 이를 건너뛰지 마세요.

2. 필요하지 않은 모든 capability를 제거하세요. Linux capability는 root의 권한을 작은 조각으로 나눕니다. 대부분의 웹 앱은 거의 필요로 하지 않습니다. --cap-drop=ALL로 시작하고 필요하다고 아는 것만 다시 추가하세요. CAP_SYS_ADMIN이 없는 컨테이너는 네임스페이스 트릭에 사용하기 훨씬 어렵습니다. 앱이 권한 있는 포트에 바인딩하려 하면 NET_BIND_SERVICE를 부여하는 대신 높은 포트에서 실행하고 앞에 프록시를 두세요.

3. 파일시스템을 읽기 전용으로 만드세요. 앱은 자체 컨테이너 레이어에 쓰면 안 됩니다. 상태를 위해 tmpfs를 마운트하세요. 디스크에 쓸 수 없는 공격자는 지속성을 심는 것이 훨씬 어렵습니다. 웹셸을 쓰려는 손상된 PHP 앱은 루트 파일시스템이 읽기 전용일 때 실패합니다. 앱이 실제로 필요로 하는 쓰기 가능한 디렉토리를 위해 명명된 볼륨을 마운트할 수 있습니다.

4. seccomp와 AppArmor 또는 SELinux를 적용하세요. 이들은 위험한 시스템 호출을 폐기 더미로 보냅니다. Docker에는 기본 seccomp 프로필이 포함되어 있습니다. 사용하세요. 추가 레이어를 위해 AppArmor 프로필을 추가하세요. 모든 시스템 호출을 마스터할 필요는 없습니다. 일반적인 웹 워커가 절대 필요로 하지 않는 것을 거부하면 됩니다. 절대 --privileged로 실행하지 마세요. 그 플래그는 방금 설정한 거의 모든 방어를 비활성화합니다.

5. 네트워크를 분할하세요. 모든 컨테이너에 다른 모든 컨테이너로의 경로를 제공하지 마세요. 기본 거부, 그런 다음 필요한 포트만 여세요. 손상된 데이터베이스 컨테이너는 관리자 패널을 스캔할 수 없어야 합니다. 테넌트가 별도의 네트워크에 있으면 한 네트워크의 침해는 측면으로 확산될 수 없습니다.

실용적인 시작:

docker run --user 10001 --cap-drop=ALL --security-opt no-new-privileges --read-only --tmpfs /tmp:rw,size=64M --security-opt seccomp=default.json --memory 512m --cpus 0.5 myimage

동일한 플래그를 Compose 파일에 넣고 모든 테넌트에 적용하세요. 이것은 완전하지 않지만 docker run이 기본 제공하는 것보다 훨씬 강력한 기본값입니다.

더 자세한 안내는 멀티 테넌트 호스팅에서 Docker 컨테이너를 위한 단계별 강화 가이드를 사용하세요.

알아야 할 예외: Docker의 Enhanced Container Isolation

관리형 Docker 환경에서 실행한다면 Docker의 Enhanced Container Isolation(ECI)을 찾아보세요. 내부적으로 사용자 네임스페이스 격리와 보안 컨테이너 런타임을 사용합니다. 컨테이너 내부의 root는 호스트의 비권한 사용자로 매핑되므로 root로 실행되는 컨테이너도 호스트 root 권한을 얻지 못합니다. 또한 기본적으로 위험한 capability와 시스템 호출을 차단합니다. 이것은 바닐라 Docker에서 몇 가지 플래그로 재현할 수 있는 것이 아닙니다. 플랫폼이 지원한다면 활성화하세요. 비루트 사용자와 리소스 제한의 필요성을 없애지는 않지만 위험 계산을 바꿉니다.

Docker 데몬의 사용자 네임스페이스 재매핑(userns-remap)으로 이 중 일부를 근사할 수 있습니다. 보안 런타임만큼 완전하지는 않지만 없는 것보다 낫습니다. 사용한다면 신뢰하기 전에 UID 매핑이 작동하는지 확인하세요.

VM 오류: 가상 머신으로 이동하는 것은 강화가 아니다

여기 반대 의견이 있습니다. 대부분의 사람들이 건너뛰는 부분입니다. 테넌트당 하나의 VM으로 이동한 다음 그 안에 일반 컨테이너를 배포한다면 컨테이너 보안 문제를 제거한 것이 아닙니다. 넓은 우리를 추가한 것입니다. 컨테이너 탈출은 여전히 작동합니다. 공격자는 호스트 대신 VM에 도달할 뿐입니다. 그것은 실질적인 개선이지만 여전히 여섯 단계가 필요합니다.

또 다른 함정은 VM 자체가 안전하다고 가정하는 것입니다. 약한 SSH 비밀번호, 패치되지 않은 기본 패키지, 또는 열린 관리 포트가 있는 기본 이미지는 선물입니다. 하이퍼바이저 경계는 게스트가 강화되고 업데이트된 경우에만 중요합니다. 그렇지 않으면 '안전한 VM'은 안전하다고 느끼고 확인을 중단하기 때문에 더 빠른 손상 경로가 됩니다.

VM이 제공하는 것은 줄일 수 있는 폭발 반경입니다. 한 테넌트의 재난은 하나의 VM에 머물러 있습니다. 그 대가는 당신의 시간입니다. 테넌트 수만큼 운영 체제의 시스템 관리자가 됩니다. 제품을 출시하는 솔로 창업자라면 시스템 군단을 패치하고 모니터링할 시간이 있는지 물어보세요. 그렇다면 테넌트당 VM이 올바른 선택일 수 있습니다. 아니라면 강력한 강화를 갖춘 컨테이너가 더 정직할 수 있습니다.

또한 하이퍼바이저 호스트가 중요한 표적임을 기억하세요. 손상된 하이퍼바이저는 모든 게스트를 볼 수 있습니다. 게스트뿐만 아니라 호스트도 패치하세요. VM은 호스트 패치를 면제하지 않습니다. 오히려 놓쳤을 때의 위험을 높입니다.

하이브리드에 대한 경고: VM 내부의 컨테이너가 '두 겹의 보안'을 공짜로 제공한다고 가정하지 마세요. VM은 경계를 추가합니다. 컨테이너는 여전히 비루트, capability, seccomp가 필요합니다. 그렇지 않으면 첫 번째 레이어는 가장 약한 컨테이너만큼만 강합니다.

10분 안에 논쟁을 끝내는 네 가지 질문

추상적으로 최적화하지 마세요. 다음 네 가지 질문을 순서대로 스스로에게 물어보고 답을 적으세요.

1. 내 테넌트가 무엇에 접근할 수 있나요? 테넌트가 자신의 웹 앱과 데이터베이스에만 접근할 수 있다면 엄격한 네트워크 규칙을 가진 테넌트별 컨테이너는 방어 가능합니다. 테넌트의 데이터가 규제 대상이거나 금융적으로 민감하다면 VM으로 이동하세요.

2. 한 테넌트의 손상이 나에게 얼마나 큰 비용이 될까요? 잃은 고객, 법적 노출, 신뢰를 합산하세요. 그 숫자가 VM 운영 비용보다 크다면 돈을 쓰세요. 그렇지 않다면 컨테이너가 합리적인 선택입니다.

3. 테넌트가 몇 명이고 얼마나 지불하나요? 소규모 구독자가 많다면 컨테이너 밀도가 중요합니다. 대규모 계정이 몇 개라면 각각 VM을 제공하고 그에 따라 청구하세요. 커피 한 잔 값도 안 되는 테넌트가 각각 관리할 OS를 요구해서는 안 됩니다.

4. 정기적으로 패치할 수 있나요? 컨테이너는 하나의 호스트 커널을 공유하므로 호스트를 패치하면 모두를 보호합니다. VM은 패치 대상을 늘립니다. 업데이트를 건너뛸 것이라는 것을 안다면 움직이는 부품이 적고 기본값이 더 강력한 아키텍처를 선택하세요.

답변은 특정 패턴으로 모일 것입니다. VM 중심 답변이 두 개 이상이면 테넌트별 컨테이너를 기본으로 선택하지 않아야 합니다. 컨테이너 중심 답변이 세 개 이상이면 VM은 성급합니다. 직관에 반하는 결과: 민감한 데이터에 접근할 수 있는 저수익 테넌트는 여전히 VM이 필요합니다. 규제 비용은 그들이 지불하는 금액과 아무 관련이 없기 때문입니다.

신뢰할 수 있는 최소한을 출시한 다음 더 많은 격리를 확보하세요

첫 번째 아키텍처가 최종 아키텍처일 필요는 없습니다. 실제로 유지할 수 있는 가장 타이트한 설정으로 시작한 다음 테넌트 기반이 정당화할 때 격리를 추가하세요. 대부분의 솔로 운영자에게는 비루트, 제한된 capability, 읽기 전용 파일시스템, seccomp, 네트워크 분할을 갖춘 테넌트별 컨테이너를 의미합니다. 규제 대상이거나 가치가 높은 테넌트의 경우 컨테이너를 내부 패키징 레이어로만 사용하여 테넌트당 하나의 VM으로 바로 이동하세요.

무엇을 선택하든 결정을 기록하고 분기별로 재검토하세요. '이 테넌트를 VM으로 옮겨야 할까요?'라는 첫 번째 질문을 받으면 답이 있을 것이고 그것을 뒷받침할 체크리스트가 있을 것입니다. 그것이 격리의 실제 의미입니다: 사는 기술이 아니라 관리하는 트레이드오프입니다.

출시 전에 실용적인 Docker 격리 보안 체크리스트를 살펴보세요. 이 체크리스트는 이러한 결정을 고객에게 페이지를 보여주기 전에 검증할 수 있는 목록으로 바꿔줍니다.

Sources (5)