블로그
신뢰의 오류: 멀티 테넌트 Docker 설정이 데이터를 유출한 방법 (그리고 우리가 해결한 방법)
한 팀의 순진한 Docker 설정이 교차 테넌트 데이터 유출로 이어지고 이를 방지한 계층적 격리 전략에 대해 알아보세요.
요약
Docker 컨테이너는 기본적으로 격리되어 있지 않습니다. 호스트 커널을 공유하며, 신중한 구성 없이는 테넌트가 서로 간섭할 수 있습니다. 이 글에서는 멀티 테넌트 호스팅 제공업체가 공유 네트워킹과 약한 보안 기본값으로 인해 클라이언트 컨테이너가 서로의 데이터베이스에 접근할 수 있음을 발견한 실제 시나리오를 다룹니다. 우리는 침해를 해결한 단계별 변경 사항을 보여줍니다: 테넌트별 사용자 정의 네트워크, 비루트 사용자, 제거된 권한, 읽기 전용 파일 시스템 및 seccomp 프로필. 컨테이너가 본질적으로 강력한 격리를 제공한다는 일반적인 가정에 대해 VM이 여전히 더 강력한 경계를 제공하는 이유와 하이브리드 접근 방식을 고려해야 할 때를 설명함으로써 반박합니다. 결론은 격리가 단일 체크박스가 아닌 계층적 작업임을 강조합니다.
사건: 컨테이너가 너무 많이 말할 때
단일 호스트에 Docker를 설정하여 여러 클라이언트 웹사이트를 실행했다고 가정해 보세요. 각 클라이언트는 자체 컨테이너를 가지고 있습니다. 깔끔하고 격리된 환경이죠? 저희도 그렇게 생각했습니다. 일상적인 보안 감사에서 클라이언트 A의 컨테이너가 같은 호스트에 있는 클라이언트 B의 컨테이너 MySQL 소켓을 읽고 있다는 사실이 밝혀질 때까지 말이죠. 그들은 기본 bridge 네트워크를 공유하고 있었습니다. 더 나쁜 것은 컨테이너가 root로 실행되어, 하나를 장악한 공격자가 호스트의 Docker 소켓이나 다른 컨테이너의 파일 시스템을 조작할 수 있었다는 점입니다. 침해는 정교한 익스플로잇이 아니라 기본적인 구성 오류였습니다. 데이터가 유출되었고, 신뢰가 무너졌습니다.
이러한 실패 시나리오는 드문 일이 아닙니다. 많은 팀이 Docker의 네임스페이스와 cgroup이 자동으로 테넌트를 격리한다고 가정하지만, 기본적으로 얼마나 많은 탈출 구멍이 열려 있는지 과소평가합니다. 기본 브릿지 네트워크는 컨테이너 간 네트워크 격리를 제공하지 않습니다. 루트로 실행하면 컨테이너에 필요한 것보다 더 많은 권한을 부여합니다. 명시적인 리소스 제한이 없으면 한 시끄러운 이웃이 다른 컨테이너의 CPU나 메모리를 고갈시킬 수 있습니다.
1단계: 단일 네트워크 공유 중단
첫 번째 수정은 각 테넌트에 자체 사용자 정의 Docker 네트워크를 제공하는 것이었습니다. 이렇게 하면 명시적으로 연결하지 않는 한 컨테이너가 서로 도달하지 못합니다. 우리는 각 테넌트에 대해 전용 네트워크를 생성하고 애플리케이션 컨테이너를 연결하는 스크립트를 만들었습니다. 데이터베이스 컨테이너는 동일한 테넌트 네트워크에 있지만, 테넌트 내 통신 전용 내부 네트워크도 추가했습니다. 더 이상 교차 테넌트 염탐은 없습니다.
또한 데이터베이스를 동일한 테넌트 네트워크의 별도 컨테이너에서 실행하고 별도의 데이터 볼륨을 사용하여 격리했습니다. 이를 통해 공격자가 앱 컨테이너에 침입하더라도 다른 테넌트의 데이터베이스 트래픽을 스니핑할 수 없도록 했습니다.
네트워크 격리 전략에 대한 자세한 내용은 멀티 테넌트 호스팅을 위한 실용적인 Docker 격리 보안 체크리스트를 참조하세요.
2단계: 불필요한 권한 제거
기본적으로 Docker 컨테이너는 제한된 Linux 기능 세트로 실행되지만, 대부분의 애플리케이션에 필요한 것보다 더 많은 기능을 가지고 있습니다. 우리 컨테이너는 root로 실행되어 내부 프로세스가 파일 시스템 마운트나 커널 매개변수 변경과 같은 작업을 수행할 수 있었습니다. 우리는 컨테이너 내에서 애플리케이션을 비루트 사용자로 실행하도록 전환하고(Dockerfile의 USER 지시문 사용), 절대적으로 필요한 기능을 제외한 모든 기능을 제거했습니다. 일반적인 웹 앱의 경우 NET_BIND_SERVICE(1024 미만 포트 바인딩)와 CHOWN(디렉토리 쓰기)만 필요할 수 있습니다. 또한 권한 상승을 방지하기 위해 --security-opt no-new-privileges를 추가했습니다.
이 한 단계만으로도 많은 일반적인 컨테이너 탈출 벡터가 제거되었습니다. 웹 서버를 장악한 공격자는 프로세스에 CAP_SYS_ADMIN 또는 CAP_DAC_OVERRIDE 기능이 없기 때문에 패키지를 설치하거나 시스템 바이너리를 수정하거나 호스트의 Docker 소켓에 액세스할 수 없습니다.
3단계: 파일 시스템 잠금
쓰기 가능한 파일 시스템은 일반적인 공격 표면입니다. 우리는 모든 컨테이너의 루트 파일 시스템을 읽기 전용(--read-only)으로 만들고, /tmp 및 애플리케이션의 캐시 디렉토리와 같이 쓰기 액세스가 필요한 디렉토리에 임시 파일 시스템(tmpfs)을 마운트했습니다. 이는 공격자가 애플리케이션 코드를 수정하거나 악성 바이너리를 유지하는 것을 방지합니다.
또한 Docker의 --mount 옵션을 사용하여 Docker 소켓과 같은 민감한 디렉토리를 절대적으로 필요한 경우에만 바인드 마운트하고, 프로덕션 컨테이너에서는 절대 마운트하지 않았습니다. 원칙: 컨테이너가 경로에 쓸 필요가 없으면 읽기 전용으로 만드십시오.
4단계: Seccomp 및 AppArmor 프로필 적용
기본 seccomp 프로필은 이미 많은 위험한 시스템 호출을 차단하지만, 애플리케이션이 실제로 필요한 시스템 호출만 허용 목록에 추가하도록 추가로 사용자 정의했습니다. 이는 애플리케이션 프로파일링이 필요하기 때문에 절충점이 있습니다. 더 간단한 방법은 Docker의 기본 seccomp 프로필을 사용하고 더 엄격한 규칙이 필요한 경우 --security-opt seccomp=path/to/profile.json을 추가하는 것입니다. 마찬가지로 AppArmor 프로필은 컨테이너 프로세스를 특정 파일 경로와 기능으로 제한할 수 있습니다. 우리는 AppArmor를 활성화하고 애플리케이션의 데이터 디렉토리에만 액세스를 제한하는 사용자 정의 프로필을 사용했습니다.
이러한 강화 단계에 대한 포괄적인 가이드는 멀티 테넌트 호스팅을 위한 Docker 컨테이너 강화: 단계별 격리 가이드를 참조하세요.
반대 의견: 때로는 VM이 필요합니다
아무리 강화해도 컨테이너는 호스트의 커널을 공유합니다. 하나의 커널 취약점으로 모든 격리가 한 번에 무너질 수 있습니다. 이것이 많은 보안에 민감한 플랫폼이 경량 VM 내에서 컨테이너를 실행하는 이유입니다. 각 테넌트는 자체 커널을 얻습니다. 이는 오버헤드를 추가하지만 컨테이너만으로는 불가능한 하드웨어 수준의 경계를 제공합니다. 테넌트가 신용 카드 데이터나 건강 기록을 처리하는 경우 하이브리드 접근 방식(VM 내 컨테이너)이 올바른 선택일 수 있습니다. 컨테이너 격리가 위협 모델에 충분하다고 가정하지 마십시오. 데이터의 민감도와 규제 요구 사항을 평가하십시오.
격리 수준에 대한 심층 비교는 멀티 테넌트 Docker 아키텍처 설계: 올바른 격리 수준 선택을 읽어보세요.
결론: 격리는 스위치가 아닌 스택입니다
수정은 단일 변경이 아니라 계층화였습니다: 네트워크 격리, 제한된 권한, 읽기 전용 파일 시스템 및 시스템 호출 필터링. 그럼에도 불구하고 공유 커널 컨테이너로 완벽한 격리는 불가능하다는 것을 인정했습니다. 가장 높은 보안 요구 사항을 가진 테넌트를 위해 전용 호스트로 이전했습니다. 교훈: 기본값을 신뢰하지 마십시오. 이미 침해가 발생한 것처럼 Docker 설정을 감사하십시오. 잠금 시간은 유출 후가 아니라 전입니다.


