안녕하세요! 컨테이너 기술의 핵심인 도커(Docker), 다들 잘 사용하고 계신가요? 🐳 애플리케이션을 가볍고 빠르게 배포할 수 있어 정말 편리하지만, 우리가 작성하는 Dockerfile에 작은 실수 하나가 큰 보안 위협으로 이어질 수 있다는 사실, 알고 계셨나요?
오늘은 Aqua Security에서 지적하는 Dockerfile에서 발생하기 쉬운 주요 보안 취약점들을 하나씩 살펴보며, 어떻게 하면 더 안전하고 효율적인 도커 이미지를 만들 수 있는지 알아보겠습니다. 이 글을 끝까지 읽으시면 여러분의 Dockerfile을 한 단계 더 업그레이드할 수 있을 거예요! 🚀

👤 1. 사용자 및 권한 관리 (User and Permission Management)
컨테이너 내부의 권한 설정은 보안의 첫걸음입니다. 최소한의 권한만 부여하는 것이 핵심이죠.
Least Privilege User (최소 권한 사용자 원칙)
- 위험성: 기본적으로 컨테이너는 root 사용자 권한으로 실행됩니다. 만약 컨테이너가 탈취당할 경우, 공격자는 root 권한을 갖게 되어 호스트 시스템에까지 심각한 피해를 줄 수 있습니다.
- 나쁜 예시 👎
FROM ubuntu:22.04 WORKDIR /app COPY . . # USER 명령어 없이 root 권한으로 실행됨 CMD ["./start.sh"] - 좋은 예시 👍
FROM ubuntu:22.04 WORKDIR /app COPY . . # 1. 전용 사용자 및 그룹 생성 RUN groupadd -r myuser && useradd -r -g myuser myuser # 2. 생성한 사용자로 전환 USER myuser CMD ["./start.sh"] - 설명: USER 명령어를 사용해 root가 아닌 일반 사용자를 생성하고 해당 사용자로 프로세스를 실행하면, 공격자가 컨테이너에 침투하더라도 권한이 제한되어 피해를 최소화할 수 있습니다.
No Sudo Run (Sudo 사용 금지)
- 위험성: Dockerfile의 RUN 명령어에서 sudo를 사용하는 것은 보통 컨테이너가 root가 아닌 사용자로 실행 중일 때 권한을 상승시키려는 시도입니다. 이는 '최소 권한 원칙'을 위배하며, 불필요한 권한 상승은 보안에 좋지 않습니다.
- 나쁜 예시 👎
USER myuser RUN sudo apt-get update && sudo apt-get install -y curl - 좋은 예시 👍
# 필요한 패키지는 root 권한일 때 미리 설치 RUN apt-get update && apt-get install -y curl USER myuser - 설명: 모든 패키지 설치는 USER를 전환하기 전, root 권한일 때 미리 끝내야 합니다. 일반 사용자로 전환한 후에는 sudo 없이 해당 사용자의 권한에 맞는 작업만 수행하도록 설계해야 합니다.
🤫 2. 민감 정보 및 호스트 접근 제어 (Secrets and Host Access Control)
비밀번호, API 키와 같은 민감 정보나 호스트 시스템의 중요한 디렉토리는 절대 컨테이너에 노출되어서는 안 됩니다.
Do Not Pass Secrets (민감 정보 전달 금지)
- 위험성: ENV 명령어나 ARG를 사용해 비밀번호, API 키 등을 Dockerfile에 직접 하드코딩하면, docker history 명령어로 누구나 해당 정보를 쉽게 확인할 수 있습니다.
- 나쁜 예시 👎
ENV DB_PASSWORD="my-secret-password" # 절대 금물! - 좋은 예시 👍
# 방법 1: 빌드 시점에 --secret 옵션 사용 (BuildKit) # Dockerfile RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret # 빌드 명령어 # DOCKER_BUILDKIT=1 docker build --secret id=mysecret,src=mysecret.txt . # 방법 2: 런타임에 환경 변수 또는 볼륨으로 주입 # docker run -e DB_PASSWORD="my-secret-password" my-image # docker run -v /path/to/secrets:/run/secrets my-image - 설명: 민감 정보는 Dockerfile에 남기지 말고, BuildKit의 secret 마운트를 사용하거나 컨테이너 실행 시점(runtime)에 환경 변수, Docker Secrets, 볼륨 등을 통해 안전하게 전달해야 합니다.
Avoid Sys Workdir Mounts (시스템 디렉토리 마운트 회피)
- 위험성: /sys, /proc 등 호스트의 민감한 시스템 파일 시스템을 컨테이너의 작업 디렉토리(WORKDIR)로 설정하거나 직접 마운트하는 것은 매우 위험합니다. 컨테이너가 호스트 시스템의 커널 정보에 직접 접근하고 조작할 수 있게 됩니다.
- 나쁜 예시 👎
WORKDIR /sys/kernel/debug - 좋은 예시 👍
WORKDIR /app # 애플리케이션 전용 디렉토리 사용 - 설명: WORKDIR은 /app이나 /usr/src/app처럼 애플리케이션을 위한 격리된 경로로 지정해야 합니다.
No Ssh Port (SSH 포트 사용 금지)
- 위험성: 컨테이너에 SSH 데몬을 설치하고 22번 포트를 노출하는 것은 일반적인 안티패턴입니다. 공격 표면을 넓히고, 관리 또한 복잡해집니다.
- 나쁜 예시 👎
RUN apt-get install -y openssh-server EXPOSE 22 CMD ["/usr/sbin/sshd", "-D"] - 좋은 예시 👍
# 컨테이너에 접근이 필요할 땐 docker exec 사용 docker exec -it <container_id> /bin/bash - 설명: 컨테이너 내부에 접근하여 디버깅하거나 명령을 실행해야 할 때는 docker exec를 사용하는 것이 훨씬 안전하고 간편합니다.
📦 3. 이미지 무결성 및 재현성 (Image Integrity and Reproducibility)
언제 어디서 빌드해도 동일한 결과를 보장하는 것이 중요합니다.
Use Specific Tags (특정 버전 태그 사용)
- 위험성: ubuntu:latest, node:latest와 같은 latest 태그는 새로운 버전이 릴리즈될 때마다 가리키는 이미지가 변경됩니다. 이로 인해 갑자기 빌드가 실패하거나, 예기치 않은 버전의 패키지가 설치되어 장애로 이어질 수 있습니다.
- 나쁜 예시 👎
FROM node:latest - 좋은 예시 👍
FROM node:18.17.1-alpine - 설명: node:18.17.1-alpine, ubuntu:22.04 처럼 구체적인 버전 태그를 사용해야 합니다. 이를 통해 빌드의 재현성을 보장하고, 의도치 않은 변경을 막을 수 있습니다.
No Dist Upgrade (배포판 업그레이드 금지)
- 위험성: apt-get dist-upgrade와 같은 명령은 커널이나 시스템 라이브러리의 주요 버전을 변경할 수 있습니다. 이는 베이스 이미지가 의도한 것과 다른 환경을 만들어 호환성 문제를 일으키거나 애플리케이션을 중단시킬 수 있습니다.
- 나쁜 예시 👎
RUN apt-get update && apt-get dist-upgrade -y - 좋은 예시 👍
# 특정 패키지만 업그레이드하거나, 더 최신 버전의 베이스 이미지를 사용 RUN apt-get update && apt-get install --only-upgrade -y my-package # 또는 Dockerfile 자체를 수정 # FROM ubuntu:24.04 - 설명: 베이스 이미지의 배포판 자체를 업그레이드하는 대신, 필요한 패키지만 업데이트하거나 상위 버전의 베이스 이미지를 사용하는 것이 안정적입니다.
Use Copy Over Add (ADD 대신 COPY 사용)
- 위험성: ADD 명령어는 로컬 파일을 복사하는 기능 외에도, URL에서 파일을 다운로드하거나 tar 압축 파일을 자동으로 해제하는 기능이 있습니다. 이러한 '마법' 같은 기능은 예기치 않은 동작을 유발할 수 있어 보안에 취약합니다. (예: Zip Bomb 공격)
- 나쁜 예시 👎
ADD application.tar.gz /app # 자동으로 압축 해제됨 ADD http://example.com/file.txt /app # 원격 다운로드 - 좋은 예시 👍
COPY application.tar.gz /app # 파일 그대로 복사 # 원격 파일이 필요하면 RUN과 curl/wget을 명시적으로 사용 RUN curl -o /app/file.txt http://example.com/file.txt - 설명: 단순히 로컬 파일을 컨테이너로 복사할 때는 기능이 명확한 COPY를 사용하는 것이 좋습니다. 원격 파일을 가져오거나 압축을 해제해야 한다면 RUN 명령어와 curl, tar 같은 도구를 조합하여 명시적으로 실행하는 것이 안전합니다.
✨ 4. 패키지 관리 및 이미지 최적화 (Package Management and Image Optimization)
이미지 크기를 줄이고 불필요한 파일을 제거하는 것은 보안과 효율성 모두에 도움이 됩니다.
No Orphan Package Update (독립적인 패키지 업데이트 금지)
- 위험성: RUN apt-get update와 RUN apt-get install을 별개의 RUN 명령어로 실행하면, update가 포함된 레이어가 캐시될 수 있습니다. 이후 Dockerfile 수정 없이 다시 빌드하면, 캐시된 오래된 패키지 목록을 사용하게 되어 최신 버전이 아닌 오래된 패키지가 설치될 수 있습니다.
- 나쁜 예시 👎
RUN apt-get update RUN apt-get install -y curl - 좋은 예시 👍
RUN apt-get update && apt-get install -y curl - 설명: 패키지 목록 업데이트와 설치는 하나의 RUN 명령어로 묶어야 합니다. 이렇게 하면 항상 최신 패키지 목록을 기준으로 설치가 진행됩니다.
Purge Package Cache (패키지 캐시 정리)
- 위험성: apt-get, yum, apk와 같은 패키지 매니저는 설치 과정에서 패키지 목록과 다운로드한 파일을 캐시로 남깁니다. 이는 이미지에 불필요하게 남아 이미지 크기를 증가시키는 원인이 됩니다.
- 나쁜 예시 👎
RUN apt-get update && apt-get install -y curl - 좋은 예시 👍
# Debian/Ubuntu (apt) RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* # Alpine (apk) RUN apk add --no-cache curl # RHEL/CentOS (yum/dnf) RUN yum install -y curl && yum clean all - 설명: 패키지 설치와 캐시 삭제를 하나의 RUN 명령어 안에서 실행해야 합니다. Alpine의 경우 --no-cache 옵션을 사용하면 편리합니다. (해당되는 취약점: Purge Apk/Dnf/Microdnf/Yum/Zipper Cache)
Use Apt No Install Recommends (불필요한 추천 패키지 설치 방지)
- 위험성: apt-get install은 기본적으로 필수 패키지 외에 '추천' 패키지도 함께 설치합니다. 대부분의 경우 이는 불필요하며, 공격 표면과 이미지 크기만 늘리게 됩니다.
- 나쁜 예시 👎
RUN apt-get update && apt-get install -y curl - 좋은 예시 👍
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* - 설명: --no-install-recommends 옵션을 추가하여 정말로 필요한 최소한의 패키지만 설치하도록 합니다.
✍️ 5. Dockerfile 명령어 모범 사례 (Instruction Best Practices)
Dockerfile을 더 깔끔하고, 예측 가능하며, 효율적으로 만드는 방법들입니다.
Use Workdir Over Cd (cd 대신 WORKDIR 사용)
- 설명: RUN cd /some/dir는 해당 RUN 명령어 내에서만 경로가 유지됩니다. 다음 RUN 명령어에서는 다시 루트(/) 디렉토리로 돌아오죠. 이로 인해 혼란을 유발할 수 있습니다. WORKDIR을 사용하면 이후의 RUN, CMD, ENTRYPOINT, COPY, ADD 명령어에 대한 기준 디렉토리를 영구적으로 변경해주므로 훨씬 명확하고 안정적입니다. (관련 취약점: User Absolute Workdir - WORKDIR은 항상 절대 경로로 지정해야 합니다.)
- 나쁜 예시 👎
RUN cd /app && touch file.txt # 다음 RUN에서는 다시 / 에서 시작 - 좋은 예시 👍
WORKDIR /app RUN touch file.txt # /app/file.txt 에 생성됨
No Maintainer (MAINTAINER 사용 금지)
- 설명: MAINTAINER 명령어는 오래전에 deprecated 되었습니다. 이미지 작성자 정보를 남기고 싶다면 LABEL을 사용하는 것이 표준입니다.
- 나쁜 예시 👎
MAINTAINER dev@example.com - 좋은 예시 👍
LABEL maintainer="dev@example.com"
Only One Cmd / Entrypoint / Healthcheck (CMD, ENTRYPOINT, HEALTHCHECK는 하나만)
- 설명: Dockerfile에는 CMD, ENTRYPOINT, HEALTHCHECK 명령어를 여러 번 작성할 수 있지만, 실제로는 가장 마지막에 작성된 하나만 적용됩니다. 이는 의도치 않은 동작을 유발할 수 있으므로, 각각 하나만 사용해야 합니다.
기타 모범 사례
- Use Specific Tags: latest 대신 python:3.9-slim 처럼 구체적인 태그를 사용해야 빌드의 재현성이 보장됩니다.
- No Duplicate Alias: 멀티 스테이지 빌드에서 FROM base as my_stage 처럼 스테이지 별칭을 중복해서 사용하지 마세요.
- No Self Referencing Copy From: COPY --from은 다른 스테이지를 참조해야지, 자기 자신을 참조해서는 안 됩니다.
- Use Apt Auto Confirm: apt-get install 시 -y 또는 --yes 플래그를 넣어 빌드 중 상호작용 프롬프트가 뜨는 것을 방지해야 합니다.
- Use Slash For Copy Args: COPY source /destination/ 처럼 목적지 경로 끝에 /를 붙여주면, 해당 경로가 디렉토리임을 명확히 하여 파일이 아닌 디렉토리로 복사되도록 보장합니다.
❤️ 6. 컨테이너 상태 및 네트워킹 (Container Health and Networking)
컨테이너가 정상적으로 동작하는지, 네트워크 설정은 올바른지 확인하는 것도 중요합니다.
No Healthcheck (HEALTHCHECK 부재)
- 위험성: HEALTHCHECK가 없으면 도커는 컨테이너의 메인 프로세스가 실행 중인지 여부만 판단합니다. 프로세스는 떠 있지만 내부적으로는 응답이 없거나 '죽은' 상태일 수 있죠. 이 경우 오케스트레이션 도구(예: Kubernetes, Docker Swarm)는 컨테이너가 정상이라고 착각하여 트래픽을 계속 보냅니다.
- 나쁜 예시 👎
# HEALTHCHECK 명령어가 없음 - 좋은 예시 👍
FROM nginx # 5초마다 nginx가 응답하는지 확인 HEALTHCHECK --interval=5s --timeout=3s \ CMD curl -f http://localhost/ || exit 1 - 설명: HEALTHCHECK를 설정하면 도커 엔진이 주기적으로 컨테이너 내부의 상태를 확인하여, 비정상일 경우 'unhealthy'로 표시해줍니다. 이를 통해 자동 복구 및 무중단 배포가 가능해집니다.
Port Out Of Range (유효하지 않은 포트 번호)
- 위험성: TCP/IP 포트 번호는 0에서 65535 사이의 값만 유효합니다. 이 범위를 벗어나는 포트 번호를 EXPOSE에 사용하면 아무런 효과가 없으며, 설정 오류입니다.
- 나쁜 예시 👎
EXPOSE 80000 - 좋은 예시 👍
EXPOSE 8080 - 설명: EXPOSE에는 반드시 유효한 범위 내의 포트 번호를 사용해야 합니다.
마치며
지금까지 Dockerfile을 작성할 때 흔히 저지르는 실수와 보안 취약점들을 살펴보았습니다. 오늘 배운 내용들을 바탕으로 여러분의 Dockerfile을 다시 한번 점검해보는 것은 어떨까요?
이러한 규칙들을 일일이 기억하기 어렵다면 hadolint와 같은 Dockerfile 정적 분석 도구(linter)를 사용하는 것을 강력히 추천합니다. 코드를 작성하면서 실시간으로 문제점을 찾아주기 때문에 훨씬 편리하게 안전한 이미지를 만들 수 있답니다.
안전한 컨테이너 환경을 만드는 첫걸음, 바로 여러분의 Dockerfile에서 시작됩니다! 💪
태그: 도커, Docker, Dockerfile, 보안, 컨테이너, 데브옵스, CI/CD, 이미지 최적화, best practice, hadolint
'일반IT > IT보안' 카테고리의 다른 글
| 🚨 웹 개발자 필독! 최신 침해 사고 사례로 알아보는 보안 위협 동향 (0) | 2025.08.21 |
|---|---|
| 잊혀지지 않을 도커의 경고: 역사상 가장 치명적이었던 CVE 취약점 Top 3 📜 (0) | 2025.08.21 |
| 🌐 우리나라에서 Web-WAS-DB 인프라를 분리해야 하는 이유: 법적 근거와 기술적 중요성 (1) | 2025.08.19 |
| CCTV 없는 집, 도둑이 들어도 모른다! OWASP A10:2017 - 불충분한 로깅 및 모니터링 (4) | 2025.08.16 |
| 🧱 튼튼한 집도 주춧돌이 썩었다면? OWASP A9:2017 - 알려진 취약점이 있는 구성요소 사용 (5) | 2025.08.16 |