Git에 비밀키를 커밋했을 때 — 키 회전과 filter-repo 히스토리 정리

6 분 소요

.env 파일을 통째로 커밋했거나, 코드에 하드코딩된 AWS 액세스 키와 API 토큰이 커밋에 섞여 들어갔고, 이미 push까지 끝난 상황입니다. 이 사고에서 가장 흔한 첫 반응은 “빨리 히스토리에서 지우자"입니다. 그런데 대응 순서가 반대입니다. 1순위는 히스토리 정리가 아니라 키 무효화입니다. 이 글은 그 이유부터 확인하고, 상황별 정리 절차와 예방 장치까지 순서대로 정리합니다.

1순위는 키 무효화 #

push된 비밀키는 지워질 수 있는 정보가 아니라 이미 유출된 정보로 간주해야 합니다.

  • 공개 저장소라면 봇이 분 단위로 수집합니다. GitHub의 공개 이벤트를 실시간으로 스캔하며 자격 증명을 수집하는 자동화 도구들이 상시로 돌고 있습니다. push하고 몇 분 안에 남의 손에 들어갔다고 가정하는 것이 안전합니다.
  • 사본은 원본 정리로 지워지지 않습니다. 포크, 동료의 클론, CI가 받아 간 체크아웃과 로그에는 이미 사본이 존재합니다. 원본 저장소의 히스토리를 다시 써도 이 사본들에는 아무 영향이 없습니다.

그래서 어떤 정리 작업보다 먼저, 발급처에서 해당 자격 증명을 폐기하고 새로 발급합니다. AWS 액세스 키는 IAM에서 비활성화 후 삭제하고, GitHub 토큰과 서드파티 API 키는 각 서비스에서 revoke하고, DB 비밀번호라면 비밀번호 자체를 변경합니다. 키가 무효화된 순간부터 히스토리에 남은 문자열은 더 이상 위험하지 않은 죽은 값이 됩니다.

노트
클라우드 자격 증명이라면 무효화와 함께 오용 여부도 확인해야 합니다. AWS라면 CloudTrail에서 해당 키의 최근 호출 이력을 점검합니다. 유출 시점 이후에 낯선 리전에서 리소스 생성 같은 이력이 있다면 키 폐기만으로 끝나지 않는 침해 대응 사안입니다.

상황 판단 — push 전인가 후인가 #

키 무효화 다음은 히스토리 정리입니다. 절차는 커밋이 원격에 올라갔는지에 따라 완전히 달라집니다.

  • 아직 push 전 — 내 컴퓨터에만 있는 커밋이므로 로컬에서 다시 쓰면 끝납니다.
  • push 후 — 원격과 모든 사본에 퍼진 상태이므로, 전용 도구로 히스토리를 다시 쓰고 팀 전체의 협조가 필요합니다.

push 전이라면 — 로컬에서 정리 #

방금 만든 마지막 커밋에 비밀키가 들어갔다면 두 명령으로 정리됩니다.

마지막 커밋에서 파일 제거
git rm --cached .env          # 추적만 해제, 파일은 작업 디렉터리에 남음
echo ".env" >> .gitignore
git add .gitignore
git commit --amend --no-edit  # 마지막 커밋을 새로 교체

git rm --cached는 파일을 지우지 않고 Git의 추적에서만 내립니다. 이어서 --amend로 커밋을 교체하면 비밀키가 없는 커밋이 마지막 커밋을 대신합니다.

비밀키가 몇 커밋 전에 들어갔다면 interactive rebase로 해당 커밋을 고칩니다.

이전 커밋에서 제거
git rebase -i <비밀키 커밋의 직전 커밋>
# todo 리스트에서 해당 커밋을 edit 로 변경

git rm --cached .env
git commit --amend --no-edit
git rebase --continue

이 경우 키 회전은 판단의 영역입니다. 커밋이 내 컴퓨터 밖으로 나간 적이 없다면 유출은 없었던 것이지만, 확신할 수 없다면(예를 들어 개인 원격에라도 한 번 올렸다면) 회전하는 쪽이 안전합니다.

push 후라면 — filter-repo로 히스토리 정리 #

원격에 올라간 커밋은 히스토리 전체를 다시 쓰는 도구가 필요합니다. 표준 도구는 git filter-repo입니다. 과거에 쓰이던 git filter-branch는 느리고 함정이 많아 Git 공식 문서도 filter-repo를 권장합니다.

filter-repo 설치
pip install git-filter-repo
# 또는 macOS: brew install git-filter-repo

filter-repo는 실수로 작업 저장소를 망치는 것을 막기 위해 새로 clone한 깨끗한 저장소에서 실행하는 것을 전제로 합니다. 정리 전용 클론을 하나 새로 받습니다.

파일 단위 제거 — .env 를 전체 히스토리에서 삭제
git clone https://github.com/example/my-service.git cleanup
cd cleanup
git filter-repo --invert-paths --path .env

--path로 지정한 파일을 남기는 것이 기본 동작이므로, --invert-paths를 붙여 반대로 그 파일만 전체 히스토리에서 제거합니다. 파일은 남기되 코드 안의 키 문자열만 바꿔야 한다면 치환 규칙 파일을 씁니다.

expressions.txt — 치환 규칙
AKIAIOSFODNN7EXAMPLE==>***REMOVED***
regex:AKIA[0-9A-Z]{16}==>***REMOVED***
문자열 치환 실행
git filter-repo --replace-text expressions.txt

정리가 끝난 히스토리는 모든 커밋의 해시가 바뀐 완전히 새로운 히스토리입니다. filter-repo가 안전을 위해 원격 설정을 제거해 두므로, 원격을 다시 연결하고 강제로 올립니다.

정리된 히스토리 올리기
git remote add origin https://github.com/example/my-service.git
git push --force --all
git push --force --tags

마지막이 가장 중요합니다. 팀 전원에게 기존 클론을 버리고 새로 clone하라고 공지합니다. 옛 히스토리를 가진 클론에서 pull이나 push를 하면 지웠던 커밋이 원격으로 되살아나는 경로가 됩니다. 히스토리 재작성은 도구가 아니라 이 협조 절차까지 끝나야 완료됩니다.

같은 용도의 대안 도구로 BFG Repo-Cleaner가 있습니다. 규칙이 단순한 대신 세밀한 제어는 filter-repo가 낫습니다. 어느 쪽을 쓰든 fresh clone 작업과 force push, re-clone 공지라는 절차는 동일합니다.

지워도 남는 곳 #

원본 저장소를 완벽히 정리해도 GitHub 플랫폼에는 흔적이 남을 수 있습니다.

  • 이미 만들어진 포크의 히스토리는 원본 정리와 무관하게 유지됩니다.
  • PR의 diff 화면과 커밋 해시로 직접 접근하는 캐시된 뷰는 저장소 히스토리에서 커밋이 사라진 뒤에도 한동안 열립니다.

이 영역은 저장소 주인이 직접 지울 수 없고, GitHub Support에 캐시 제거와 포크 처리를 요청해야 정리됩니다. 뒤집어 말하면 히스토리 정리를 아무리 잘해도 완전한 삭제는 보장되지 않으며, 이것이 키 회전이 1순위인 두 번째 이유입니다.

예방 장치 #

같은 사고를 반복하지 않는 장치는 커밋 전 단계부터 겹겹이 둘 수 있습니다.

  • .gitignore 선등록.env, *.pem 같은 파일은 만들기 전에 등록하는 순서를 습관으로 만듭니다. Git 기초 #2에서 다룬 원칙 그대로입니다.
  • GitHub push protection — 저장소의 secret scanning 설정에서 push protection을 켜 두면, 알려진 형식의 자격 증명이 포함된 push를 GitHub가 수신 단계에서 차단합니다.
  • pre-commit 훅 — gitleaks 같은 스캐너를 pre-commit 훅으로 걸어 두면 커밋이 만들어지기 전에 로컬에서 걸러집니다.
  • 키를 코드 밖으로 — 애초에 코드와 저장소에 키가 존재하지 않게 환경변수와 시크릿 매니저(AWS Secrets Manager 등)로 주입하고, 저장소에는 값이 빈 .env.example만 둡니다.

마무리 #

사고 대응 순서를 다시 정리합니다.

  1. 회전 — 발급처에서 즉시 폐기하고 재발급합니다. push된 키는 이미 유출된 것으로 간주합니다.
  2. 정리 — push 전이면 amend나 rebase로, push 후면 fresh clone에서 filter-repo로 히스토리를 다시 씁니다.
  3. 공지 — force push 후 팀 전원이 re-clone하게 안내합니다. 포크와 캐시는 GitHub Support 요청으로 처리합니다.
  4. 예방 — .gitignore 선등록, push protection, pre-commit 스캐너, 시크릿 매니저로 재발을 막습니다.

히스토리 정리는 눈에 보이는 흔적을 지우는 마무리 작업이고, 사고의 실제 봉합은 키 무효화가 담당합니다. 순서만 지켜도 이 사고는 수습 가능한 해프닝으로 끝납니다.

X