← 목록으로

도커 공격 표면 줄이기 - 보안 점수 미달을 계기로 새롭게 공부한 개념들

·11 min read#Docker#Container Security#DevSecOps#Kubernetes

그동안 도커를 공부하고 사용한 경험은 여럿 있다. 하지만 도커의 보안 요소를 따로 정리하거나 공부한 적은 달리 없었다. 사실 나는 그 존재를 그렇게까지 크게 인식하고 지내질 않았다. 솔직하게 이실직고하자면 그게 그렇게 필요하다고 생각하지도 않았다. 어쩌면 다른 이것저것을 공부하느라 바빠 애써 정리할 필요성이 있던 걸 눈 앞에 두고도 외면하던 걸지도 모르겠다.

하지만 최근 이러한 외면을 그만둘 계기가 한 가지 생겼다. 최근 참여하는 프로젝트가 한 가지 있는데, 그 서버에 도커 컨테이너로 띄운 서비스가 몇 가지 있었다. 그런데 고객에게서 해당 서버의 도커 보안 점수가 53.8점으로 표준 미달이니 아무쪼록 관련 보안 조치를 요청한다는 것이었다. 그래서 어차피 이 요청을 내가 일방적으로 거절할 수도 없는지라, 이를 계기로 삼아 몇 가지 도커 보안 개념을 새롭게 공부하고 블로그에 정리해두려고 한다. 차후 컨테이너를 띄울 때는 이렇게 늑장대응할 게 아니라 미리 이러한 보안 방향성에 맞춰 앱 자체도 설계하도록 개발자와 소통하고, DevOps로서 나 역시도 그에 맞춰 서비스를 띄워보고 설계할 수 있도록 말이다.

  1. seccomp — 시스템 콜(syscall) 화이트리스트
docker run --security-opt seccomp=/path/to/custom-profile.json myapp

seccomp는 secure computing mode의 줄임말이다. 이는 리눅스 커널 자체의 기능으로, 프로세스가 호출할 수 있는 시스템 콜을 제한한다. 이러한 제한이 설정되어 있지 않으면 공격자가 컨테이너 내부에서 컨테이너를 탈출하여 호스트에 영향을 줄 수가 있다. 예를 들어 mount 시스템 콜이 열려 있어 호스트 디스크를 컨테이너로 마운트해서 호스트 파일을 읽고 쓰는 경로를 만들거나 ptrace 시스템 콜이 열려 있어 다른 호스트 프로세스의 실행 흐름에 개입하거나 하는 식이다.

  1. apparmor — 강제 접근 제어(MAC) 프로필
# 컨테이너 실행 시 프로필 지정
docker run --security-opt apparmor=docker-default myapp

# 커스텀 프로필 적용
docker run --security-opt apparmor=my-custom-profile myapp

seccompmount 시스템 콜을 호출할 수 있는지 여부를 막는다면, apparmormount 시스템 콜로 어느 리소스까지 접근할 수 있는지를 제한한다. 예를 들어 mount 시스템 콜로 /proc, /sys 등 민감한 경로에는 접근하지 못하도록 차단할 수가 있다. 1차 보안이 seccomp라면 apparmor가 2차 보안인 셈이다.

  1. Linux Capability 최소화

리눅스는 root 권한을 CAP_NET_ADMIN, CAP_SYS_ADMIN, CAP_CHOWN 등 약 40여 개의 세분화된 capability로 나눈다. 컨테이너에 필요하지 않은 과도한 권한의 capability가 부여되면, 침해시 공격자가 해당 권한을 그대로 악용할 수가 있다. 예를 들어 CAP_NET_ADMIN 권한이 있으면, 여러 컨테이너가 동일한 도커 브릿지 네트워크를 사용하는 상황에서 공격자가 명령어로 해당 컨테이너의 트래픽 뿐만이 아니라 같은 브릿지를 타는 다른 컨테이너의 패킷까지 스니핑해갈 수가 있다. 따라서 컨테이너에는 딱 해당 컨테이너에 필요한 capability만을 부여할 필요가 있다.

  1. Docker Content Trust (DOCKER_CONTENT_TRUST)
# 환경변수로 전역 활성화
export DOCKER_CONTENT_TRUST=1

Docker Content Trust는 앞의 3개와는 달리 런타임 단계가 아닌 이미지 공급망 단계의 방어선이다. 이를 사용하면 이미지에 디지털 서명이 적용되고, 레지스트리에서 pull하는 이미지가 신뢰할 수 있는 게시자가 만든 변조되지 않은 이미지인지를 사전 검증할 수가 있다. 예를 들어 공격자가 내부 레지스트리에 침입하여 이미지를 악성 이미지로 바꿔치기했다고 가정해보자. 만약 Docker Content Trust가 설정되어 있지 않다면 태그가 동일하니 이미지가 그냥 pull되고 멀쩡하게 서버에 배포되어 버리고 말 것이다. 반면 Docker Content Trust가 설정되어 있으면 서명이 원본 콘텐츠의 해시와 일치하지 않아 서명 불일치 오류로 이미지 pull이 사전 차단된다.

  1. User Namespace Remap — 컨테이너 root의 격리
// /etc/docker/daemon.json
// 설정 후 sudo systemctl restart docker로 재시작
{
  "userns-remap": "default"
}

기본적으로 컨테이너 내부의 루트는 호스트의 루트와 동일한 UID를 공유한다. 즉 컨테이너 탈출에 성공하면 곧바로 호스트 root 권한을 얻게 될 위험이 있다. userns-remap은 컨테이너 내부 UID/GID를 호스트의 별도 UID/GID 범위로 매핑함으로서 컨테이너 안에서는 root처럼 보여도 호스트에서는 일반 사용자 권한만 갖도록 격리한다.

위 다섯 가지 정도가 내가 적용한 보안 조치 중 가장 크리티컬한 보안 조치 아니었는지 싶다. 이렇게 적용한 조치도 있지만, 아쉽게도 서비스 영향 상 적용하는데 실패한 보안 조치도 몇 가지 있었다. 예를 들자면

  • 읽기 전용 루트 파일시스템 (--read-only)
docker run --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  myapp

컨테이너의 루트 파일시스템을 읽기 전용으로 마운트하여 런타임 중 파일 시스템 변조를 원천 차단하는 옵션이다. 이게 없으면 공격자가 취약점으로 컨테이너 진입 후 루트 파일 시스템에 악성 파일을 생성할 수가 있다. 안타깝게도 나는 이걸 컨테이너 내부에 설정하는데 실패했다. 왜냐하면 우리 어플리케이션이 내부적으로 불특정 경로에다가 파일을 작성하여, 특정 경로만 tmpfs로 지정해두기 운영 서버 특성상 위험했기 때문이었다. 개발 서버에서 충분히 테스트하고, 운영 서버에 반영하면 좋겠지만 GPU를 사용하는 어플리케이션이며, 개발 서버에는 GPU가 없어 그러한 테스트가 불가능했다. 따라서 이것의 적용은 애석하게도 보류했다.

  • userland proxy 비활성화
// /etc/docker/daemon.json
// 설정 후 sudo systemctl restart docker로 재시작
{ 
   "userland-proxy": false 
}

도커는 컨테이너 포트를 호스트에 노출할 때 기본적으로 docker-proxy라는 별도 프로세스로 트래픽을 중계하지만, userland-proxy를 false로 설정해두면 iptables 규칙만으로 포트포워딩이 이뤄지게 된다고. 하지만 이 역시도 적용하는데 실패했다. 왜냐하면 적용하니 어플리케이션이 정상 동작하지 않기 때문이었다. 정확한 이유는 모르겠으나, 서버 내부에 Istio가 있다 보니, IstioDocker가 동시에 iptables 규칙을 건드리며 뭔가 충돌나는 게 아닌가 하고 추정하고 있다. 이 문제를 꾸역꾸역 해결하는 건 수지타산이 맞지 않는 관계로 이는 그냥 적용을 포기했다.

아무튼, 그 외 이런저런 보안 조치를 취해 해당 서버의 도커 보안 점수를 53.8점에서 74점까지 꽤 높게 끌어올렸다. 더 비약적으로 올렸다면 좋겠지만, 서비스 영향으로 안타깝게 쉽지 않았다. 뭐 그래도 보안 관련 개념도 배워가고, 좋은 경험이었다. 향후에는 이러한 상황에 대비하여 사전적으로 서비스를 점검해보는 것도 나쁘지 않을 것 같다.