Docker 엔진 보안
Docker 엔진 보안
Docker 엔진의 보안을 검토할 때는 크게 네 가지 영역을 살펴봐야 합니다. 커널 자체의 보안 특성과 네임스페이스·cgroup 지원, Docker 데몬 자체의 공격 표면, 컨테이너 구성 프로필의 허점(기본값이든 사용자 지정이든), 그리고 커널의 "하드닝" 보안 기능이 컨테이너와 어떻게 상호작용하는지가 바로 그것이에요. 하나씩 차근차근 살펴보도록 할게요.
출처: 공식문서
본문
커널 네임스페이스
Docker 컨테이너는 LXC 컨테이너와 매우 유사하고, 비슷한 보안 기능을 갖고 있어요. docker run으로 컨테이너를 시작하면, 내부적으로 Docker는 그 컨테이너를 위해 여러 네임스페이스와 컨트롤 그룹을 생성합니다.
네임스페이스는 가장 기본적이면서도 가장 확실한 격리 수단을 제공해요. 컨테이너 안에서 실행되는 프로세스는 다른 컨테이너나 호스트 시스템에서 실행 중인 프로세스를 볼 수 없고, 영향을 주는 것도 훨씬 어렵습니다.
각 컨테이너는 고유한 네트워크 스택도 갖게 되는데, 이는 어떤 컨테이너도 다른 컨테이너의 소켓이나 네트워크 인터페이스에 특권을 가지고 접근할 수 없다는 뜻이에요. 물론 호스트 시스템을 그렇게 설정해 두면 컨테이너끼리 각자의 네트워크 인터페이스를 통해 서로 통신할 수 있어요. 외부 호스트와 통신하는 것과 마찬가지로 말이죠. 컨테이너에 공용 포트를 지정하거나 링크를 사용하면 컨테이너 간 IP 트래픽이 허용됩니다. 서로 ping을 주고받거나 UDP 패킷을 송수신하고 TCP 연결을 맺을 수 있지만, 필요하다면 제한할 수도 있어요. 네트워크 아키텍처 관점에서 보면 특정 Docker 호스트의 모든 컨테이너는 브리지 인터페이스 위에 놓여 있어요. 즉, 공통 이더넷 스위치에 연결된 물리 머신들과 똑같은 관계라고 보면 됩니다. 그 이상도 이하도 아니에요.
커널 네임스페이스와 프라이빗 네트워킹을 제공하는 코드는 얼마나 성숙해 있을까요? 커널 네임스페이스는 커널 버전 2.6.15와 2.6.26 사이에 도입됐어요. 즉 2008년 7월(2.6.26 릴리스 시점) 이후로 네임스페이스 코드는 수많은 프로덕션 시스템에서 실행되고 검증되어 왔다는 뜻이에요. 게다가 네임스페이스 코드의 설계와 영감은 더 오래됐어요. 네임스페이스는 사실 OpenVZ의 기능을 메인스트림 커널에 통합할 수 있도록 다시 구현하려는 노력의 결과물이거든요. OpenVZ는 2005년에 처음 릴리스되었으니, 설계와 구현 모두 꽤 성숙한 상태라고 할 수 있어요.
컨트롤 그룹
컨트롤 그룹(Control Groups)은 Linux 컨테이너의 또 다른 핵심 구성 요소예요. 리소스 사용량을 기록하고 제한하는 기능을 구현하며, 유용한 메트릭을 많이 제공해요. 또한 각 컨테이너가 메모리, CPU, 디스크 I/O를 공정하게 배분받도록 보장하고, 더 중요한 것은 단일 컨테이너가 이런 리소스 중 하나를 고갈시켜 시스템 전체를 다운시키는 일을 막아줍니다.
컨트롤 그룹은 한 컨테이너가 다른 컨테이너의 데이터나 프로세스에 접근하거나 영향을 주는 것을 막는 역할은 하지 못하지만, 일부 DoS(서비스 거부) 공격을 막는 데는 필수적이에요. 특히 퍼블릭/프라이빗 PaaS 같은 멀티 테넌트 플랫폼에서 일부 애플리케이션이 문제를 일으키기 시작해도 일관된 업타임(과 성능)을 보장하는 데 아주 중요합니다.
컨트롤 그룹도 꽤 오래됐어요. 코드는 2006년에 시작되어 처음에는 커널 2.6.24에 통합되었습니다.
Docker 데몬 공격 표면
Docker로 컨테이너(와 애플리케이션)를 실행한다는 것은 Docker 데몬을 실행한다는 뜻이에요. Rootless mode를 선택하지 않는 한 이 데몬은 root 권한이 필요하므로, 몇 가지 중요한 세부 사항을 알고 있어야 합니다.
우선, 신뢰할 수 있는 사용자만 Docker 데몬을 제어하도록 허용해야 해요. 이는 Docker의 강력한 기능 몇 가지에서 비롯된 직접적인 결과입니다. 구체적으로 Docker는 호스트와 게스트 컨테이너 간에 디렉터리를 공유할 수 있게 해주는데, 이때 컨테이너의 접근 권한을 제한하지 않고 공유할 수 있어요. 즉, 컨테이너 안의 /host 디렉터리가 호스트의 / 디렉터리가 되도록 컨테이너를 시작할 수 있고, 컨테이너가 호스트 파일시스템을 제한 없이 변경할 수 있다는 뜻이에요. 이는 가상화 시스템이 파일시스템 리소스 공유를 허용하는 방식과 비슷합니다. 루트 파일시스템(또는 루트 블록 디바이스)을 가상 머신과 공유하는 것을 막을 방법은 없어요.
여기에는 강력한 보안 함의가 있어요. 예를 들어 웹 서버에서 API를 통해 컨테이너를 프로비저닝하도록 Docker를 연동한다면, 평소보다 훨씬 신중하게 파라미터 검증을 해야 해요. 악의적인 사용자가 조작된 파라미터를 전달해 Docker가 임의의 컨테이너를 생성하게 만들 수 없도록 말이죠.
이런 이유로 Docker CLI가 Docker 데몬과 통신할 때 사용하는 REST API 엔드포인트는 Docker 0.5.2에서 변경되어, 이제 127.0.0.1에 바인딩된 TCP 소켓 대신 Unix 소켓을 사용해요. (기존 TCP 소켓 방식은 VM 밖에서 로컬 머신에 Docker를 직접 실행할 때 CSRF(교차 사이트 요청 위조) 공격에 취약했어요.) 이제 전통적인 Unix 권한 검사를 사용해 컨트롤 소켓에 대한 접근을 제한할 수 있습니다.
명시적으로 결정한다면 REST API를 HTTP로 노출할 수도 있어요. 하지만 그렇게 한다면 앞서 언급한 보안 함의를 반드시 인지하고 있어야 해요. 네트워크의 다른 호스트에서 REST API 엔드포인트로의 접근을 제한하는 방화벽이 있더라도, 엔드포인트는 컨테이너에서 여전히 접근 가능할 수 있고, 쉽게 권한 상승으로 이어질 수 있어요. 따라서 API 엔드포인트를 HTTPS와 인증서로 보호하는 것이 필수입니다. TLS 없이 HTTP로 데몬 API를 노출하는 것은 허용되지 않으며, 그런 구성은 데몬이 시작 시 조기에 실패하게 만들어요. Unauthenticated TCP connections 문서를 참고하세요. 또한 신뢰할 수 있는 네트워크나 VPN에서만 접근 가능하도록 하는 것도 권장합니다.
TLS 대신 SSH를 선호한다면 DOCKER_HOST=ssh://USER@HOST 또는 ssh -L /path/to/docker.sock:/var/run/docker.sock을 사용할 수도 있어요.
데몬은 다른 입력에도 취약할 수 있어요. docker load로 디스크에서 이미지를 로드하거나 docker pull로 네트워크에서 이미지를 가져오는 경우가 그 예시죠. Docker 1.3.2부터 이미지는 Linux/Unix 플랫폼에서 chroot된 하위 프로세스에서 추출되는데, 이는 권한 분리를 위한 더 넓은 노력의 첫 단계예요. Docker 1.10.0부터는 모든 이미지가 내용의 암호화 체크섬으로 저장되고 접근되므로, 공격자가 기존 이미지와 충돌을 일으킬 가능성이 제한됩니다.
마지막으로, 서버에서 Docker를 실행한다면 서버에서 Docker만 전용으로 실행하고 다른 모든 서비스는 Docker가 제어하는 컨테이너 안으로 옮기는 것을 권장해요. 물론 즐겨 쓰는 관리 도구(아마도 SSH 서버 정도)와 NRPE, collectd 같은 기존 모니터링/감독 프로세스를 유지하는 것은 괜찮습니다.
Linux 커널 역량(capabilities)
기본적으로 Docker는 제한된 역량 세트로 컨테이너를 시작해요. 그게 무슨 뜻일까요?
역량(capabilities)은 이진적인 "root/비-root" 구분을 세밀한 접근 제어 시스템으로 바꿔줍니다. 1024 이하 포트에 바인딩만 하면 되는 프로세스(웹 서버 같은)는 root로 실행될 필요 없이 net_bind_service 역량만 부여받으면 돼요. 그리고 root 권한이 보통 필요한 거의 모든 특정 영역에 대해 수많은 다른 역량들이 존재합니다. 이는 컨테이너 보안에 큰 의미가 있어요.
일반적인 서버는 SSH 데몬, cron 데몬, 로깅 데몬, 커널 모듈, 네트워크 설정 도구 등 여러 프로세스를 root로 실행해요. 컨테이너는 달라요. 이런 작업의 거의 전부가 컨테이너 주변의 인프라가 처리하기 때문이에요.
- SSH 접근은 일반적으로 Docker 호스트에서 실행되는 단일 서버가 관리해요
cron은 필요할 때 플랫폼 전체 기능보다는 스케줄링 서비스가 필요한 앱을 위해 맞춤화된 전용 사용자 프로세스로 실행해야 해요- 로그 관리도 일반적으로 Docker나 Loggly, Splunk 같은 서드파티 서비스에 맡겨요
- 하드웨어 관리는 관련이 없어서 컨테이너 안에서
udevd나 이에 상응하는 데몬을 실행할 필요가 전혀 없어요 - 네트워크 관리는 컨테이너 밖에서 이루어지며 관심사 분리를 최대한 강화해요. 즉 컨테이너는
ifconfig,route,ip명령을 수행할 필요가 없어야 해요 (컨테이너가 특별히 라우터나 방화벽처럼 동작하도록 설계된 경우는 물론 예외)
즉, 대부분의 경우 컨테이너는 "진짜" root 권한이 전혀 필요하지 않아요. 따라서 컨테이너는 축소된 역량 세트로 실행될 수 있고, 컨테이너 안의 "root"는 실제 "root"보다 훨씬 적은 권한을 가지게 됩니다. 예를 들어 다음과 같은 것이 가능해요.
- 모든 "마운트" 작업 거부
- 원시 소켓 접근 거부(패킷 스푸핑 방지)
- 새 디바이스 노드 생성, 파일 소유자 변경, 속성 변경(불변 플래그 포함) 같은 일부 파일시스템 작업 거부
- 모듈 로딩 거부
이는 침입자가 컨테이너 안에서 root로 권한 상승에 성공하더라도 심각한 피해를 입히거나 호스트로 권한을 상승시키는 것이 훨씬 어렵다는 뜻이에요.
이 기능은 일반적인 웹 앱에는 영향을 주지 않지만, 악의적인 사용자의 공격 벡터를 상당히 줄여줍니다. 기본적으로 Docker는 필요한 역량을 제외한 모든 역량을 제거하는데, 차단 목록 방식이 아닌 허용 목록 방식을 사용해요. 사용 가능한 전체 역량 목록은 Linux manpages에서 확인할 수 있어요.
Docker 컨테이너를 실행할 때의 주요 위험 중 하나는 컨테이너에 주어지는 기본 역량 세트와 마운트가 불완전한 격리를 제공할 수 있다는 점이에요. 단독으로든, 커널 취약점과 결합되었을 때든 말이죠.
Docker는 역량의 추가와 제거를 모두 지원하므로 기본이 아닌 프로필을 사용할 수 있어요. 역량을 제거하면 Docker를 더 안전하게 만들 수 있고, 역량을 추가하면 덜 안전해질 수 있어요. 사용자에게 가장 좋은 방법은 프로세스에 명시적으로 필요한 역량을 제외한 모든 역량을 제거하는 것입니다.
Docker Content Trust 서명 검증
Docker Engine은 서명된 이미지만 실행하도록 구성할 수 있어요. Docker Content Trust 서명 검증 기능은 dockerd 바이너리에 직접 내장되어 있으며, Dockerd 구성 파일에서 설정합니다.
이 기능을 활성화하려면 daemon.json에서 트러스트 고정(trustpinning)을 구성할 수 있어요. 이 경우 사용자가 지정한 루트 키로 서명된 저장소만 풀하고 실행할 수 있습니다.
이 기능은 관리자에게 CLI로 이미지 서명 검증을 강제하고 수행할 때보다 더 많은 통찰력을 제공해요.
Docker Content Trust 서명 검증 구성에 대한 자세한 내용은 Content trust in Docker를 참고하세요.
기타 커널 보안 기능
역량은 현대 Linux 커널이 제공하는 많은 보안 기능 중 하나일 뿐이에요. TOMOYO, AppArmor, SELinux, GRSEC 등 이미 잘 알려진 시스템을 Docker와 함께 활용하는 것도 가능합니다.
Docker는 현재 역량만 활성화할 뿐 다른 시스템을 방해하지 않아요. 즉 Docker 호스트를 하드닝하는 방법은 매우 다양하다는 뜻이에요. 몇 가지 예를 들어볼게요.
- GRSEC와 PAX가 포함된 커널을 실행할 수 있어요. 이는 컴파일 타임과 런타임 모두에 많은 안전 검사를 추가하고, 주소 무작위화 같은 기법 덕분에 많은 익스플로잇을 무력화해요. 이런 보안 기능은 컨테이너와 무관하게 시스템 전체에 적용되므로 Docker 특화 구성이 필요하지 않아요.
- 배포판에 Docker 컨테이너용 보안 모델 템플릿이 있다면 바로 사용할 수 있어요. 예를 들어 Docker는 AppArmor와 함께 동작하는 템플릿을 제공하고, Red Hat은 Docker용 SELinux 정책을 제공해요. 이 템플릿들은 역량과 크게 겹치긴 하지만 추가적인 안전망을 제공합니다.
- 선호하는 접근 제어 메커니즘으로 자신만의 정책을 정의할 수도 있어요.
특수 네트워크 토폴로지나 공유 파일시스템 등 서드파티 도구로 Docker 컨테이너를 확장할 수 있는 것처럼, Docker 자체를 수정하지 않고 Docker 컨테이너를 하드닝하는 도구도 존재합니다.
Docker 1.10부터 User Namespaces가 docker 데몬에서 직접 지원돼요. 이 기능은 컨테이너의 root 사용자를 컨테이너 외부의 uid-0이 아닌 사용자에 매핑할 수 있게 해주며, 컨테이너 탈출(container breakout) 위험을 완화하는 데 도움이 됩니다. 이 기능은 사용 가능하지만 기본적으로 활성화되지는 않아요.
이 기능에 대한 자세한 내용은 명령줄 참조의 daemon command를 확인하세요. Docker에서 User Namespaces 구현에 대한 추가 정보는 이 블로그 게시물에서 찾을 수 있어요.
결론
Docker 컨테이너는 기본적으로 꽤 안전해요. 특히 컨테이너 안에서 프로세스를 비특권 사용자로 실행한다면 더욱 그렇습니다.
AppArmor, SELinux, GRSEC 또는 다른 적절한 하드닝 시스템을 활성화하면 보안 계층을 하나 더 추가할 수 있어요.
Docker를 더 안전하게 만들 방법이 생각난다면, 기능 요청, 풀 리퀘스트, 또는 Docker 커뮤니티 포럼의 의견을 환영합니다.
관련 정보
- 신뢰할 수 있는 이미지 사용
- Docker용 Seccomp 보안 프로필
- Docker용 AppArmor 보안 프로필
- 컨테이너 보안에 관하여 (2014)
- Docker 스웜 모드 오버레이 네트워크 보안 모델