Post

[Git] 민감 정보 커밋 내역 삭제하기(.pem)

[Git] 민감 정보 커밋 내역 삭제하기(.pem)

문제

RSA Private Key 가 git repository에 업로드 되어서 Gitguardian에서 incident detected 메일을 받았다.

.pem 을 삭제해도 커밋 기록에 남아서 확인이 key값을 확인할 수 있었다.

image.png

해결

1. commit history 삭제

1
2
3
4
5
6
7
8
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch {경로/삭제할 파일}" \
  --prune-empty --tag-name-filter cat -- --all

git reflog expire --expire=now --all
git gc --prune=now --aggressive

git push origin --force --all

git filter-branch : Git 저장소의 커밋 기록을 강제로 수정하는 명령어로 특정 파일 또는 디렉토리를 과거의 모든 커밋에서 제거하는 데 사용한다.

주의: git filter-branch는 저장소의 커밋 SHA-1 해시 값을 변경하므로, 이미 원격 저장소에 푸시된 이력이 있는 브랜치에 대해 이 명령어를 실행하면 협업하는 다른 사람들의 작업에 심각한 문제를 일으킬 수 있으므로 가능하면 먼저 백업을 해둬야 한다.

참고: GPT 코드 분석

명령어 분석:

  1. git filter-branch: 히스토리 다시 쓰기 도구입니다. 주어진 필터를 기반으로 커밋들을 다시 작성합니다.
  2. -force: 필터링이 안전하지 않을 수 있다는 경고를 무시하고 강제로 실행합니다. 이 옵션을 사용하는 것은 잠재적인 위험을 감수하겠다는 의미입니다.
  3. -index-filter "<명령어>": 각 커밋의 인덱스(스테이징 영역)를 수정하는 셸 명령어를 실행합니다. 인덱스는 해당 커밋의 내용을 임시로 나타내는 공간입니다.
    • git rm --cached --ignore-unmatch {경로/삭제할 파일}: 이 부분이 실제로 파일을 제거하는 역할을 합니다.
      • git rm --cached: 워킹 디렉토리의 파일은 그대로 두고 Git 인덱스에서만 파일을 제거합니다. 즉, 과거 커밋에서 해당 파일이 더 이상 추적되지 않도록 합니다.
      • -ignore-unmatch: 지정된 파일이 해당 커밋의 인덱스에 존재하지 않더라도 오류를 발생시키지 않습니다. 이는 과거의 모든 커밋에 해당 파일이 있었던 것은 아닐 수 있기 때문에 유용합니다.
      • {경로/삭제할 파일}: 제거하려는 파일 또는 디렉토리의 경로를 지정합니다. 예를 들어, sensitive_data.txt 또는 build/ 와 같이 사용할 수 있습니다.
  4. -prune-empty: 필터링 후에 내용이 완전히 비어버린 커밋들을 제거합니다. 파일 제거로 인해 커밋이 아무런 변경 사항을 포함하지 않게 될 경우, 이 옵션이 해당 커밋을 히스토리에서 삭제합니다.
  5. -tag-name-filter cat: 태그 이름을 변경하지 않고 그대로 유지합니다. cat 명령어는 입력받은 내용을 그대로 출력하는 역할을 하므로, 태그 이름에 아무런 변화를 주지 않습니다. 이 옵션을 사용하지 않으면 태그도 필터링 대상이 되어 이름이 변경될 수 있습니다.
  6. - --all: 필터링을 적용할 브랜치 및 커밋 범위를 지정합니다. -all은 로컬 저장소의 모든 브랜치(refs/heads/*)와 모든 태그(refs/tags/*)에 대해 필터링을 수행합니다. 특정 브랜치에만 적용하고 싶다면 -main develop 과 같이 브랜치 이름을 명시할 수 있습니다.

동작 방식:

이 명령어는 다음과 같은 과정을 거쳐 저장소의 히스토리를 수정합니다.

  1. Git은 -all 옵션에 지정된 모든 브랜치와 태그를 순회합니다.
  2. 각 커밋에 대해 -index-filter에 지정된 셸 명령어(git rm --cached --ignore-unmatch {경로/삭제할 파일})를 실행합니다. 이 과정에서 해당 커밋의 인덱스에서 지정된 파일이 제거됩니다.
  3. 필터링된 각 커밋의 내용을 기반으로 새로운 커밋을 생성합니다. 이때, 원래 커밋의 메타데이터(작성자, 커밋 메시지 등)는 유지됩니다.
  4. -prune-empty 옵션이 사용되었으므로, 파일 제거로 인해 내용이 비어버린 새로운 커밋은 히스토리에서 제거됩니다.
  5. -tag-name-filter cat 옵션에 따라 원래 태그와 동일한 이름으로 새로운 커밋을 가리키는 태그를 생성합니다.
  6. 원래의 브랜치 및 태그는 임시적으로 백업(refs/original/refs/heads/<branch-name>refs/original/refs/tags/<tag-name>)됩니다.
  7. 새롭게 생성된 커밋들을 기반으로 브랜치와 태그가 업데이트됩니다.


위의 명령어를 적용 후 로컬에서 확인하니 성공적으로 키 관련 커밋 기록이 삭제되었다.

하지만 푸쉬 후에 확인해보니 github repository에서는

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

라는 문구와 함께 커밋 기록을 확인할 수 있었다.

image.png


2. 기존 repository 삭제

원인 : Github 에서는 커밋 기록을 삭제하는 것이 아니라 새로운 커밋을 생성하여 기존 커밋을 덮어쓰는 방식으로 처리하기 때문에, 기존 커밋 캐시가 남아있어 발생

기존 레포지토리를 삭제하고 mirror push 해주었다

이후에 커밋 주소로 들어가면 제대로 삭제된 것을 확인할 수 있었다.

image.png

github support에 지원 메일을 보내서 도움을 받아서 지울 수도 있다고 한다.
https://support.github.com/request

reference

  1. https://devspoon.tistory.com/64
This post is licensed under CC BY 4.0 by the author.