Docker cap-drop/add: Linux Capability 최소화
Linux Capability 개념, 컨테이너 기본 capability 집합, --cap-drop ALL로 전체 제거 후 필요한 것만 추가하는 최소 권한 패턴, 앱 유형별 권장 설정을 실전 예제와 함께 설명합니다.
지난 글에서 파일 시스템 불변성으로 공격자의 파일 변조를 막는 방법을 다뤘다. 이번에는 Linux Capability를 다룬다. 컨테이너가 커널에게 요청할 수 있는 권한 목록을 최소화하면 exploit이 성공하더라도 할 수 있는 일이 제한된다.
Linux Capability란
전통적인 Unix 권한 모델은 root(UID 0)와 non-root로 이분된다. root는 모든 권한을 갖는다. Linux Capability는 이 “모든 권한”을 약 40개의 독립된 단위로 쪼갠다.
# 현재 프로세스의 capability 확인
cat /proc/1/status | grep Cap
# CapInh: 00000000000005fb
# CapPrm: 00000000000005fb
# CapEff: 00000000000005fb
# capability 비트마스크 해석
capsh --decode=00000000000005fb
# → cap_chown, cap_dac_override, cap_fowner, cap_kill, cap_net_bind_service...
컨테이너 기본 Capability
Docker는 컨테이너에 약 14개의 기본 capability를 부여한다. 전체 호스트 root 권한(~40개)보다 적지만 여전히 일부는 위험하다.
NET_RAW는 raw 소켓을 열어 ARP 스푸핑이나 ICMP 플러딩에 악용될 수 있다. MKNOD는 디바이스 노드를 만들 수 있어 /dev/sda 같은 블록 장치 접근이 가능해진다. 대부분의 웹 앱은 이 두 capability가 필요 없다.
—cap-drop과 —cap-add
# 특정 capability 제거
docker run --cap-drop=NET_RAW myapp
# 여러 개 제거
docker run --cap-drop=NET_RAW --cap-drop=MKNOD --cap-drop=AUDIT_WRITE myapp
# 전체 제거
docker run --cap-drop=ALL myapp
# 전체 제거 후 필요한 것만 추가 (권장 패턴)
docker run --cap-drop=ALL --cap-add=CHOWN --cap-add=SETUID myapp
# 현재 컨테이너의 effective capabilities 확인
docker exec mycontainer sh -c "cat /proc/1/status | grep CapEff"
현재 컨테이너 capability 확인
# capsh로 capability 목록 확인
docker exec mycontainer sh -c \
"apk add -q libcap && capsh --print 2>/dev/null | grep 'Current:'"
# 또는 /proc 직접 확인
docker exec mycontainer sh -c \
"cat /proc/1/status | grep Cap"
# inspect로 추가/삭제된 capability 확인
docker inspect mycontainer | \
python3 -c "import sys,json; c=json.load(sys.stdin)[0]; \
print('Add:', c['HostConfig']['CapAdd']); \
print('Drop:', c['HostConfig']['CapDrop'])"
최소 권한 패턴: —cap-drop=ALL
가장 안전한 접근법은 전체 capability를 제거한 후 필요한 것만 추가하는 것이다.
# 비루트 사용자, 고번호 포트 앱 — capability 불필요
docker run \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--user=1000:1000 \
--read-only \
myapp:latest
앱 유형별 최소 Capability
일반 웹 앱 (비루트 + 8080 포트)
services:
web:
image: myapp:latest
user: "1000:1000"
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
비루트 사용자와 고번호 포트를 사용하면 capability가 전혀 필요 없다.
Nginx (80 포트)
services:
nginx:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 80 포트 바인딩
- CHOWN # 파일 소유자 변경
- SETUID # worker 프로세스 UID 변경
- SETGID # worker 프로세스 GID 변경
- DAC_OVERRIDE # 파일 권한 우회 (일부 버전 필요)
cron을 사용하는 앱
docker run \
--cap-drop=ALL \
--cap-add=SETUID \
--cap-add=SETGID \
--cap-add=SYS_NICE \ # cron: nice 값 조정
myapp-with-cron
네트워크 진단 도구 포함
docker run \
--cap-drop=ALL \
--cap-add=NET_RAW \ # ping, traceroute
network-tools:latest
privileged 플래그는 사용하지 않는다
# 절대 하지 말 것
docker run --privileged myapp
# --privileged는 모든 capability를 부여하고
# 디바이스 접근, AppArmor/Seccomp 비활성화까지 한다
# → 컨테이너 탈출 거의 보장된 상황이 됨
--privileged가 필요한 것처럼 보이는 경우, 실제로 어떤 capability가 필요한지 파악해 그것만 추가한다.
Capability 문제 디버깅
# 앱이 "Operation not permitted" 오류를 낼 때
# strace로 어떤 syscall이 실패하는지 확인
docker run --cap-add=SYS_PTRACE myapp strace -e trace=process myapp-binary
# 또는 audit 로그 확인 (호스트에서)
dmesg | grep "avc: denied"
ausearch -m avc -ts recent
특정 Capability 필요 여부 판단
# 프로그램이 실제로 필요한 capability를 파일 속성으로 확인
getcap /usr/bin/ping
# → /usr/bin/ping = cap_net_raw+ep
# 실행 파일에 capability 부여 (capability 추가 대신 파일에 부여)
setcap cap_net_bind_service=+ep /usr/local/bin/myapp
파일에 capability를 부여하면 컨테이너 전체에 --cap-add를 주는 것보다 범위가 작다. 단, Dockerfile에서 RUN setcap ...을 실행하려면 cap_setfcap capability가 필요하다.
Kubernetes SecurityContext 연동
spec:
containers:
- name: web
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
- CHOWN
allowPrivilegeEscalation: false
runAsNonRoot: true
지난 글: Docker 읽기 전용 루트 파일 시스템: —read-only 완전 활용
다음 글: Docker seccomp 프로파일: 허용 syscall 화이트리스트
읽어주셔서 감사합니다. 😊