9월 1일에 올라온 보안 회사 보고서 하나가 이번 주 해커뉴스 첫 페이지에 올랐다. 인증 없이 열려 있던 컨테이너 레지스트리에서 이미지를 받아 빌드 히스토리를 읽었더니 2023년 3월 3일 빌드 단계에 GitHub 토큰이 그대로 들어 있었다는 내용이다. 그 토큰은 2026년 7월에도 살아 있었다. 토큰은 저장소 세 곳에 admin 권한이 있었다. 사고 자체보다 궁금한 건 이거다. 내 Dockerfile의 ARG GITHUB_TOKEN도 같은 상태인가.
빌드 인자가 남는 자리
Docker 공식 문서 "Build secrets"의 문장이다. "Build arguments and environment variables are inappropriate for passing secrets to your build, because they persist in the final image." Dockerfile 레퍼런스의 ARG 절은 더 구체적이다. 빌드 인자는 docker history 명령과 provenance attestation에서 보인다고 적혀 있다.

ARG는 ENV처럼 컨테이너 환경변수로 남지는 않는다. 대신 그 값을 쓴 RUN 줄이 이미지 config의 history[].created_by 필드에 명령 문자열 그대로 기록된다. OCI 이미지 스펙이 created_by를 "레이어를 만든 명령"이라고 정의해 두었으니 저장 위치가 규격이다. 보고서가 문제 삼은 Dockerfile도 딱 이 패턴이다.
# 보고서에 실린 패턴. 인자를 git 설정에 박는다
ARG GITHUB_TOKEN
RUN GITHUB_TOKEN=${GITHUB_TOKEN} bash -c '\
if [[ "${GITHUB_TOKEN}" != "" ]]; then \
git config --global --add \
url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"; \
fi'
이 줄은 두 군데에 흔적을 남긴다. 히스토리에 명령 문자열이 남는다. git config --global은 인증 URL을 설정 파일에도 쓴다. 보고서 표현대로 나중 레이어에서 파일을 지워도 히스토리에 남은 사본은 그대로다.
내 이미지 확인하는 법
오래된 이미지를 골라 두 명령을 돌린다.
# 잘린 출력은 값을 가리니 --no-trunc 를 붙인다
docker history --no-trunc <이미지>
# 환경변수만 따로. 여기에 토큰이 있으면 ENV 로 넘긴 것
docker image inspect --format '{{json .Config.Env}}' <이미지>
docker history는 각 레이어의 CREATED BY 칸에 그 레이어를 만든 명령을 보여준다. --no-trunc 없이 보면 긴 명령은 앞부분만 나와서 뒤에 붙은 값이 잘릴 수 있다. 보고서의 점검 권고도 같다. 로그인 없이 뭘 받을 수 있는지 본 다음 docker history --no-trunc나 config blob의 history[].created_by를 읽고 레이어도 확인하라는 것이다.
솔직히 말하면 이 글을 쓰면서 내 노트북에서 빌드해 히스토리를 찍어 보려 했는데 Docker Hub 조회 단계에서 빌드가 멈춰 결과를 얻지 못했다. 위 두 명령은 문서와 보고서 기준이고 내 출력은 아니다. 오래된 사내 레지스트리가 있는 팀이라면 이 두 줄을 먼저 돌려보는 게 이 글의 전부다.
시크릿 마운트로 바꾸기
BuildKit의 시크릿 마운트는 빌드 컨테이너가 빌드 중에만 값을 읽고 이미지에는 굽지 않는 방식이다. Dockerfile 레퍼런스 정의는 "without baking them into the image"다.
# syntax=docker/dockerfile:1
FROM python:3
RUN pip install awscli
# 빌드 중에만 /root/.aws/credentials 로 보인다. 레이어에 남지 않는다
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
aws s3 cp s3://... .
문서 예제 그대로다. target을 안 주면 기본 경로는 /run/secrets/<id>다. 파일 대신 환경변수로 받고 싶으면 env= 옵션을 준다. GitHub 토큰이라면 id=gh_token으로 마운트해 RUN 안에서 $(cat /run/secrets/gh_token)으로 읽으면 된다. 빌드 명령은 파일이나 환경변수 중 하나를 소스로 지정한다.
# 파일을 소스로
docker build --secret id=gh_token,src=./token.txt .
# 환경변수를 소스로. 변수 이름이 id 와 같으면 env= 는 생략해도 된다
gh_token=... docker build --secret id=gh_token,env=gh_token .
Docker Engine 23.0부터 리눅스 엔진의 docker build가 BuildKit을 기본으로 탄다. 데스크톱도 Buildx가 같이 깔려 있어 따로 켤 건 없다.
마운트해도 새는 경우
보고서 권고 세 번째가 이거다. 시크릿을 읽는 명령이 그 값을 다시 이미지 안에 쓰지 않게 하라는 것. git config --global로 인증 URL을 박는 패턴이 바로 그 경우다. 시크릿 마운트는 값을 이미지에 안 남기지만 내가 RUN 안에서 어딘가에 써 버리면 그건 마운트 탓이 아니다. 나도 처음엔 마운트만 바꾸면 끝인 줄 알았다.
토큰 권한
보고서 권고 네 번째는 빌드 토큰이 실제로 뭘 할 수 있는지 보라는 것이다. 의존성 하나 받는 데 필요한 건 그 저장소 읽기 권한이지 배포 저장소 admin이 아니다. GitHub 문서도 classic 토큰 대신 fine-grained 토큰을 권한다. fine-grained 토큰은 만료를 1일에서 366일 사이로 고르게 돼 있다.
Dockerfile의 ARG는 현관 비밀번호를 택배 상자 겉면에 적어 두는 것과 같다. 상자를 열어 볼 사람이 없다고 믿는 동안엔 문제가 없다. 내 경우 3년 전 이미지가 어느 레지스트리에 남아 있는지부터 모르겠어서 그 목록을 만드는 게 첫 일이 됐다.
참고
- Docker Docs, Build secrets https://docs.docker.com/build/building/secrets/
- Docker Docs, Dockerfile reference (ARG, RUN --mount=type=secret) https://docs.docker.com/reference/dockerfile/
- Docker Docs, docker buildx build --secret https://docs.docker.com/reference/cli/docker/buildx/build/
- 공개 사례 보고서 (2026-09-01) https://www.strix.ai/blog/baseten-harbor-github-pat-takeover
- OCI Image Configuration, history.created_by https://github.com/opencontainers/image-spec/blob/main/config.md
'개발 > 백엔드' 카테고리의 다른 글
| 🪟 윈도우10 ESU 연장 · 2027년 10월 12일까지 받는 법 (0) | 2026.10.26 |
|---|---|
| 🟩 Node.js 26 LTS 10월 28일 · 22는 2027년 4월 종료 (0) | 2026.10.25 |
| ☕ Java 27 나왔다, LTS 아니지만 기본값이 셋 바뀐다 (0) | 2026.09.17 |
| 🔐 개정 개인정보보호법 9월 11일 시행, 서비스 운영자가 바꿀 것 4가지 (1) | 2026.09.11 |
| ⏰ 맥 launchd vs cron, 자동 실행 뭘로 하나 (1) | 2026.09.10 |