루트로 돌리지 않는다
RUN addgroup -g 10001 app && adduser -u 10001 -G app -D app
USER 10001:10001
기본은 root 다 — 명시하지 않으면 컨테이너 안 프로세스가 root 로 돈다
왜 위험한가
- 컨테이너 탈출 취약점이 나오면 호스트에서도 root 다
- (User 네임스페이스를 켜지 않았다면 UID 가 같다)
- 볼륨 마운트한 호스트 파일을 마음대로 고칠 수 있다
- 컨테이너 안에서 패키지를 설치하거나 시스템을 바꿀 수 있다
이름이 아니라 '숫자 UID' 로 지정한다 쿠버네티스의 runAsNonRoot 검증이 숫자를 요구한다 이름만 있으면 판정할 수 없다
- 1024 미만 포트는 root 만 bind 할 수 있다
- 컨테이너 안에서 8080 을 쓰고 밖에서 -p 80:8080 으로 매핑한다
읽기 전용 루트
docker run --read-only --tmpfs /tmp myapp
침투해도 바이너리를 심거나 설정을 바꿀 수 없다 쓰기가 필요한 곳만 tmpfs·볼륨으로 연다
어디에 쓰는지 파악해야 하므로 도입에 품이 든다
- docker diff 로 조사하면 목록이 나온다
권한을 좁힌다
docker run \
--cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--read-only \
myapp
- capability — root 권한을 쪼갠 것. 전부 끄고 필요한 것만 켠다
- no-new-privileges — setuid 로 권한이 올라가는 것을 막는다
- seccomp — 허용할 시스템 콜 목록. 기본 프로파일이 적용된다
--privileged 는 사실상 호스트 권한이다
'조금 더 많은 권한' 이 아니다
- 모든 capability
- seccomp·AppArmor 해제
- 호스트 장치 접근
컨테이너 탈출이 어렵지 않아진다 필요한 이유를 설명할 수 없다면 쓰지 않는다 필요하면 그 능력 하나만 --cap-add 로 준다
도커 소켓 마운트는 곧 호스트 장악이다
-v /var/run/docker.sock:/var/run/docker.sock
이 한 줄이면 그 컨테이너는 호스트에 아무 컨테이너나 띄울 수 있다
- --privileged 로 호스트 루트를 마운트한 컨테이너를 만들면 끝이다
CI 러너·모니터링 도구에서 흔히 보이는 설정이다 대안 — 소켓 프록시로 필요한 API 만 노출 · rootless 모드
- DinD 대신 Kaniko·Buildah 로 빌드
비밀을 이미지에 넣지 않는다
남는 경로
-
COPY 로 넣은 설정 파일 (.env · 키 파일)
-
ARG 로 넘긴 값 → docker history 에 명령이 남는다
-
ENV → docker inspect 로 그대로 보인다
-
앞 레이어에 넣고 뒤에서 지운 파일 → 레이어에 남아 있다
-
빌드 시점 — RUN --mount=type=secret
-
런타임 — 환경변수 주입 · 시크릿 매니저 · 파일 마운트
확인
- docker history --no-trunc myapp:1.0
- docker save myapp:1.0 | tar -tv | head -50
런타임 위협 탐지
빌드 시점 방어(스캔·설정)만으로는 실행 중 이상을 못 잡는다
- 예상 못 한 프로세스 실행 (컨테이너 안에서 셸이 떴다)
- 예상 못 한 아웃바운드 연결
- 민감 파일 접근
Falco 같은 도구가 시스템 콜을 관찰해 규칙 위반을 알린다 eBPF 기반이라 애플리케이션 변경 없이 붙는다
"셸이 없는 이미지" 와 조합하면 강력하다 — 셸이 뜨는 것 자체가 이상 신호가 된다
최소 권한 체크리스트
□ USER 를 숫자 UID 로 지정했다 □ --read-only + 필요한 곳만 tmpfs □ --cap-drop=ALL 후 필요한 것만 add □ --security-opt=no-new-privileges □ --privileged 를 쓰지 않는다 □ docker.sock 을 마운트하지 않는다 □ 호스트 경로를 rw 로 마운트하지 않는다 □ 네트워크·PID 네임스페이스를 호스트와 공유하지 않는다
하나씩 켜면서 애플리케이션이 도는지 확인하는 순서로 도입한다 한꺼번에 적용하면 무엇 때문에 깨졌는지 못 찾는다
면접 함정
- ❌ "컨테이너는 격리돼 있으니 root여도 된다" → 탈출 시 호스트 root다.
- ❌ "docker.sock은 읽기 전용으로 주면 안전하다" → API로 무엇이든 만들 수 있다.