
git filter-repo 민감정보 삭제 — 커밋한 API 키·비밀번호 히스토리에서 지우는 법
API 키나 비밀번호를 깃에 커밋해버렸다면? git filter-repo로 민감정보를 히스토리에서 완전히 삭제하는 법과, 그보다 먼저 해야 할 키 재발급(rotate)까지 단계별로 정리했다.
git push 를 누르고 커피를 가지러 일어난 순간, 머릿속에 한 줄이 스친다. "방금 그 커밋에 .env 들어 있지 않았나?" 자리로 돌아와 확인하면 늦었다. AWS 액세스 키가, 혹은 DB 비밀번호가 원격 저장소 히스토리에 그대로 박혀 있다. 공개 repo라면 이미 늦은 수준이 아니라 초 단위로 늦고 있는 중이다.
이 상황에서 개발자들이 가장 먼저 저지르는 실수는 "그 줄을 지우고 다시 커밋"하는 것이다. 그래봐야 최신 커밋만 깨끗해질 뿐, 과거 커밋을 git log -p 로 열면 비밀번호가 멀쩡히 보인다. 깃은 모든 변경 이력을 보존하는 도구다. 한 번 커밋된 값은 히스토리를 통째로 다시 쓰지 않는 한 사라지지 않는다.

손대기 전에 — 딱 두 가지만 확인
히스토리 재작성은 되돌리기 어려운 작업이다. 명령을 치기 전에 두 가지를 확보해두자.
- 백업. 지금 상태 그대로 디렉터리를 통째로 복사하거나(
cp -r myrepo myrepo-backup), 최소한git clone --mirror로 원본 미러를 하나 떠둔다. filter-repo가 꼬였을 때 돌아올 지점이 필요하다. - 협업자 조율. 혼자 쓰는 repo가 아니라면, 히스토리를 갈아엎는 순간 팀원들의 로컬 저장소가 전부 어긋난다. "지금부터 force push 할 테니 작업 멈추고 새로 클론받으세요"를 먼저 공지한다. 안 그러면 누군가 옛 히스토리를 다시 push 해서 지운 비밀번호가 부활한다.
제일 먼저 — 키부터 폐기하라 (rotate)
여기서 순서를 뒤집는 게 핵심이다. 많은 가이드가 "히스토리 삭제 → 그다음 재발급"으로 적지만, 실무에서 올바른 순서는 재발급이 먼저다.
이유는 단순하다. 일단 push된 비밀은, 특히 공개 repo에 올라간 비밀은, 히스토리에서 지운다고 안전해지지 않는다. GitHub는 커밋을 SHA 해시로 한동안 캐시해두고, 누군가 포크를 떠갔다면 그 포크에는 그대로 남는다. 공개 repo의 시크릿은 자동 스캐너들이 수십 초 안에 긁어간다. 올리는 순간 이미 남의 손에 있다고 가정하는 게 맞다.
그러니 명령어를 치기 전에 이것부터 한다.
- 유출된 API 키를 발급처 콘솔에서 즉시 폐기(revoke)하고 새로 발급한다.
- 비밀번호였다면 지금 당장 변경한다.
- OAuth 토큰·웹훅 시크릿이면 재생성한다.
이걸 하고 나면 히스토리 청소는 "흔적 지우기"일 뿐, 보안 조치의 본체는 이미 끝난 셈이다. 반대로 이걸 건너뛰면 히스토리를 아무리 깨끗이 닦아도 유출된 키는 살아 있다. 다시 강조한다. 명령어보다 재발급이 먼저다.

git-filter-repo 설치하고 지우기
git filter-branch 는 공식 문서조차 쓰지 말라고 안내하는 느리고 위험한 도구다. 대체재가 git-filter-repo — 파이썬 스크립트 하나로 동작하고, 대형 저장소도 빠르게 처리한다.
설치부터.
# macOS
brew install git-filter-repo
# pip (파이썬 3.6+)
pip install git-filter-repo
# Debian/Ubuntu 최신 버전
sudo apt install git-filter-repo
파일 통째로 지우기. .env 파일이나 config/secrets.yml 처럼 파일 자체가 유출원이면 --invert-paths 로 해당 경로를 모든 커밋에서 제거한다.
git filter-repo --invert-paths --path .env --path config/secrets.yml
--invert-paths 는 "지정한 경로만 빼고 나머지는 유지"라는 뜻이다. 빼먹으면 정반대로 그 파일만 남기고 전부 날아가니 주의한다.
코드에 박힌 문자열만 지우기. 파일은 남기되 그 안에 하드코딩된 키 값만 지우고 싶다면 --replace-text 를 쓴다. 먼저 치환 규칙 파일을 만든다.
# replacements.txt
AKIAIOSFODNN7EXAMPLE==>***REMOVED***
my-db-password==>***REMOVED***
regex:sk-[A-Za-z0-9]{32}==>***REMOVED***
그리고 실행한다.
git filter-repo --replace-text replacements.txt
규칙 파일에 적은 문자열이 전체 히스토리에서 REMOVED 로 바뀐다. regex: 접두사를 붙이면 패턴 매칭도 된다.
한 가지 함정. filter-repo는 작업이 끝나면 origin 리모트를 스스로 제거한다. 절반만 정리된 히스토리가 실수로 push되는 걸 막으려는 안전장치다. 그래서 push 전에 리모트를 다시 연결해야 한다.
git remote add origin git@github.com:계정/저장소.git
BFG라는 대안
자바가 깔려 있고 "파일 지우기 / 문자열 치환" 정도의 단순 작업이라면 BFG Repo-Cleaner 가 더 간단할 때가 있다. 속도도 빠르다.
bfg --delete-files .env
bfg --replace-text replacements.txt
git reflog expire --expire=now --all && git gc --prune=now --aggressive
딱 하나 기억할 것. BFG는 현재 HEAD 커밋은 건드리지 않는다. 그러니 "최신 커밋에서 비밀번호를 지운 커밋"을 먼저 하나 만들어두고 BFG를 돌려야 과거까지 깨끗해진다. filter-repo가 HEAD까지 전부 처리하는 것과 다른 지점이라, 둘을 섞어 생각하면 꼭 한 군데를 놓친다.
force push와 협업자 재클론
히스토리를 다시 썼으니 원격도 강제로 덮어야 한다.
git push origin --force --all
git push origin --force --tags
그리고 팀원들에게 공지한다. "기존 로컬 저장소는 버리고 새로 클론받으세요." 옛 클론에서 git pull 로 합치려 하면 지운 비밀번호가 머지되어 돌아온다. 가장 깔끔한 건 폴더를 지우고 git clone 을 다시 받는 것이다. GitHub라면 캐시된 옛 커밋 뷰를 지워달라고 Support에 요청할 수도 있지만, 그건 어디까지나 보조 수단이다.
다시는 안 당하려면
사고는 한 번으로 끝내야 한다. 나는 두 번 당하고 나서야 아래를 전부 깔았다.
.gitignore..env,*.pem,credentials.json같은 파일을 커밋 대상에서 아예 빼둔다. 기본 중의 기본인데 신규 프로젝트에서 자주 빠진다.- pre-commit 훅.
git-secrets(AWS Labs)나gitleaks를 pre-commit 훅으로 걸어두면 커밋 시점에 키 패턴을 잡아 차단한다. 사람이 까먹어도 기계가 막아준다. - GitHub Push Protection. GitHub는 공개 저장소에 시크릿 스캐닝과 push protection을 무료로, 그것도 기본 활성 상태로 제공한다. 알려진 형태의 토큰이 섞인 push를 아예 거절한다. 조직·비공개 저장소라면 설정에서 직접 켜두자.
한국 개발 현장에서 이 사고가 유독 잦은 데는 이유가 있다고 본다. 사내 위키나 블로그 예제가 "일단 돌아가게" 만드느라 키를 코드에 직접 박아 설명하는 경우가 많고, 그걸 그대로 따라 하다 커밋까지 가는 식이다. 신입 교육 자료부터 .env 와 환경변수 주입을 기본값으로 바꾸는 편이, 개인 실수를 탓하는 것보다 효과가 크다. 국내 스타트업은 레포를 급하게 공개로 돌렸다가 다시 비공개로 바꾸는 일도 흔한데, 잠깐 공개됐던 그 몇 분이면 스캐너가 키를 긁어가기 충분하다. "잠깐 공개였으니 괜찮겠지"가 가장 위험한 착각이다. 히스토리 정리 기술보다 "push된 순간 유출로 간주한다"는 태도 하나가 실제 피해를 훨씬 많이 줄인다.
순서를 다시 정리하면 이렇다. ① 키부터 재발급(rotate) → ② 백업·협업자 공지 → ③ filter-repo나 BFG로 히스토리 청소 → ④ force push & 재클론 → ⑤ 훅과 push protection으로 재발 차단. 이 중 하나만 꼽으라면 단연 1번이다. 나머지는 흔적을 지우는 작업이고, 실제 방어선은 재발급 하나다.
여러분은 시크릿 관리를 어떻게 하고 있나요? pre-commit 훅까지 걸어두는 편인지, 아니면 아직 .gitignore 에만 의존하는지 — 사고를 겪어본 경험담도 궁금합니다.
댓글 0
댓글을 불러오는 중…