본문으로 건너뛰기
CodeAuditAgent
전체 글

Git 히스토리에 남은 시크릿: 찾기, 교체하기, 예방하기

커밋된 API 키를 지워도 git 히스토리에서는 사라지지 않습니다. 유출된 시크릿을 찾고 교체하고 히스토리를 정리하는 법.

· 7분 분량 · Lina Source LLC

누군가 .env 파일을 커밋하거나, 빠르게 테스트해 보려고 실제 API 키를 설정 모듈에 붙여 넣습니다. 리뷰어가 발견해 다음 커밋에서 키를 지우고 모두가 넘어갑니다. 그런데 키는 여전히 거기 있습니다. Git은 모든 파일의 모든 버전을 저장하며, 저장소를 클론할 수 있는 사람이라면 누구나 그 키를 추가한 커밋을 읽을 수 있습니다.

하드코딩된 자격 증명은 CWE-798로 분류됩니다. 이 가이드는 이미 히스토리에 들어가 버린 경우에 무엇을 해야 하는지 다룹니다. 모든 시크릿을 찾고, 무엇보다 먼저 교체하고, 히스토리를 다시 쓸 가치가 있는지 판단하고, 다음 유출을 막을 안전장치를 마련하는 순서입니다.

줄을 지우는 것으로 충분하지 않은 이유

시크릿을 제거하는 커밋은 그것이 없는 새 스냅샷을 추가할 뿐입니다. 시크릿이 들어 있는 이전 스냅샷은 여전히 브랜치의 히스토리에서 참조됩니다. git log -p로 볼 수 있고, 예전 커밋을 git checkout하면 복원되며, 이미 존재하는 모든 클론과 포크에 사본이 있습니다. 병합 전에 브랜치를 스쿼시하는 것도 원래 커밋이 한 번이라도 푸시되었다면 도움이 되지 않습니다. 원격에 아직 남아 있을 수 있고, 그것을 페치한 사람은 로컬에 가지고 있습니다.

공개 저장소라면 시크릿이 이미 노출되었다고 가정하세요. 자동화된 스크레이퍼가 공개 푸시에서 자격 증명 패턴을 감시하고 있으며, 푸시와 첫 악용 사이의 간격은 매우 짧을 수 있습니다. 비공개 저장소라면 노출 범위가 작긴 하지만 0은 아닙니다. 읽기 권한을 가진 모든 직원과 외주 인력, CI 시스템, 연동 서비스가 그것을 가지고 있습니다.

1단계: 먼저 교체하기

교체는 실제로 위험을 없애는 유일한 단계입니다. 히스토리를 다시 쓰는 것은 앞으로의 클론에서 시크릿을 감출 뿐, 이미 존재하는 사본에 대해서는 아무것도 하지 못합니다. 그러니 저장소를 건드리기 전에 자격 증명을 폐기하고 교체하세요.

장애가 나지 않도록 교체 계획을 세우세요. 많은 제공자가 두 개의 키를 동시에 유효하게 둘 수 있게 해 줍니다. 새 키를 만들고, 기존 키를 쓰던 모든 곳에 배포하고, 트래픽이 옮겨 갔는지 확인한 뒤 기존 키를 폐기하세요. 제공자가 키를 하나만 허용한다면 짧은 중단을 감수하세요. 완벽한 전환을 조율하는 동안 유출된 것이 확실한 자격 증명을 계속 살려 두는 것보다는 몇 분의 다운타임이 나은 거래입니다.

  • 제공자 콘솔에서 새 키를 발급하고 평소 설정 경로를 통해 배포하세요.
  • 기존 키를 폐기하세요. 그냥 쓰지 않는 것으로는 부족합니다. 쓰이지 않는 유효한 키도 여전히 유효한 키입니다.
  • 커밋 시점 이후 기존 키로 이뤄진 활동을 제공자의 감사 로그에서 확인하세요. API 호출, 새 사용자, 변경된 권한, 예상치 못한 청구 등입니다.
  • 시크릿이 데이터베이스 비밀번호나 서명 키였다면 그것으로 무엇을 열 수 있었을지 생각해 보세요. JWT 서명 시크릿이 유출되었다면 어떤 토큰이든 위조될 수 있었다는 뜻이므로 기존 세션을 무효화하세요.
  • 무엇이 얼마나 오래 노출되었고 무엇을 바꿨는지 기록해 두세요. 고객이나 감사인이 물어볼 때 필요합니다.

2단계: 유출된 것을 모두 찾기

커밋된 시크릿이 하나 있다면 다른 것도 있는 경우가 많습니다. 현재 트리만이 아니라 전체 히스토리를 검색하고, 모든 브랜치와 태그를 포함하세요.

# 히스토리 어디에서든 특정 문자열을 추가하거나 제거한 커밋
git log -p -S "sk_live_" --all

# 모든 커밋의 diff를 정규식으로 검색
git log -p -G "AKIA[0-9A-Z]{16}" --all

# 의심스러운 경로에 한 번이라도 존재했던 파일
git log --all --oneline -- .env config/production.json

# 전용 스캐너는 알려진 자격 증명 형식 수백 가지를 검사합니다
gitleaks git -v .
trufflehog git file://. --only-verified

gitleaks와 trufflehog는 바로 이 작업을 위해 만들어진 오픈소스 스캐너입니다. 흔한 제공자 키의 형식을 알고 있으며, trufflehog는 찾아낸 자격 증명이 아직 살아 있는지 확인할 수도 있습니다. 예전 gitleaks 릴리스는 git 하위 명령 대신 gitleaks detect --source .을 씁니다. 전체 히스토리에 스캐너를 한 번 돌린 뒤, 새 커밋에도 계속 돌아가게 하세요. 테스트 픽스처나 문서의 예시 키처럼 오탐도 어느 정도 나옵니다. 하나씩 확인한 다음 확정된 오탐은 스캐너의 허용 목록이나 베이스라인 파일에 기록해 두면, 다음 실행에서는 새로운 발견 사항만 보이게 됩니다.

저장소 주변도 잊지 마세요. 환경 변수를 출력한 CI 로그, 이슈와 풀 리퀘스트 댓글, 위키 페이지, 저장소로 빌드한 Docker 이미지 레이어, 디버깅 중에 공유한 gist나 붙여넣기 서비스가 여기에 해당합니다.

3단계: 히스토리를 다시 쓸지 결정하기

시크릿을 교체하고 나면 그것은 쓸모없어지므로, 히스토리 재작성은 봉쇄가 아니라 정리 작업입니다. 그래도 저장소가 공개이거나, 시크릿이 내부 호스트명이나 고객사 이름처럼 그 자체를 넘어선 정보를 드러내거나, 규정 준수상 필요할 때는 할 만한 가치가 있습니다. 비용도 실제로 발생합니다. 모든 협업자가 다시 클론해야 하고, 열려 있는 풀 리퀘스트가 깨지며, 커밋 해시가 바뀝니다. 릴리스 노트와 배포 기록, 이슈 링크, 다른 저장소에 고정된 의존성처럼 해시로 커밋을 참조하는 모든 것이 브랜치에 더는 존재하지 않는 커밋을 가리키게 됩니다. 재작성을 미리 공지하고, 한산한 시점을 골라, 열려 있는 풀 리퀘스트를 먼저 병합하거나 닫으세요.

git filter-repo는 Git 프로젝트가 예전 git filter-branch 대신 권장하는 도구입니다. 새로 클론한 저장소에서 동작하며 안전장치로 origin 원격을 제거하므로, 푸시하기 전에 다시 추가해야 합니다.

# 새로 클론한 저장소에서 시작
git clone [email protected]:acme/api.git api-clean
cd api-clean

# 옵션 A: 모든 커밋에서 파일 제거
git filter-repo --invert-paths --path config/production.env

# 옵션 B: 시크릿 문자열이 나타나는 모든 곳을 치환
# replacements.txt에는 한 줄에 규칙 하나를 적습니다. 예:
#   PASTE_THE_OLD_KEY_HERE==>REDACTED
git filter-repo --replace-text ../replacements.txt

# filter-repo는 origin을 제거합니다. 다시 추가한 뒤 강제 푸시하세요
git remote add origin [email protected]:acme/api.git
git push --force --all
git push --force --tags

재작성으로 할 수 없는 일

  • 이미 존재하는 클론과 포크, CI 캐시, 백업에는 손댈 수 없습니다. 재작성 전에 페치한 사람은 예전 커밋을 그대로 가지고 있습니다.
  • GitHub에서는 풀 리퀘스트용으로 생성된 참조가 읽기 전용이라 예전 커밋이 계속 접근 가능할 수 있습니다. 캐시된 화면과 그 참조를 제거하려면 GitHub 지원팀에 문의해야 합니다.
  • 예전 클론에서 풀하거나 병합하는 협업자가 옛 히스토리를 그대로 다시 푸시할 수 있습니다. 모두에게 다시 클론하도록 요청하고, 그동안 브랜치를 보호하세요.
  • 로그와 티켓, 채팅 메시지, 빌드 산출물처럼 시크릿이 복사된 다른 곳에서는 제거되지 않습니다.

그래서 순서가 중요합니다. 교체한 뒤 정리하세요. 살아 있는 키가 남은 채 히스토리만 다시 쓴 것은 잘못된 안도감일 뿐입니다. 푸시한 뒤에는 새로 클론한 저장소에 스캐너를 돌려 모든 브랜치와 태그에서 시크릿이 정말 사라졌는지 확인하세요.

4단계: 다음 유출 막기

목표는 시크릿을 커밋하기 어렵게 만들고, 커밋되었을 때 빨리 알아차리는 것입니다. 하나의 통제로 둘 다 해결되지는 않으므로 여러 겹으로 쌓으세요.

시크릿을 코드 경로 밖에 두세요

자격 증명은 런타임에 환경 변수나 시크릿 매니저에서 읽고, 필요한 값이 존재하는지 시작 시점에 검증하세요. 플레이스홀더 값이 들어간 .env.example을 커밋하고, 첫 커밋부터 .env를 .gitignore에 넣으세요. 프로덕션에서는 AWS Secrets Manager, Google Secret Manager, HashiCorp Vault 또는 플랫폼의 암호화된 환경 설정 같은 시크릿 매니저를 쓰면 접근 제어와 감사 로그, 더 쉬운 교체를 얻을 수 있습니다.

커밋되기 전에 차단하세요

pre-commit 훅은 원격에 도달하기도 전에 개발자의 컴퓨터에서 실수를 잡아냅니다. pre-commit 프레임워크를 쓰면 모든 기여자가 설치할 수 있는 몇 줄짜리 설정으로 끝납니다.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: vX.Y.Z  # 최신 릴리스 태그로 고정
    hooks:
      - id: gitleaks

# 클론마다 한 번씩 설치
#   pip install pre-commit
#   pre-commit install

훅은 로컬에 있고 건너뛸 수 있으므로, 최후의 방어선으로 모든 푸시마다 CI에서 같은 스캐너를 실행하세요. GitHub에서는 플랜이 지원하는 범위에서 시크릿 스캐닝과 푸시 보호를 활성화하세요. 푸시 보호는 알려진 제공자 토큰이 포함된 푸시를 서버 측에서 거부합니다.

놓친 유출의 피해를 줄이세요

  • 모든 키에 최소한의 권한만 부여하고, 제공자가 허용한다면 특정 IP나 리퍼러로도 제한하세요.
  • 장기 정적 키보다 CI에서 클라우드 제공자로의 OIDC 페더레이션처럼 수명이 짧은 자격 증명을 선호하세요.
  • 환경마다 별도의 키를 쓰세요. 그래야 유출된 테스트 키가 프로덕션에 손댈 수 없습니다.
  • 중요한 시크릿마다 교체 런북을 유지해, 급박한 상황에서의 교체가 즉흥이 아니라 일상적인 절차가 되게 하세요.

코드 리뷰가 놓이는 자리

스캐너는 알려진 형식을 잘 맞히지만, 두 개의 상수를 조합해 만든 비밀번호나 설정 폴백에 들어 있는 기본 자격 증명 같은 맥락에는 약합니다. 코드를 한 번 훑는 리뷰가 그런 것을 잡아냅니다. CodeAuditAgent는 공개 저장소의 기본 브랜치나 붙여넣은 스니펫을 감사할 때 하드코딩된 자격 증명을 CWE-798로 인용 근거와 함께 표시합니다. 커밋 히스토리가 아니라 현재 코드를 읽으므로, 지난 커밋에 대해서는 히스토리 스캐너를 함께 유지하세요.

요약하면 이렇습니다. 커밋된 시크릿은 유출된 시크릿입니다. 오늘 교체하고, 혼란을 감수할 가치가 있다면 히스토리를 정리하고, 다음 유출은 공개된 페이지가 아니라 pre-commit 훅에서 걸리도록 만드세요.