컨테이너 보안 · 시크릿 취급 · 이론
이미지에 들어간 토큰은 되돌릴 수 없다
한 줄 요약
환경변수는 저장 위치가 아니라 전달 방식입니다. 그리고 이미지 빌드 중에 넣은
시크릿은 지운다고 사라지지 않습니다. 레지스트리에 푸시했다면 이미 유출된 것으로
간주하고 폐기해야 합니다.
왜 이게 필요했나
.env 파일을 로드한 뒤 지우면 안전하다고 생각하기 쉽습니다. 그런데/proc/<pid>/environ 은 프로세스가 사는 동안 계속 읽힙니다. 같은 호스트에
있는 사이드카, 디버깅용 셸, 크래시 덤프, 모니터링 에이전트가 전부 읽을 수 있는
자리입니다. 환경변수는 값을 프로세스에 건네주는 통로일 뿐, 값을 숨겨 주는 장치가
아닙니다.
이미지 쪽은 더 나쁩니다. ENV 로 넣은 토큰은 이미지 설정에 영구히 남습니다.
docker inspect -f '{{json .Config.Env}}' bad:v1["NPM_TOKEN=npm_9fA3...", ...]ARG 로 받아 쓰고 rm ~/.npmrc 로 지우는 방식도 마찬가지입니다. 파일은
하위 레이어에 남아 있고 빌드 히스토리에도 남습니다.
docker history --no-trunc bad:v2 | grep -o 'NPM_TOKEN=[^ ]*'여기서 나와야 할 결론은 하나입니다. **레지스트리에 푸시했다면 그 토큰은 이미
유출된 것으로 간주하고 폐기해야 합니다. 이미지를 지우는 것으로는 되돌릴 수
없습니다.**
어떻게 동작하나
빌드 중에 시크릿이 정말 필요하다면 BuildKit 의 secret mount 를 씁니다.
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci이 마운트는 tmpfs 라서 해당 RUN 이 끝나면 사라지고 레이어에 남지 않습니다.
빌드 인자로 넘기는 방식과 근본적으로 다릅니다.
쿠버네티스 Secret 의 base64 는 암호화가 아닙니다. 인코딩일 뿐이고 etcd
암호화나 외부 시크릿 저장소를 따로 켜야 실제 보호가 시작됩니다.
이미 커밋한 시크릿의 조치 순서는 정해져 있습니다. **폐기 → 영향 조사 → 히스토리
정리**. 순서를 바꾸면 안 됩니다. 그리고 히스토리 재작성은 유출을 되돌리는 조치가
아니라 재발을 줄이는 위생 작업입니다. 이미 포크, 로컬 클론, PR 참조, CI 캐시에
복제돼 있기 때문입니다.
현장에서 만나는 모습
로테이션은 운영 절차가 아니라 설계 속성입니다. 코드가 "유효한 키는 정확히
하나"라고 가정하면 로테이션은 반드시 실패합니다. 새 키를 배포하는 순간과 옛 키를
쓰는 클라이언트가 남아 있는 순간이 겹치기 때문입니다. 그래서 **서명은 현재 키
하나로, 검증은 유효 키 집합 전체로** 설계해야 합니다. 이 원칙이 없으면 로테이션
계획서는 실행되지 않습니다.
탐지 도구에도 한계가 셋 있습니다. 첫째, 사전 커밋 훅은 --no-verify 한 번이면
우회됩니다. 그래서 훅은 편의 장치이고 통제 장치는 서버 쪽 검사입니다. 둘째,
규칙은 AKIA, sk_live_ 처럼 형태가 뚜렷한 값에만 잘 듣습니다. 사내 시스템의
자체 형식 토큰은 잘 안 잡힙니다. 셋째, 스캐너는 코드 저장소만 봅니다. 위키,
이슈 첨부파일, 채팅 로그는 보지 못합니다.
마지막으로 팀에 던져야 할 질문이 하나 있습니다. **지금 이 자격증명이 공개됐다고
가정할 때, 폐기하고 교체해 서비스를 정상화하는 데 몇 분이 걸립니까.** 이 질문에
답하지 못하면 시크릿 관리 체계가 아직 없는 것입니다.
다음 실습에서 할 것
환경변수로 넘긴 시크릿이 docker inspect 에 그대로 보이는 것을 확인하고, 같은
값을 파일 마운트로 옮겨 inspect 에서 사라지는 것을 확인합니다. 그리고 컨테이너를
재시작하지 않은 채 값을 바꿔 두 방식의 로테이션 특성 차이를 직접 재어 본 뒤,
간단한 시크릿 탐지 스크립트를 작성합니다.