Docker 보안은 왜 여러 계층이 필요한가
단일 보안 설정으로 충분하지 않은 이유부터 Seccomp·Capabilities·User Namespace·Secrets 관리까지, Docker Defense in Depth의 설계 논리를 추적한다.
- 01 컨테이너는 어떻게 격리되면서도 빠른가
- 02 Docker 이미지 크기가 10배 차이 나는 이유
- 03 Docker 네트워크는 어떻게 패킷을 옮기는가
- 04 Docker 스토리지는 어디서 끝나고 어디서 시작되는가
- 05 Docker Compose에서 Swarm까지, 어떻게 설계가 이어지는가
- 06 Docker 보안은 왜 여러 계층이 필요한가
- 07 Docker 컨테이너 리소스 관리의 철학
- 08 Docker는 어떻게 컨테이너를 만드는가
- 09 컨테이너 패턴의 통일된 철학은 무엇인가
- 10 코드 한 줄이 프로덕션에 닿기까지
- 11 컨테이너 디버깅은 왜 이렇게 어려운가
- 12 Docker로 Full-stack 서비스를 운영한다는 것은 무엇인가
- 13 Docker에서 Kubernetes로 — 무엇이 달라지는가
Docker 컨테이너는 기본 설정으로도 실행된다. 문제는 “실행된다”와 “안전하다”가 전혀 다른 말이라는 점이다. root로 실행 중인 컨테이너, 환경 변수에 박힌 패스워드, 스캔 없이 배포된 이미지 — 이 중 하나만 뚫려도 호스트 전체가 위험해진다. Docker 보안이 단일 설정이 아니라 여러 계층의 조합인 이유는 무엇인가?
공격 표면과 계층 방어의 출발점
컨테이너가 격리된다고 느끼는 이유는 namespace 덕분이다. 하지만 namespace는 프로세스와 네트워크를 분리할 뿐, 커널은 공유한다. 컨테이너 안의 프로세스가 mount 시스템 콜을 호출하거나, /proc/sys에 쓰거나, setuid 바이너리를 실행하면 — 그 영향은 호스트까지 닿는다.
Defense in Depth는 이 공유 커널 위에서 여러 독립적인 방어선을 쌓는 전략이다.
Application Security
Container Runtime (Seccomp)
Image Security (Scan)
Network Isolation
Docker Daemon (TLS)
Host OS (AppArmor/SELinux)
한 계층이 뚫려도 다른 계층이 살아 있다. 각 계층은 서로 다른 공격 벡터를 막는다.
Capabilities와 Seccomp — 커널 접근을 좁힌다
기본 컨테이너는 CAP_NET_RAW(패킷 스니핑), CAP_SYS_CHROOT(chroot 실행), CAP_NET_BIND_SERVICE(1024 미만 포트 바인딩) 등 14개의 capability를 갖는다. 웹 서버 하나를 띄우는 데 이 중 필요한 건 하나뿐이다.
# 모두 제거 후 필요한 것만
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-p 80:80 \
nginx:alpine
Seccomp은 더 낮은 레벨에서 작동한다. 컨테이너가 호출할 수 있는 시스템 콜 자체를 필터링한다. Docker 기본 Seccomp 프로필은 mount, reboot, ptrace 등 44개의 위험한 syscall을 차단한다. 커스텀 프로필을 쓰면 허용 목록을 50여 개로 좁힐 수 있다.
# Seccomp 상태 확인
docker run --rm alpine grep Seccomp /proc/1/status
# Seccomp: 2 (2 = filter 모드 활성화)
--privileged 플래그 하나가 Seccomp, AppArmor, Capabilities 제한을 전부 무력화한다. 호스트의 /dev에 접근하고, 커널 모듈을 로드하고, namespace를 탈출할 수 있게 된다. “일단 돌아가게” 하려고 붙인 이 플래그가 프로덕션에서 살아남는 경우가 생각보다 많다.
AppArmor와 User Namespace — 탈출해도 막는다
Seccomp이 syscall을 막는다면, AppArmor/SELinux는 파일 경로와 네트워크 접근을 커널 레벨에서 강제한다. root 프로세스라도 프로필이 허용하지 않은 경로에는 쓸 수 없다.
# AppArmor 프로필 적용
docker run --security-opt apparmor=docker-restricted alpine \
sh -c 'echo 1 > /proc/sys/kernel/randomize_va_space'
# sh: can't create /proc/sys/kernel/randomize_va_space: Permission denied
User Namespace는 더 근본적인 격리를 제공한다. 컨테이너 안의 UID 0(root)을 호스트에서는 UID 100000(일반 사용자)으로 매핑한다.
컨테이너 내부 → 호스트
UID 0 (root) → UID 100000 (dockremap)
UID 1000 → UID 101000
CVE-2019-5736처럼 runc를 통한 컨테이너 탈출 취약점이 발생했을 때, User Namespace가 활성화된 환경에서는 탈출에 성공해도 호스트에서 일반 사용자 권한밖에 얻지 못한다. 피해 반경이 극적으로 줄어든다.
# daemon.json에서 활성화
{
"userns-remap": "default"
}
이미지 스캐닝 — 배포 전 차단
실행 중인 컨테이너를 아무리 잘 보호해도, 이미지 자체에 CVE-2023-1234가 박혀 있으면 소용없다. Trivy는 OS 패키지와 애플리케이션 라이브러리 취약점을 동시에 스캔한다.
trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:latest
CI/CD 파이프라인에서 --exit-code 1로 Critical/High 취약점 발견 시 빌드 자체를 실패시킨다. “배포 전 차단”이 “배포 후 패치”보다 수정 비용이 10-100배 낮다.
베이스 이미지 선택도 공격 표면에 직접 영향을 준다.
ubuntu:latest → 70MB, 100+ 패키지, 취약점 많음
alpine → 7MB, 14 패키지, 취약점 중간
distroless → 20MB, 0 패키지, 쉘 없음
distroless나 scratch 이미지는 쉘 자체가 없기 때문에 공격자가 docker exec로 진입해도 할 수 있는 일이 거의 없다.
Secrets 관리 — 평문 패스워드가 가장 빠른 침투 경로다
# 이것이 얼마나 위험한지
docker service inspect insecure-db | grep PASSWORD
# "DB_PASSWORD=mysecretpassword" ← 평문 노출
환경 변수는 docker inspect, 프로세스 목록, 로그에서 평문으로 노출된다. Docker Secrets는 Raft 분산 스토리지에 AES-256으로 암호화해 저장하고, 실행 중인 컨테이너에는 tmpfs(메모리)로만 마운트한다.
echo "super-secret" | docker secret create db_password -
docker service create \
--secret db_password \
--env POSTGRES_PASSWORD_FILE=/run/secrets/db_password \
postgres:alpine
파일 경로만 환경 변수로 전달하고, 실제 값은 /run/secrets/db_password에서 읽는다. docker inspect에는 파일 경로만 보인다. 컨테이너 종료 시 자동 삭제된다.
트레이드오프
각 계층은 운영 복잡도를 더한다. User Namespace를 켜면 bind mount 볼륨 권한이 꼬인다(컨테이너 UID 0 → 호스트 UID 100000이므로 호스트 파일 소유권을 맞춰야 한다). Seccomp 커스텀 프로필은 애플리케이션 업데이트 시 새 syscall이 추가되면 깨진다. AppArmor 프로필이 너무 엄격하면 정상 동작이 차단된다. Complain 모드에서 시작해 필요한 권한을 파악한 뒤 Enforce 모드로 전환하는 순서가 안전하다. “보안 100% vs 운영 편의성” 사이의 균형점은 환경마다 다르지만, 기준선은 명확하다 — Seccomp 기본 프로필, --cap-drop=ALL + 필요한 capability만 추가, 비특권 사용자 실행, 이미지 스캔은 협상 불가다.
정리
- Docker 보안은 단일 설정이 아니다. Capabilities, Seccomp, AppArmor/SELinux, User Namespace, Secrets 관리, 이미지 스캔이 각각 다른 공격 벡터를 막는다.
- 컨테이너가 탈출에 성공해도 User Namespace가 있으면 피해는 일반 사용자 권한으로 제한된다.
--privileged, 평문 환경 변수,:latest태그 — 이 세 가지는 보안 계층 대부분을 무력화한다.- 이미지 스캐닝을 CI/CD 파이프라인 게이트로 만들면 Critical 취약점을 배포 전에 차단할 수 있다.
다음 글에서는 Docker 네트워크 격리가 실제로 어떻게 작동하는지, 그리고 컨테이너 간 통신을 제어하는 icc=false와 사용자 정의 네트워크의 차이를 추적한다.