container-runtime

3 개의 포스트

Escaping containers using the Dirty Pipe vulnerability | Datadog Security Labs (새 탭에서 열림)

리눅스 커널의 Dirty Pipe(CVE-2022-0847) 취약점은 권한이 없는 프로세스가 읽기 전용 파일에 데이터를 쓸 수 있게 하여, 컨테이너 환경에서 호스트의 권한을 탈취하는 '컨테이너 탈출'을 가능하게 한다. 이 글은 Kubernetes 환경에서 runC 바이너리를 덮어쓰는 방식을 통해, 공격자가 격리된 컨테이너를 벗어나 호스트 수준의 관리자 권한을 획득하는 과정을 상세히 설명한다. 이는 과거 runC 취약점 패치가 성능 최적화를 위해 커널 페이지 캐시를 공유한다는 점을 역이용한 결과로, 현대적 컨테이너 런타임 구조 내의 보안 허점을 시사한다. ### 컨테이너 런타임과 OCI 명세의 이해 * Kubernetes는 컨테이너 실행을 위해 containerd나 CRI-O 같은 고수준 런타임을 사용하며, 이들은 내부적으로 runC와 같은 저수준 OCI(Open Container Interface) 런타임을 호출한다. * runC는 리눅스의 네임스페이스와 제어 그룹(cgroups)을 설정하여 프로세스를 논리적으로 격리하며, 최종적으로 `execve` 시스템 콜을 통해 사용자가 지정한 엔트리포인트를 실행한다. * 컨테이너 프로세스가 생성되는 시점에 `/proc/self/exe` 파일 기술자(File Descriptor)를 통해 호스트의 runC 바이너리에 접근할 수 있는 경로가 일시적으로 열리게 된다. ### runC 취약점의 역사적 맥락 * 과거 CVE-2019-5736 취약점은 컨테이너 내부에서 호스트의 runC 바이너리를 직접 수정하여 루트 권한을 획득하는 방식을 사용했다. * 이를 방어하기 위해 runC 개발팀은 바이너리를 복제(clone)하여 실행하거나, 호스트의 runC 바이너리를 읽기 전용으로 마운트하여 컨테이너 내부에 제공하는 패치를 적용했다. * 하지만 Dirty Pipe 취약점은 커널 페이지 캐시를 조작하여 읽기 전용 파일조차 수정할 수 있게 하므로, 성능 향상을 위해 도입된 '읽기 전용 공유 방식'이 오히려 새로운 공격 경로가 되었다. ### Dirty Pipe를 이용한 컨테이너 탈출 메커니즘 * 공격자는 권한이 없는 컨테이너 내부에서 스크립트를 실행하여 호스트의 runC가 다시 실행되기를 기다린다(예: 관리자의 `kubectl exec` 호출). * runC가 실행되는 순간, 공격 프로세스는 `/proc/<runC-pid>/exe` 경로를 통해 Dirty Pipe 취약점을 가동한다. * 이 취약점은 커널 페이지 캐시 수준에서 메모리를 덮어쓰기 때문에, 호스트의 물리적 디스크에 저장된 runC 파일은 건드리지 않으면서도 현재 실행 중인 runC 프로세스를 악성 바이너리로 교체할 수 있다. ### 공격 증명(PoC) 및 실행 과정 * 공격 스크립트는 루프를 돌며 `ps` 명령어로 `/proc/self/exe`를 참조하는 runC 프로세스의 PID를 지속적으로 감시한다. * 대상 PID가 발견되면 Dirty Pipe 익스플로잇 코드를 실행하여, 해당 프로세스가 참조하는 바이너리 데이터를 호스트 권한으로 실행될 악성 ELF 파일로 덮어쓴다. * 조작된 runC는 호스트 시스템에서 루트 권한으로 실행되며, 공격자가 의도한 명령(예: 호스트의 `/tmp/hacked` 파일 생성 등)을 수행한 뒤 호스트 전체를 장악할 수 있게 한다. ### 보안 결론 및 대응 방안 * 본 취약점은 컨테이너 격리 기술 자체가 아닌 리눅스 커널의 메모리 관리 결함에서 비롯된 것이므로, 가장 확실한 해결책은 Dirty Pipe 보안 패치가 적용된 최신 커널 버전으로 노드를 업데이트하는 것이다. * 컨테이너 환경에서는 `/proc` 파일 시스템에 대한 비정상적인 접근을 모니터링하고, 불필요한 고권한(Privileged) 컨테이너 사용을 지양하는 보안 정책이 병행되어야 한다. * 시스템 재부팅이나 캐시 초기화 시 조작된 페이지 캐시가 사라져 공격 흔적이 휘발될 수 있으므로, 실시간 침입 탐지 시스템을 통한 조기 대응이 중요하다.

Dirty Pipe 취약점을 이용한 컨 (새 탭에서 열림)

리눅스 커널에서 발견된 Dirty Pipe 취약점은 권한이 없는 프로세스가 읽기 권한만 가진 파일에 데이터를 쓸 수 있게 허용하며, 이를 통해 컨테이너 환경에서 호스트 시스템의 루트 권한을 탈취할 수 있는 심각한 위협을 초래합니다. 특히 Kubernetes 환경에서 널리 쓰이는 컨테이너 런타임인 runC의 실행 바이너리를 페이지 캐시 수준에서 변조함으로써, 격리된 컨테이너를 탈출하여 호스트 시스템을 완전히 장악하는 시나리오가 가능합니다. 본 글에서는 이 취약점의 기술적 배경과 함께 실제 컨테이너 탈출이 이루어지는 공격 메커니즘을 상세히 설명합니다. **컨테이너 런타임과 runC의 구조적 취약성** - Kubernetes는 containerd나 CRI-O 같은 런타임을 통해 컨테이너를 관리하며, 실제 프로세스 생성은 OCI 규격을 준수하는 하위 레벨 런타임인 runC가 담당합니다. - runC는 컨테이너 내부 프로세스를 실행할 때 자신을 포크(fork)한 뒤 `execve` 시스템 콜을 호출하는데, 이때 `/proc/self/exe` 경로를 통해 호스트에 있는 runC 이진 파일에 대한 파일 서술자(File Descriptor)를 열어두게 됩니다. - 과거 CVE-2019-5736 취약점에 대한 대응으로 runC를 읽기 전용으로 마운트하는 방어책이 도입되었으나, Dirty Pipe는 커널의 페이지 캐시를 직접 수정하므로 이러한 파일 시스템 수준의 권한 제한을 무력화합니다. **Dirty Pipe를 이용한 컨테이너 탈출 과정** - 공격자는 먼저 취약한 웹 애플리케이션 등을 통해 권한이 제한된 일반 컨테이너에 침투한 뒤, 호스트의 runC 바이너리가 실행되기를 대기합니다. - 관리자가 `kubectl exec`와 같은 명령을 수행하여 컨테이너 내부에서 runC가 구동되는 순간, 공격 프로세스는 `/proc/<runC-pid>/exe`를 통해 호스트의 runC 실행 파일에 접근합니다. - Dirty Pipe 공격 프리미티브를 활용하여 페이지 캐시에 로드된 runC 바이너리 내용을 공격자의 악성 ELF 코드로 덮어씁니다. - 이렇게 변조된 runC는 호스트의 루트 권한으로 실행되므로, 공격자는 호스트 시스템에서 임의의 명령(예: 호스트 이름 확인, 루트 권한 쉘 실행 등)을 수행하며 컨테이너 격리를 완전히 무너뜨립니다. **메모리 기반 공격의 비영구적 특성** - Dirty Pipe를 통한 바이너리 변조는 디스크의 실제 파일을 직접 수정하는 것이 아니라 커널의 페이지 캐시 내에서 발생합니다. - 따라서 공격으로 인한 변조는 시스템이 재부팅되거나 커널 캐시가 드롭(drop)되기 전까지만 유지되는 비영구적 특성을 가집니다. - 하지만 단 한 번의 실행만으로도 호스트에 백도어를 설치하거나 권한을 상승시키기에 충분하므로 그 위험성은 매우 높습니다. Dirty Pipe 취약점은 리눅스 커널 수준의 결함이므로 이를 근본적으로 해결하기 위해서는 최신 보안 패치가 적용된 커널로 신속히 업데이트해야 합니다. 또한 컨테이너 환경에서는 최소 권한 원칙을 철저히 준수하고, 런타임 보안 모니터링 도구를 도입하여 `/proc` 파일 시스템에 대한 의심스러운 접근이나 시스템 이진 파일의 비정상적인 동작을 실시간으로 감지하고 차단하는 방어 전략이 필요합니다.

우리 작업 시스템에서 쿠버네티스 오버헤드를 최소화한 방법 (새 탭에서 열림)

Datadog은 기존 작업 시스템을 Kubernetes로 이전하는 과정에서 CPU 사용량은 증가하고 작업 처리 속도는 40-50% 저하되는 성능 퇴행 문제를 겪었습니다. 이를 해결하기 위해 VM과 Kubernetes 간의 정밀한 비교 실험을 설계하고, 지표 측정 방식과 리소스 할당(Resource Requests) 설정을 최적화하여 성능을 이전 수준으로 복구했습니다. 본 분석을 통해 Kubernetes 오버헤드의 실체를 파악하고, 고성능 워크로드를 위한 포드 배치 전략을 도출했습니다. ### 실험 환경의 통제와 정렬 * **환경 변수 통일**: 성능 차이의 원인을 정확히 규명하기 위해 VM과 Kubernetes 클러스터의 인스턴스 유형(c5.2xlarge), 커널 버전(3.13.0-141), 실행 스크립트를 동일하게 맞추어 비교 대상을 단일화했습니다. * **배치 구조 최적화**: 기존 VM 방식과 유사하게 하나의 포드 내에 하나의 부모 프로세스와 그 자식 워커들을 배치하여, 노드당 부모 프로세스 수와 포드 수가 일치하도록 구성했습니다. ### 측정 지표의 재정의: Idle CPU의 중요성 * **Load Average의 한계**: Kubernetes에서는 상태 확인 등을 위한 배경 프로세스가 빈번하게 실행되는데, 이는 실제 CPU 사용량과 무관하게 Load Average 수치를 비정상적으로 높여 시스템이 바쁜 것처럼 오해하게 만듭니다. * **Idle CPU 활용**: 프로세스 개수가 아닌 '실제 CPU가 일하지 않는 시간'을 측정하는 Idle CPU 지표를 선택함으로써, 시스템의 남은 용량을 더 정확하게 파악하고 성능 분석의 신뢰도를 높였습니다. * **처리량(Throughput) 중심**: 배치 작업의 특성에 맞춰 지연 시간(Latency)보다는 30초당 완료된 작업 수라는 처리량 지표를 핵심 성능 지표로 설정했습니다. ### 리소스 요청(Resource Requests) 및 스케줄링 튜닝 * **스케줄링 병목 해결**: 초기에 각 포드가 1 Core CPU를 요청하도록 설정했을 때, 노드당 4개의 포드만 배치되는 과소 활용 문제가 발생했습니다. 목표치인 노드당 6개 포드 배치를 위해 CPU 요청을 100m으로, 메모리 요청을 500MB로 대폭 낮췄습니다. * **단일 리소스 기준 권장**: 여러 리소스(CPU, 메모리 등)의 요청 값을 모두 엄격하게 잡으면 스케줄링이 복잡해지므로, 하나의 주된 리소스를 기준으로 배치를 유도하고 나머지는 실제 필요량에 가깝게 설정하는 것이 효율적임을 확인했습니다. * **Request와 Limit의 구분**: `request`는 스케줄링을 위한 최소 보장치이며, 실제 실행 중의 제약은 `limit`이 담당하므로 `request`를 낮추는 것이 실행 성능에 부정적인 영향을 주지 않는다는 점을 활용했습니다. ### 포드별 오버헤드의 실체 분석 * **프로세스 구조**: `pstree`를 통해 분석한 결과, 포드당 오버헤드는 주로 컨테이너 런타임인 `containerd-shim`에서 발생했습니다. * **CPU 및 메모리 비용**: 실험 결과 포드당 CPU 오버헤드는 무시할 수 있는 수준이었으며, 메모리는 포드당 약 24MB(containerd-shim 및 pause 컨테이너 포함) 수준으로 측정되었습니다. * **결론적 선택**: 오버헤드가 크지 않기 때문에, 관리 효율성을 위해 포드 하나에 여러 부모 프로세스를 억지로 집어넣기보다 '포드당 1 부모 프로세스' 구조를 유지하는 것이 더 유리하다는 결론을 내렸습니다. Kubernetes로 이전 시 발생하는 성능 저하는 플랫폼 자체의 문제라기보다 잘못된 리소스 요청 설정과 지표 해석에서 기인하는 경우가 많습니다. 노드당 포드 밀도를 최적화하기 위해 `Resource Requests`를 전략적으로 낮게 설정하고, 시스템의 부하를 판단할 때는 Load Average 대신 Idle CPU를 관찰함으로써 VM에 근접한 성능을 확보할 수 있습니다.