왜 환경변수/빌드 인자로 넣으면 위험한가
가장 흔한 실수는 시크릿을 ENV나 --build-arg로 넣는 것이다. Docker의 ARG와 ENV 값은 이미지 레이어 히스토리에 그대로 박제된다. docker history나 이미지를 pull한 누구든 값을 복원할 수 있고, 중간 레이어에 캐시된 파일도 마찬가지다. CI 로그 역시 위험 지점이다. 마스킹이 걸려 있어도 base64로 인코딩되거나 다른 명령의 출력에 섞이면 평문으로 노출된다. 핵심 원칙은 하나다. 시크릿은 최종 이미지와 로그, 캐시 어디에도 흔적을 남기지 않아야 한다.
방식 비교
| 방식 | 이미지 잔존 | 로그 노출 위험 | 권장 |
|---|---|---|---|
| ENV / build-arg | 남음(history) | 높음 | 금지 |
| 파일 COPY 후 삭제 | 남음(중간 레이어) | 중간 | 금지 |
| BuildKit secret mount | 없음 | 낮음 | 권장 |
| 런타임 시크릿 매니저 | 없음 | 낮음 | 런타임에 권장 |
BuildKit secret mount으로 빌드 타임 주입
BuildKit의 --mount=type=secret은 해당 RUN 스텝이 실행되는 동안에만 tmpfs로 파일을 마운트한다. 스텝이 끝나면 사라지고, 어떤 레이어에도 기록되지 않는다. 예를 들어 사설 패키지 저장소 인증 토큰을 쓰는 경우:
#syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
# 시크릿은 이 RUN 동안만 /run/secrets/pip_token 로 존재한다
RUN --mount=type=secret,id=pip_token,required=true \
PIP_TOKEN=$(cat /run/secrets/pip_token) && \
pip install --index-url "https://__token__:${PIP_TOKEN}@pypi.internal/simple" \
-r requirements.txt
COPY . .
required=true로 두면 시크릿 누락 시 빌드가 조용히 통과하지 않고 실패한다. 값을 ENV로 승격하지 말고, 그 RUN 안에서만 읽어 쓰는 것이 중요하다.
CI에서 시크릿 전달하기
빌드를 트리거하는 쪽에서는 CI 시크릿 스토어의 값을 파일이나 표준입력으로 넘긴다. GitHub Actions 예시:
name: build
on: [push]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: false
secrets: |
pip_token=${{ secrets.PIP_TOKEN }}
순수 CLI로는 echo "$PIP_TOKEN" | docker build --secret id=pip_token,src=/dev/stdin . 형태로 넘긴다. 파일로 넘길 때는 src= 경로가 워크스페이스 밖(예: $RUNNER_TEMP)에 있게 하고, 스텝 종료 후 삭제한다.
흔한 함정
RUN cat /run/secrets/x처럼 값을 출력하는 디버깅 코드를 남기지 않는다. 로그로 새어나간다.- 시크릿을 읽은 뒤 같은 레이어에서 파일로 쓰면 그 파일이 이미지에 남는다. 반드시 같은
RUN내에서 소비하고 끝낸다. - 멀티스테이지 빌드에서 시크릿을 쓴 스테이지의 산출물에 토큰이 섞여 들어가지 않았는지 확인한다. 최종 스테이지로
COPY되는 건 빌드 결과물뿐이어야 한다. - BuildKit이 꺼져 있으면
--mount문법이 무시되거나 실패한다.DOCKER_BUILDKIT=1또는buildx를 명시한다.
런타임 시크릿과 구분하기
빌드 타임 시크릿(패키지 설치 토큰, 사설 git 접근)과 런타임 시크릿(DB 비밀번호, API 키)은 다르게 다뤄야 한다. 런타임 값은 이미지에 넣지 말고 오케스트레이터가 주입한다. Kubernetes라면 Secret을 파일로 마운트하는 방식이 환경변수보다 안전하다. 환경변수는 /proc나 크래시 덤프로 새기 쉽기 때문이다.
volumes:
- name: db-cred
secret:
secretName: db-credentials
containers:
- name: api
volumeMounts:
- name: db-cred
mountPath: /etc/secrets
readOnly: true
검증 방법
안전하게 주입됐는지는 실제로 확인해야 한다. 빌드 후 docker history --no-trunc <image>로 시크릿 문자열이 없는지 보고, docker save로 tar를 풀어 레이어 안에 토큰 파일이 남지 않았는지 grep한다. CI 파이프라인에는 커밋 단계 시크릿 스캐너를 붙여 소스에 하드코딩된 값이 애초에 들어오지 않게 막는다. 주입 방식이 아무리 안전해도 저장소에 평문이 있으면 무의미하다.