컨테이너 보안 · 최소 권한 · 이론
컨테이너 안의 root 는 호스트의 root 다
한 줄 요약
유저 네임스페이스를 켜지 않았다면 **컨테이너의 root 는 호스트의 root 와 같은
UID 0** 입니다. "컨테이너는 격리돼 있으니 안에서 root 여도 된다"는 말은 틀렸습니다.
왜 이게 필요했나
대부분의 공식 이미지가 아직도 root 로 뜹니다. docker run --rm node:22-bookworm-slim id
를 쳐 보면 uid=0(root) 가 나옵니다. 여기까지는 놀랍지 않은데, 다음 한 줄이
문제입니다.
docker run --rm -v /etc:/host-etc alpine:3.20 sh -c 'echo "# injected" >> /host-etc/hosts'이게 그대로 먹습니다. 호스트의 /etc/hosts 가 수정됩니다. 컨테이너가 상자였다면
불가능한 일이지만, 앞 코스에서 본 대로 컨테이너는 상자가 아니라 시야가 제한된
프로세스이고, 마운트로 뚫린 경로에는 그 프로세스의 UID 0 권한이 그대로 적용
됩니다. 격리가 깨진 것이 아니라 처음부터 그렇게 설계된 것입니다.
어떻게 동작하나
정석은 이미지 자체를 비특권 사용자로 만드는 것입니다.
RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin appCOPY --chown=10001:10001 . /appUSER 10001:10001이름이 아니라 숫자 UID 여야 하는 이유가 있습니다. 쿠버네티스의runAsNonRoot 는 이미지의 USER 가 이름이면 root 인지 아닌지 판별하지 못하고
파드를 거부합니다.
Error: container has runAsNonRoot and image has non-numeric user (app),cannot verify user is non-root비특권 사용자는 1024 미만 포트를 열 수 없습니다. 여기서 NET_BIND_SERVICE
capability 를 더하고 싶어지는데, 대개는 **앱 포트를 8080 으로 바꾸고 서비스
계층에서 80 으로 노출**하는 편이 훨씬 단순합니다. 권한을 더하는 선택은 마지막에
두세요.
capability 는 root 권한을 40여 조각으로 쪼갠 것이고, 컨테이너 기본 세트는 그중
14개입니다(CapEff: 00000000a80425fb). cap_chown, cap_dac_override,
cap_fowner, cap_setuid, cap_setgid, cap_net_bind_service, cap_net_raw,
cap_sys_chroot, cap_mknod, cap_setfcap 등이 들어 있고 SYS_ADMIN, NET_ADMIN,
SYS_PTRACE 는 빠져 있습니다.
권장 조합은 이렇습니다.
--cap-drop=ALL --cap-add=NET_BIND_SERVICE--security-opt no-new-privileges:true--read-only --tmpfs /tmp--user 10001:10001seccomp 기본 프로파일은 400여 개 시스템 콜 중 약 44개를 막습니다. 그 안에
컨테이너 탈출에 실제로 쓰인 open_by_handle_at, 그리고 keyctl, kexec_load
가 포함돼 있습니다.
현장에서 만나는 모습
--privileged 는 위 보호를 한 번에 전부 해제하는 스위치입니다. 그리고
도커 소켓을 마운트하는 것도 사실상 같은 의미입니다. 그 소켓이 있으면 호스트
루트 파일시스템을 마운트한 특권 컨테이너를 새로 띄울 수 있기 때문입니다. CI 러너
설정에서 이 두 가지를 습관처럼 켜 두는 경우가 많은데, 그 순간 나머지 보안 설정은
장식이 됩니다.
실제 사고도 이 그림 위에 있습니다. CVE-2019-5736 은 /proc/self/exe 를 통해
호스트의 runc 바이너리를 덮어썼고, CVE-2024-21626 은 runc 가 파일 디스크립터를
닫지 않은 탓에 WORKDIR 조작만으로 호스트 파일시스템에 접근할 수 있었습니다.
둘 다 커널 버그가 아니라 런타임 버그였지만, 컨테이너가 호스트와 같은 커널
아래 있기 때문에 성립했습니다.
다음 실습에서 할 것
기본 이미지가 root 로 뜨는 것을 확인하고, 런타임 플래그와 Dockerfile 두 가지
방법으로 비특권 실행을 만들고, 읽기 전용 루트와 capability 전면 제거,
권한 상승 차단을 하나씩 겁니다. 마지막에는 이 조건을 모두 만족하는 이미지를
직접 만듭니다.