지운 비밀값은 왜 살아 있는가
한 줄 요약
비밀값이 Git 에 한 번 들어가면 지우는 것으로는 끝나지 않습니다. 가장 먼저 할 일은 그 값을 무효로 만드는 것(회전)이고, 기록 정리와 재발 방지는 그다음입니다.
왜 이게 문제인가
Git 은 파일이 아니라 스냅숏을 저장합니다. 커밋 하나는 그 순간 저장소 전체의 모습이고, 다음 커밋에서 파일을 지워도 앞 커밋은 그대로 남습니다. 화면에서 사라졌다는 것과 저장소에서 사라졌다는 것은 다른 사실입니다.
게다가 저장소는 한 곳에만 있지 않습니다. 동료의 노트북, CI 러너의 캐시, 포크, 미러, 백업에 같은 객체가 복제됩니다. 공개 저장소라면 자동으로 비밀값을 긁어 가는 수집기가 몇 분 안에 지나갑니다. 그래서 "바로 지웠으니 괜찮다" 는 판단은 거의 항상 틀립니다. 토큰이 노출된 순간부터 그 토큰은 이미 다른 사람의 손에 있다고 가정해야 합니다.
어떻게 동작하나
대응은 네 박자입니다.
| 순서 | 할 일 | 이유 |
|---|---|---|
| 1 | 찾기 | 작업 트리만이 아니라 모든 브랜치와 태그의 기록을 훑는다 |
| 2 | 회전 | 새 값을 발급하고 옛 값을 거절하게 만든다. 서비스가 새 값을 실제로 읽었는지 확인한다 |
| 3 | 정리 | 기록을 다시 쓰고 원격에 덮어 올린 뒤, 참조를 잃은 옛 객체까지 지운다 |
| 4 | 재발 방지 | 커밋 직전과 CI 에서 같은 검사를 돌린다 |
찾기 도구로는 gitleaks 를 씁니다. 정규식과 엔트로피로 비밀값처럼 생긴 문자열을 찾는데, 회사마다 토큰 형식이 다르므로 기본 규칙에 사내 규칙을 더하는 설정 파일(.gitleaks.toml)을 저장소에 둡니다. 같은 도구가 작업 트리만 보는 모드, 기록 전체를 보는 모드, 스테이지만 보는 모드를 갖고 있어서 세 자리를 같은 규칙으로 지킬 수 있습니다.
회전에는 함정이 하나 있습니다. 설정 파일을 바꿔도 서비스가 그 값을 기동할 때 한 번만 읽었다면, 서비스는 메모리에 든 옛 값을 계속 받습니다. 그래서 회전의 완료 조건은 "파일을 바꿨다" 가 아니라 "옛 값으로 보낸 요청이 거절된다" 입니다.
기록 정리는 git filter-repo 로 합니다. 모든 커밋을 다시 만들기 때문에 커밋 해시가 바뀌고, 이미 클론한 사람은 새로 받아야 합니다. 원격에 강제로 올려도 원격 서버에는 참조를 잃은 옛 객체가 한동안 남습니다. 직접 운영하는 서버라면 정리(gc)를 돌리고, 호스팅 서비스라면 그 서비스의 절차에 따라 지원 요청을 해야 합니다.
현장에서 만나는 모습
가장 흔한 사고는 시험용 설정 파일입니다. 로컬에서 빨리 확인하려고 .env 에 운영 토큰을 넣었다가 git add . 한 번에 함께 올라갑니다. 지운 커밋이 바로 뒤따르니 리뷰에서도 잘 보이지 않습니다. 태그가 붙어 있으면 브랜치 기록을 아무리 다시 써도 태그가 옛 커밋을 붙잡고 있어 토큰이 살아남습니다.
두 번째는 로그입니다. 토큰을 접근 로그에 그대로 찍는 서비스가 많습니다. 로그에는 원문 대신 지문(해시의 앞부분)을 남기면, 유출된 토큰이 언제 어디서 쓰였는지 추적할 수 있으면서 로그 자체가 새로운 유출원이 되지 않습니다.
고친 뒤 무엇으로 확인하나
비밀값 사고는 "했다" 가 아니라 "확인했다" 로 닫아야 합니다. 확인할 것은 네 가지입니다.
- 옛 값으로 보낸 요청이 실제로 거절되는가. 설정 파일을 열어 보는 것이 아니라 요청을 보내 봅니다.
- 새 값으로 서비스가 정상 동작하는가. 회전하다가 서비스를 멈추면 다음 회전을 아무도 하지 않으려 합니다.
- 작업 저장소와 원격 저장소의 모든 객체에 옛 값이 없는가. 브랜치 기록만 보면 태그와 참조를 잃은 객체를 놓칩니다.
- 옛 값이 노출된 동안 누가 썼는가. 접근 기록에서 옛 값의 지문을 찾아 사용 주체와 시각을 정리하고, 필요하면 그 주체가 가져간 데이터의 범위를 사고 보고서에 적습니다.
마지막 항목이 빠지면 사고의 크기를 모른 채 끝납니다. 토큰을 죽인 것은 앞으로의 피해를 막은 것이고, 이미 일어난 피해는 기록을 읽어야 보입니다.
English corner
공식 문서와 보안 권고에서 자주 보는 표현입니다.
- rotate the credential: 자격 증명을 새로 발급하고 옛것을 폐기한다. "revoke" 는 폐기 쪽에 힘을 준 말입니다.
- assume it has been compromised: 이미 남의 손에 있다고 가정하라. 유출 대응 문서가 거의 첫 줄에 적는 문장입니다.
- rewrite history: 기록을 다시 쓴다. 이 말이 나오면 커밋 해시가 바뀐다는 뜻입니다.
- pre-commit hook: 커밋 직전에 도는 검사. 실패하면 커밋이 만들어지지 않습니다.
다음 실습에서 할 것
토큰이 기록과 태그에 남은 결제 클라이언트 저장소를 받습니다. 기록에서 토큰을 찾고, 그 토큰으로 고객 목록이 실제로 열리는 것을 본 뒤, 토큰을 회전하고 기록과 원격에서 지웁니다. 마지막으로 훅으로 재발을 막고, 저널에서 이미 누가 그 토큰을 썼는지 찾습니다.
한 걸음 더 — 범위를 먼저 정한다
유출 대응에서 가장 먼저 흔들리는 것은 '무엇이 새었나' 입니다. 한 저장소의 토큰 하나만 샜는지, 그 토큰으로 접근 가능한 다른 자원까지 번졌는지 범위를 긋지 못하면 회전해야 할 목록이 끝없이 늘어납니다. 그래서 토큰마다 '이것이 열 수 있는 것' 을 미리 적어 두면(권한 목록), 유출 순간 회전 대상과 조사 범위가 즉시 정해집니다. 반대로 한 비밀값을 여러 서비스가 공유하고 있으면 그 하나가 샜을 때 전부를 회전해야 하므로, 평소에 비밀값을 서비스별로 나눠 두는 것이 사고의 비용을 미리 줄이는 일입니다.