Docker를 프로덕션 환경에서 몇 달씩 운영해도 눈에 띄는 문제가 없을 수 있습니다. 컨테이너는 정상적으로 시작되고, 앱은 응답하며, 아무것도 깨지지 않습니다. 그러다 노출된 포트 하나, 잘못 설정된 권한 하나가 공격자에게 발판을 제공합니다. 공격자가 특별히 노력하지 않아도 됩니다. Docker 보안 실수는 대부분 문제가 터지기 전까지는 실수처럼 보이지 않습니다.
이 글에서는 컨테이너 환경을 위험에 빠뜨리는 구체적인 설정들을 다룹니다. 각 설정이 공격자에게 어떤 기회를 주는지 설명하고, 지금 바로 자신의 환경에 적용할 수 있는 체크리스트로 마무리합니다.
Docker 보안이 생각보다 어려운 이유
컨테이너는 격리된 것처럼 느껴집니다. 컨테이너를 시작하면 자체 프로세스 공간에서 실행되고, 내부에서 보면 옆 컨테이너는 존재하지 않는 것 같습니다. 격리는 실제로 제공되지만, 완전하지는 않습니다. 컨테이너는 호스트의 커널을 공유하기 때문에, 특정 조건에서 컨테이너 내부의 프로세스가 호스트 시스템 전체에 접근할 수 있습니다.
Docker의 기본 설정은 개발자 편의를 위한 것이지, 프로덕션 보안 강화를 위한 것이 아닙니다. root 접근이 활성화되어 있고, 모든 포트는 모든 인터페이스에 바인딩할 수 있으며, 런타임 모니터링도 없습니다. 대부분의 개발자는 이 설정을 그대로 받아들여 컨테이너를 배포하고 넘어갑니다. 시작 단계에서는 합리적인 접근이지만, 완성된 보안 태세는 아닙니다.
에 따르면 Red Hat의 2024년 Kubernetes 보안 현황 보고서, 67%의 조직이 컨테이너 또는 Kubernetes 보안 문제로 인해 애플리케이션 배포를 지연하거나 늦췄습니다. 이 마찰은 대부분 실제 공격 때문이 아닙니다. 팀이 컨테이너 설정에 처음부터 적용했어야 할 보안 강화가 빠져 있다는 사실을 뒤늦게 발견하기 때문입니다.
개발자 로컬 머신에서 사용하던 설정 그대로 프로덕션에서 실행되는 컨테이너를 자주 봅니다. Docker 보안 실수가 소리 없이 쌓이는 지점이 바로 여기입니다. 감사를 받거나 장애가 발생하기 전까지는 아무런 증상도 나타나지 않습니다.
이런 취약점을 만드는 실수들은 구체적이고, 예측 가능하며, 대부분 피할 수 있습니다. 시작은 설정 단계입니다.
흔한 Docker 설정 실수
대부분의 컨테이너 침해는 제로데이 익스플로잇으로 시작되지 않습니다. 첫날 네트워크 노출 범위나 권한 범위를 깊이 고려하지 않고 설정한 구성에서 시작됩니다.
Docker의 기본 설정은 동작하도록 만들어져 있습니다. 동작하는 것과 안전한 것 사이의 간극에서 Docker 컨테이너 보안 위험이 쌓입니다. 특히 한 번 배포하고 다시 들여다보지 않는 자체 호스팅 환경에서 두드러집니다.
이런 패턴을 자주 봅니다. 퍼블릭 IP 서버에서 실행 중인 컨테이너의 포트 바인딩, 사용자 설정, 네트워크 구성이 최초 배포 당시와 완전히 동일한 경우입니다.
컨테이너를 root로 실행하기
Docker 컨테이너를 시작할 때 사용자를 지정하지 않으면 root로 실행됩니다. 이는 애플리케이션을 포함한 컨테이너 내부의 모든 프로세스가 컨테이너 네임스페이스 내에서 root 수준의 권한을 갖는다는 의미입니다.

컨테이너 내부의 root는 호스트의 root와 같지 않지만, 그 분리가 완전하지는 않습니다. 잘 알려진 runc CVE-2019-5736과 유사한 런타임 취약점처럼, 런타임을 대상으로 한 권한 상승 익스플로잇은 성공하기 위해 root 컨테이너 프로세스를 필요로 하는 경우가 많습니다.
루트가 아닌 컨테이너는 해당 취약점이 의존하는 루트 프로세스 요구사항을 제거하여, 그 종류의 취약점에 대한 공격 표면을 크게 줄입니다. 다만 컨테이너 탈출 위험을 완전히 없애지는 못합니다.
Docker 파일에 USER 지시어를 추가하면 이 문제를 해결할 수 있습니다. 일부 공식 이미지는 USER 지시어로 활성화할 수 있는 비권한 사용자를 함께 제공하지만, 많은 이미지가 별도의 앱 사용자 없이 여전히 root를 기본값으로 사용합니다. 그런 경우에는 Docker 파일에서 사용자로 전환하기 전에 해당 사용자를 직접 생성해야 합니다. 대부분의 셀프호스팅 환경에서 이 한 가지 변경만으로 권한 상승 위험의 전체 범주를 제거할 수 있습니다.
퍼블릭 인터넷에 너무 많은 포트 노출하기
Docker로 포트를 퍼블리시하면, Docker는 자체 iptables 규칙을 직접 작성합니다. 이 규칙은 호스트 수준의 방화벽 규칙보다 먼저 실행됩니다. 이것은 커뮤니티에서 보고된 잘 알려진 동작입니다 및 Docker의 패킷 필터링 가이드에 문서화된 내용으로, 잘못된 설정이 아닙니다. 즉, UFW 등의 도구는 Docker가 이미 열어 놓은 포트를 차단하지 못합니다.

Docker는 iptables에 직접 규칙을 작성하여, 많은 Linux 호스트에서 UFW 및 firewalld 기본 설정을 우회합니다. 따라서 방화벽이 제대로 설정된 것처럼 보여도 0.0.0.0에 바인딩된 포트가 외부에서 접근 가능할 수 있습니다. 클라우드 보안 그룹과 DOCKER-USER 체인 규칙으로 해당 트래픽을 차단할 수는 있으므로, 실제 노출 범위는 네트워크 설정에 따라 달라집니다.
가능한 경우 서비스를 127.0.0.1에 바인딩하고, 외부에 노출되는 트래픽은 리버스 프록시를 통해 라우팅하며, 외부 접근이 실제로 필요한 포트만 퍼블리시하세요. 리버스 프록시는 호스트 외부에서 무엇을 노출할지 제어하는 가장 확실한 방법입니다.
컨테이너 간 네트워크 격리 무시하기
해당 네트워크에 있는 컨테이너는 제한 없이 같은 네트워크의 다른 컨테이너에 접근할 수 있습니다. 기본 브리지는 이를 공유하는 컨테이너 간 트래픽을 필터링하지 않으며, 대부분의 환경에서 이 설정은 변경되지 않습니다.

컨테이너 하나가 침해되면, 그 열린 통신 경로가 측면 이동 경로가 됩니다. 프론트엔드 컨테이너는 데이터베이스, 내부 API, 또는 같은 기본 브리지 네트워크에 있는 모든 것에 접근할 수 있습니다. 그런 접근이 처음부터 의도되지 않았더라도 마찬가지입니다.
사용자 정의 네트워크를 사용하면 어떤 컨테이너가 통신할 수 있는지 명시적으로 제어할 수 있지만, 모든 서비스가 하나의 커스텀 네트워크를 공유하면 컨테이너 간 자유로운 통신은 여전히 가능합니다. 진정한 격리는 서로 통신해서는 안 되는 서비스를 별도의 네트워크에 분리하는 것을 의미합니다. 기본 브리지를 끄는 것은 출발점일 뿐, 끝이 아닙니다.
Docker 소켓 간과하기
/var/run/docker.sock에 위치한 Docker 소켓은 Docker 엔진 전체의 제어 인터페이스입니다. 이것을 컨테이너에 마운트하면, 해당 컨테이너는 호스트에서 실행 중인 데몬에 API로 직접 접근할 수 있게 됩니다.

이 접근 권한을 가진 컨테이너는 새 컨테이너를 시작하고, 호스트 디렉토리를 마운트하고, 실행 중인 컨테이너를 검사 및 수정하고, 사실상 호스트 머신 전체를 제어할 수 있습니다. 공격 표면은 호스트의 root와 동등하므로, 소켓 접근이 필요한 도구는 신중하게 검토해야 합니다.
대부분의 사용 사례에는 더 안전한 대안이 있습니다. 범위가 제한된 API나 Docker 관리 도구 가 소켓 접근 없이도 사용 가능합니다. Docker-in-Docker는 자체적인 보안 및 운영상의 트레이드오프가 있어 단순한 대안이 되지 않습니다.
설정 실수가 최초의 노출을 만들고, 이미지와 의존성 선택이 그 노출이 시간이 지나면서 얼마나 커지는지를 결정합니다.
컨테이너보다 오래가는 이미지 및 시크릿 실수
컨테이너를 중지하면 그 안의 설정 실수도 함께 멈춥니다. 하지만 취약점이나 하드코딩된 자격증명을 포함한 이미지로 다시 빌드하면, 그 문제는 컨테이너가 시작될 때마다 함께 돌아옵니다. 이미지 수준의 실수는 배포 사이에 초기화되지 않습니다.
이미지를 pull하는 모든 환경, 이미지를 저장하는 모든 레지스트리, 이미지를 실행하는 모든 팀원에게 함께 따라다닙니다. 이러한 지속성 때문에 이미지 및 시크릿 관리는 별도의 위험 범주로 분류되며, 설정 관리와는 독립적으로 감사할 필요가 있습니다.
이런 패턴을 자주 봅니다. 프로젝트 초기에 신중하게 선택한 이미지를 그 이후로 한 번도 재빌드하지 않아, 처음에 보장하던 보안 기준에서 점점 멀어지는 경우입니다.
신뢰할 수 없거나 오래된 이미지 사용
퍼블릭 레지스트리는 누구에게나 열려 있습니다. Docker Hub를 통해 악성 이미지가 배포된 사례가 있으며, 이런 이미지에는 컨테이너를 재시작해도 사라지지 않는 레이어 히스토리에 크립토마이너와 백도어가 심겨 있습니다. 특히 비공식 또는 출처 불명의 배포자가 올린 이미지를 pull하기 전에는 반드시 검증이 필요합니다.

별개의 문제는 오래된 이미지입니다. 6개월 전에 pull한 뒤 한 번도 재빌드하지 않은 공식 이미지는, 그동안 공개된 CVE마다 패치되지 않은 Docker 취약점을 쌓아왔습니다. 이미지 자체가 고장난 것은 아닙니다. 단지 더 이상 최신 상태가 아닐 뿐입니다.
Sonatype의 2024 State of the Software Supply Chain 보고서 에 따르면, 취약한 컴포넌트가 사용되는 경우의 95%에서 이미 수정된 버전이 존재하며, 애플리케이션 의존성의 80%는 1년 이상 업그레이드되지 않은 상태로 유지됩니다. 이 패턴은 동일한 오픈소스 패키지에 의존하는 Docker 베이스 이미지에도 그대로 적용됩니다.
검증된 배포자의 공식 이미지를 사용하고, "latest" 태그에 의존하지 말고 특정 버전 태그를 고정하세요. 이미지를 최신 상태로 유지하기 위해 정기적인 재빌드 주기를 만들어두세요.
Dockerfile과 Compose 파일에 시크릿 하드코딩하기
Dockerfile의 ENV나 ARG 명령어에 작성된 자격 증명, Compose의 environment 블록에 하드코딩된 값, 빌드 인수로 전달된 값, 또는 버전 관리에 커밋된 .env 파일에 저장된 값은 컨테이너를 중지해도 사라지지 않습니다. 이미지 레이어 히스토리나 소스 컨트롤에 남아, 접근 권한이 있는 누구에게나 노출됩니다.

이것은 Docker 보안에서 가장 간과되는 실수 중 하나인데, 개발 중에 눈에 띄는 문제를 일으키지 않기 때문입니다. ENV 명령어에 있는 API 키는 정상적으로 동작합니다. 동시에 그 키는 저장소에 남아 있고, 이미지에 내장되어, 해당 이미지가 이동하는 모든 곳에 배포됩니다.
최신 Docker Compose는 이미지에 자격 증명을 내장하지 않고 런타임에 마운트하는 네이티브 시크릿 메커니즘을 지원합니다. Docker의 시크릿 API와 외부 시크릿 매니저도 동일한 원칙을 따릅니다. 이 방법들을 사용하면 자격 증명이 빌드 아티팩트와 커밋된 파일에서 완전히 분리됩니다.
런타임 환경 변수는 하드코딩된 자격 증명보다는 낫지만, Docker inspect 출력, 로그, 크래시 덤프를 통해 여전히 노출될 수 있습니다. 이미지에 내장된 시크릿보다 한 단계 나은 방법일 뿐, 완전한 해결책은 아닙니다.
컨테이너 이미지를 정기적으로 업데이트하지 않는 경우
몇 달째 같은 이미지를 실행하는 것은 흔한 습관입니다. 새로운 취약점이 공개된 이후 재빌드하기까지 매일이 지날수록, 컨테이너는 외부에서 전혀 변화가 없어 보이면서도 노출 기간이 늘어납니다.
일관된 재빌드 일정을 만드세요. 가능한 부분은 자동화하고, 현재 이미지에 대해 주기적으로 취약점 스캐너를 실행하세요. 목표는 완벽함이 아닙니다. 패치가 출시된 시점과 배포되는 시점 사이의 간격을 줄이는 것입니다.
접근 제어와 모니터링은 빠른 배포 환경에서 우선순위가 밀리기 쉽습니다. 동시에 이 두 가지는 문제가 가장 오랫동안 감지되지 않는 영역이기도 합니다.
접근 제어와 가시성 공백
컨테이너가 탄탄한 설정과 최신 이미지로 실행되고 있더라도, 두 가지 실패 범주가 남아 있습니다. 둘 다 본질적으로 보이지 않습니다. 누군가 약한 접근 제어를 실제로 이용하기 전까지는 문제를 알아채지 못하고, 모니터링 공백은 로그에 남지 않은 활동을 조사해야 할 때가 되어서야 드러납니다.
동일함 Red Hat 2024년 연구 에 따르면, 팀의 42%가 컨테이너 보안 및 관련 위협에 대응할 충분한 역량을 갖추지 못한 것으로 나타났습니다.
모니터링 공백은 대개 사고가 발생하기 전이 아니라, 사고 조사 과정에서 드러납니다. 가시성이 우선순위가 될 때쯤이면, 이미 무언가에 대응하고 있는 경우가 많습니다.
취약한 인증과 노출된 관리 대시보드
공개 IP에 인증 없이 노출된 컨테이너 관리 대시보드를 공격하는 데 고급 기술이 필요한 건 아닙니다. 주소만 알면 됩니다. 대부분의 팀이 생각하는 것보다 훨씬 낮은 진입 장벽입니다.

셀프 호스팅 모니터링 및 관리 도구는 대부분 모든 네트워크 인터페이스에서 접근 가능한 웹 인터페이스가 기본으로 활성화된 상태로 제공됩니다. 인증 없이 공개 IP에 그대로 두는 건, 어드민 패널을 잠그지 않고 놔두는 것과 컨테이너 세계에서 동일한 행위입니다.
기본 조건은 세 가지입니다. 인증, 리버스 프록시, 그리고 프라이빗 네트워크 배치. 접근 제어는 관리 인터페이스에 별도로 추가하는 설정 단계이며, 기본적으로 활성화되어 제공되지 않습니다.
같은 원칙이 적용됩니다 Docker CLI 및 GUI 관리. 데몬에 대한 어드민 수준의 접근은 인터페이스 종류에 관계없이 동일한 위험을 수반합니다.
컨테이너가 무엇을 하는지 모니터링하지 않는 것
컨테이너가 침해되면 공격자의 활동은 흔적을 남깁니다. 프로세스 동작 변화, 비정상적인 네트워크 연결, 예상치 못한 파일 변경이 그것입니다. 로그 수집이 갖춰져 있지 않으면, 그 흔적은 실제로 활용할 수 있는 형태로 남지 않습니다.
중앙화된 로그 수집, 컨테이너 감사 로깅, 런타임 모니터링 도구를 갖추면 비정상적인 활동이 커지기 전에 감지할 수 있는 데이터를 확보할 수 있습니다. 목표는 모든 로그를 분석하는 것이 아닙니다. 조사가 필요할 때 데이터가 있어야 한다는 것입니다.
로그 파이프라인도 없고 알림도 없이 프로덕션에서 조용히 돌아가는 컨테이너 환경은 관리가 적게 드는 게 아닙니다. 점검이 안 되고 있는 겁니다. 이 둘은 전혀 다른 운영 상태입니다.
인프라 환경이 중요한 이유
컨테이너 보안은 설정에서 시작하지만, 그 설정은 인프라 위에서 동작합니다. 네트워크 설정이 잘못된 호스트, 공유 리소스, 네트워크 수준 필터링 부재는 그 위에서 실행되는 모든 컨테이너에 영향을 미치는 조건을 만들어냅니다. 컨테이너 설정을 올바르게 하는 것과 서버 설정을 올바르게 하는 것은 별개의 작업입니다.
많은 Docker 보안 취약점은 컨테이너 자체가 물려받는 환경 조건에 의해 증폭됩니다:
- 테넌트 간 하드웨어 격리가 없는 공유 테넌시 서버
- 패치가 적용되지 않은 호스트 커널
- 네트워크 수준 필터링이 내장되지 않은 호스트
이것이 위에서 설명한 설정 단계의 필요성을 없애지는 않습니다. 인프라 계층과 관계없이 컨테이너 하드닝은 중요합니다. 격리된 인프라에서 시작하면 고려해야 할 요소 하나를 줄일 수 있습니다.
Cloudzy에서는 설정 요구사항에 따라 두 가지 옵션을 제공합니다:
- Linux VPS: Docker를 직접 배포하고 이 글의 하드닝 단계를 적용할 수 있는 깔끔한 환경
- Portainer VPS: Portainer가 사전 설치된 원클릭 옵션. 서버를 시작하면 바로 대시보드에 접속할 수 있습니다
두 옵션 모두 동일한 인프라에서 실행됩니다: KVM 가상화, 부스트 클록 최대 5.7 GHz의 AMD Ryzen 9 CPU, DDR5 메모리, NVMe SSD 스토리지, 최대 40 Gbps 네트워크, BuyVM 필터링을 통한 무료 DDoS 보호. 전 세계 12개 리전에서 제공되며 99.95% 가동률 SLA를 보장합니다.
VPS에서 Portainer를 운영하는 방법에 대한 자세한 내용은 별도 아티클에서 다루고 있습니다.
Docker 배포를 위한 실용적인 보안 체크리스트
위에서 살펴본 Docker 보안 실수들은 대부분 한 번 내리고 다시 검토하지 않은 단일 설정 결정에서 비롯됩니다. 기존 환경에 이 체크리스트를 적용하면 그러한 취약점을 발견할 수 있습니다. 배포 가이드가 아니라 감사 도구로 활용하세요.
이러한 Docker 보안 권장 사항은 앞서 설명한 가장 일반적인 구성 오류로부터 Docker 컨테이너를 보호하는 방법을 다룹니다.
빠른 참조: 9가지 실수 전체 목록
| 실수 | 항목 | 한 줄 수정 |
| root로 실행 중 | 설정 | 추가 USER Docker 파일에 지시문 추가 |
| 0.0.0.0에 바인딩된 포트 | 설정 | 127.0.0.1에 바인딩하고 리버스 프록시를 통해 라우팅 |
| 네트워크 격리 없음 | 설정 | 접근 권한에 따라 서비스를 별도의 사용자 정의 네트워크로 분리하세요. |
| Docker 소켓 마운트됨 | 설정 | 마운트를 제거하고, 범위가 지정된 API 또는 대안을 사용하세요. |
| 신뢰할 수 없거나 오래된 이미지 | 이미지 | 버전 태그가 고정된 공식 이미지 사용 |
| 하드코딩된 비밀번호 | 이미지 | 자격 증명을 런타임 환경 변수 또는 시크릿 매니저로 이동 |
| 이미지 재빌드 일정 없음 | 이미지 | 월별 재빌드 주기를 설정하고, 가능한 경우 자동화하세요. |
| 인증되지 않은 대시보드 | 접근 | 인증을 추가하고 관리 UI를 프라이빗 네트워크로 이동 |
| 컨테이너 로그 수집 없음 | 접근 | 중앙화된 로깅 및 런타임 모니터링 설정 |
기존 설정에 먼저 실행해 보길 권장합니다. 문제가 이미 존재할 가능성이 가장 높은 곳이기 때문입니다.
루트가 아닌 사용자로 실행 중인 컨테이너: Docker 파일에 USER 지시문이 있는지 확인하세요. 없으면 컨테이너는 루트로 실행됩니다.
포트 바인딩이 로컬호스트로 제한되거나 프록시 처리됨: docker ps를 실행하고 포트 바인딩을 확인하세요. 0.0.0.0:PORT 항목은 업스트림 보안 그룹, 외부 방화벽, 또는 DOCKER-USER 체인 규칙이 차단하지 않는 호스트에서 외부에서 접근 가능할 수 있습니다.
사용자 정의 브리지 네트워크 사용 여부: Docker의 기본 브리지에 있는 컨테이너는 서로 자유롭게 통신할 수 있습니다. 동일한 사용자 정의 브리지의 컨테이너도 서로 통신이 가능하므로, 실질적인 격리를 위해 신뢰 경계에 따라 서비스를 별도의 네트워크로 분리하세요.
컨테이너에 Docker 소켓이 마운트되지 않음: Compose 파일과 실행 인수를 확인하세요. /var/run/docker.sock이 볼륨으로 나타나면, 해당 마운트가 필요하고 의도된 것인지 확인하세요.
검증된 퍼블리셔의 버전이 고정된 베이스 이미지 사용: FROM ubuntu:latest는 최신 버전이 아닐 수 있는 불특정 버전을 가져옵니다. 특정 릴리스 버전을 명시해서 고정하세요.
Docker 파일, Compose 파일, 빌드 인수에 시크릿 미포함: 이미지 레이어 히스토리는 컨테이너 삭제 후에도 자격 증명을 유지합니다. Compose secrets, Swarm secrets, 빌드 시크릿 마운트, 또는 외부 시크릿 매니저를 사용하세요. 하드코딩된 값보다는 런타임 환경 변수가 낫지만, 이 방법도 inspect 출력 및 로그에 노출됩니다.
이미지 재빌드 일정 정의: 오래된 이미지에는 취약점이 쌓입니다. 월 1회 재빌드 주기를 유지하면 대부분의 환경에서 노출 기간을 적정 수준으로 관리할 수 있습니다.
관리 인터페이스에 인증 적용: 인증 없이 공개 IP에 노출된 대시보드는 그 자체로 열린 진입점입니다. 가능하면 프라이빗 네트워크에 배치하는 것이 바람직합니다.
컨테이너 로그 수집 여부: 로그 파이프라인이 없으면 시스템에 가시적인 영향이 나타난 뒤에야 장애를 감지할 수 있습니다. 그건 대응하기엔 너무 늦은 신호입니다.
결론
Docker의 기본 설정은 편의성을 위한 것이지, 보안을 위한 것이 아닙니다. 이 글에서 다루는 실수들은 대부분 정교한 공격이 아니라, 초기 배포 이후 한 번도 바꾸지 않은 설정에서 비롯됩니다.
수정 사항은 대부분 한 번만 적용하면 되는 설정 결정입니다. USER 지시문, 포트 바인딩 변경, 커스텀 네트워크, 재빌드 일정 등 대부분의 환경에서 새로운 툴링이 필요하지 않습니다.
컨테이너 설정을 올바르게 하는 것이 첫 번째 과제이고, 그 위에서 실행되는 인프라가 두 번째 과제입니다. 둘 다 중요하며, 어느 하나가 다른 하나를 대신할 수 없습니다.