Dockerfile에 비밀이 새고 있습니다 - 99%가 모르는 레이어 보안 함정
4분, 그리고 모든 게 끝났다
최근 동료 하나가 금요일 하루 종일 14개의 프로덕션 자격 증명을 교체하느라 고생했다는 얘기를 들었습니다. 원인은 Docker 이미지에 숨어있던 API 토큰이었는데, 정작 그 친구는 토큰을 '제대로' 삭제했다고 확신하고 있었거든요.
공개 Docker 이미지에 자격 증명이 노출되고 악용되기까지 걸리는 평균 시간이 4분이라는 통계가 있습니다. 4시간도 4일도 아닙니다. 고작 4분.
문제는 대부분의 개발자가 Docker 이미지를 zip 파일처럼 생각한다는 점입니다. 실제로는 투명한 레이어가 겹쳐진 구조인데 말이죠.

완벽해 보이는 실수
다음 Dockerfile을 보면 어떤 문제가 있는지 바로 눈에 띄시나요?
FROM node:20-alpine
COPY . .
RUN npm install
RUN echo "$NPM_TOKEN" > ~/.npmrc && npm install && rm ~/.npmrc
CMD ["node", "server.js"]
언뜻 보면 완벽합니다. 토큰을 생성하고, 사용하고, 삭제까지 했으니까요. 하지만 Docker는 그렇게 작동하지 않습니다.
각각의 RUN 명령어는 새로운 불변 레이어를 생성합니다. .npmrc 파일이 존재했던 그 레이어는 이미지 안에 영구적으로 동결됩니다. rm 명령어는 단지 최종 파일시스템 뷰에서만 파일을 제거할 뿐, 그 아래 레이어는 그대로 남아있죠.
30초 안에 확인하는 방법
지금 당장 여러분이 빌드한 이미지에서 이걸 실행해보세요:
docker history --no-trunc my-app:latest
또는 더 직접적으로:
docker save my-app:latest | tar -xO | strings | grep -i "token\|secret\|password"
출력 결과와 잠시 마주앉아 보시길 권합니다. 레지스트리에 대한 접근 권한이 있는 사람이라면 누구나 표준 Docker 명령어만으로 여러분의 자격 증명을 찾을 수 있거든요.
레이어 구조를 이해하기
Docker 이미지는 다음과 같은 레이어 구조를 갖습니다:
Layer 0: 베이스 이미지 (깨끗함)
Layer 1: COPY . . (소스 코드)
Layer 2: RUN npm install (node_modules 생성)
Layer 3: RUN echo $TOKEN > .npmrc (토큰이 여기서 동결됨)
Layer 4: rm ~/.npmrc (삭제는 새로운 레이어에서)
-----------------------------------------------
최종 이미지에는 .npmrc가 보이지 않지만, Layer 3은 그대로 존재
삭제 작업은 비밀이 담긴 레이어 위에 또 다른 레이어를 만들 뿐입니다. 둘 다 이미지에 영구적으로 포함되죠.

올바른 해결책: BuildKit 비밀
Docker BuildKit은 정확히 이 문제를 위한 --mount=type=secret 플래그를 제공합니다. 빌드 타임에 비밀을 마운트하고, 실행 중인 프로세스에서 사용할 수 있게 하되, 어떤 레이어에도 흔적을 남기지 않습니다.
# syntax=docker/dockerfile:1
FROM node:20-alpine
RUN --mount=type=secret,id=npm_token \
cp /run/secrets/npm_token ~/.npmrc && \
npm install && \
rm ~/.npmrc
CMD ["node", "server.js"]
빌드할 때는 이렇게:
docker build \
--secret id=npm_token,src=.npmrc \
-t my-app:latest .
이후 docker history를 실행해보면 토큰이 완전히 사라진 걸 확인할 수 있습니다. 레이어 아래 숨겨진 게 아니라, 이미지 매니페스트에서 아예 없어진 거죠.
대안: 멀티스테이지 빌드
빌드 플래그 없이 더 직관적인 방법을 원한다면 멀티스테이지 빌드도 이 문제를 해결합니다:
FROM node:20-alpine AS builder
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
RUN npm install
FROM node:20-alpine AS runner
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "server.js"]
최종 이미지는 runner 스테이지에서만 빌드됩니다. builder의 레이어 히스토리는 전혀 상속받지 않죠. 복사되는 건 파일뿐이고, 비밀도 히스토리도 과거도 아닙니다.

지금 당장 확인해야 할 것들
- 가장 자주 배포하는 Dockerfile을 열어보세요
- "token", "key", "password", "secret"이 포함된 RUN 명령어를 찾아보세요
--mount=type=secret를 사용하지 않는다면 취약점이 존재합니다docker history --no-trunc로 빌드된 이미지를 점검해보세요
이건 이론적인 위험이 아닙니다. 2023년 CircleCI 침해 사건은 빌드 환경의 비밀을 대규모로 노출시켜 수천 개 팀이 일요일 밤 긴급 자격 증명 교체에 나서게 했거든요. 보안 연구원들도 정기적으로 공개 레지스트리를 스크랩해서 이미지 레이어에서 자격 증명을 가져가고 있습니다.
Docker 23부터는 BuildKit이 기본 활성화됩니다. 이전 버전이라면 빌드 전에 DOCKER_BUILDKIT=1 환경 변수만 설정하면 되고요.
두 해결책 모두 10분 이내로 적용 가능합니다. 하지만 이미 몇 달간 열려있었을 노출을 막는 일이죠.
Dockerfile 자체는 처음부터 올바르게 작성된 게 맞습니다. 삭제 로직도 있고, 논리도 타당하거든요. 다만 개발자의 머릿속 모델과 Docker가 실제로 이미지에 저장하는 방식 사이의 간극에서 자격 증명이 새어나가고 있을 뿐입니다.