안녕하세요, まさきん입니다.
집에서 돌리는 자동화 스크립트를 GitHub 공개 저장소에 올리려고 준비 중입니다. 그런데 비밀 정보를 점검해 보니, 스캐너는 “유출 없음”이라고 했는데도 2건이 남아 있었습니다. 그 과정을 정리해 둡니다.
※글 작성 시점의 정보입니다. 최신 조건은 공식 사이트에서 확인해 주세요.
- gitleaks는 정규 표현식과 엔트로피로 API 키처럼 “형태로 알아볼 수 있는 비밀”을 찾는 도구입니다
- 형태에 특징이 없는 하드코딩 값은 스캐너를 빠져나갈 수 있습니다
- 예전 .env의 실제 값으로 저장소 전체를 대조하면, 빠져나간 값을 잡아낼 수 있습니다
- 공개 저장소에서는 GitHub Secret Scanning이 무료로 자동 실행됩니다
- 이력에 들어가 버린 비밀은 이력을 다시 쓰기 전에 먼저 키를 폐기하고 재발급해야 합니다
”no leaks”인데도 남아 있던 2건
저희 집에서는 여러 자동화 도구가 돌아가고 있습니다. 설정 파일(.env)은 14개로 나뉘어 있었고, 키 파일도 여기저기 흩어져 있었습니다. 이것을 설정 파일 하나(41개 키)와 credentials 폴더 하나로 통합했습니다. 코드 쪽에서는 약 70개 파일의 참조를 바꿨습니다.
옮긴 파일들을 git에 넣는다고 가정하고 gitleaks를 돌렸습니다. 결과는 “no leaks”였습니다.
그래도 혹시 몰라 예전 .env의 실제 값으로도 대조해 봤습니다. 그랬더니 gitleaks가 잡지 못한 하드코딩 값이 2건 나왔습니다. 하나는 LAN 내부 기기의 접속 코드였고, 다른 하나는 개인적인 파일 이름과 관련된 값이었습니다.
둘 다 API 키처럼 생긴 값은 아닙니다. 스캐너의 동작 방식을 생각하면, 놓친 것도 어찌 보면 자연스러운 일입니다.
gitleaks는 비밀을 어떻게 찾을까
gitleaks는 비밀번호나 API 키 같은 비밀을 찾아내는 도구입니다. git 이력, 파일, 표준 입력을 검사할 수 있습니다.
탐지의 중심은 “규칙”입니다. 규칙마다 정규 표현식이 있어서, 서비스별 토큰의 형태를 확인합니다. 공식 저장소의 기본 설정에는 200개가 넘는 규칙이 들어 있었습니다.
또 하나의 축은 엔트로피입니다. 문자열이 얼마나 무작위인지를 나타내는 수치입니다. 범용 API 키 규칙은 key 같은 단어와 값의 엔트로피를 함께 보고 판정합니다.
정리하면 다음과 같습니다.
| 종류 | gitleaks로 잡히는가 | 이유 |
|---|---|---|
| 주요 서비스의 API 키·토큰 | 잘 잡힘 | 정해진 접두사나 형태가 있음 |
| password= 뒤에 오는 긴 무작위 문자열 | 잘 잡힘 | 키워드와 엔트로피 양쪽에 해당 |
| 숫자로만 된 짧은 접속 코드 | 잘 안 잡힘 | 형태에도 무작위성에도 특징이 적음 |
| 내부 URL·호스트명·파일명 | 잘 안 잡힘 | 일반 문자열과 구분이 안 됨 |
| 키 이름 없이 하드코딩된 값 | 잘 안 잡힘 | 단서가 될 단어가 없음 |
설치와 기본 명령어
공식 README에서는 Homebrew, Docker, Go 빌드로 설치할 수 있다고 안내합니다. Mac이라면 다음과 같습니다.
brew install gitleaks
주의할 점은 명령어 이름입니다. 오래된 글에 나오는 detect와 protect는 v8.19.0에서 사용 중단(deprecated)되었습니다. 지금은 다음 세 가지 모드가 기본입니다.
| 명령어 | 대상 | 쓰임새 |
|---|---|---|
| gitleaks git | git 이력(git log -p의 diff) | 커밋된 내용을 과거까지 거슬러 올라가 검사 |
| gitleaks dir | 디렉터리나 파일 | 아직 git에 넣지 않은 파일을 검사 |
| gitleaks stdin | 표준 입력 | 다른 명령어의 출력을 그대로 검사 |
자주 쓰는 형태는 다음과 같습니다.
# 현재 저장소의 이력을 검사
gitleaks git -v
# 공개할 폴더를 git에 넣기 전에 검사
gitleaks dir -v ./my-scripts
유출이 발견되면 종료 코드는 1, 아무것도 없으면 0입니다. 스크립트에서 중단 여부를 판단하는 데 쓸 수 있습니다.
오탐 처리
테스트용 더미 값 등으로 오탐이 나올 때도 있습니다. 공식 README에 나온 방법은 세 가지입니다.
- 해당 줄에
gitleaks:allow주석을 붙인다 - 리포트의 Fingerprint를
.gitleaksignore에 적어 그 1건만 무시한다 - 설정 파일
.gitleaks.toml의 allowlist로 경로나 정규 표현식 단위로 제외한다
allowlist는 v8.25.0에서 [[allowlists]]라는 표기로 바뀌었습니다. 예전 설정 예시를 그대로 옮겨 쓸 때는 주의가 필요합니다.
개인적으로는 Fingerprint로 한 건씩 무시하는 쪽이 안전하다고 봅니다. 경로 단위로 제외해 버리면, 나중에 진짜 비밀이 들어와도 알아차릴 수 없습니다.
pre-commit으로 커밋 전에 막기
평소에 막아 주는 장치로, gitleaks는 pre-commit 훅으로도 쓸 수 있습니다.
- pre-commit을 설치한다
- 저장소 루트의
.pre-commit-config.yaml에 아래 내용을 적는다 pre-commit autoupdate와pre-commit install을 실행한다
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
GitHub의 Secret Scanning과 Push Protection
GitHub 쪽에도 비밀을 찾아내는 기능이 있습니다. 공개 저장소에서는 다음과 같이 동작합니다.
| 기능 | 공개 저장소에서의 취급 | 하는 일 |
|---|---|---|
| Secret Scanning | 무료로 자동 실행 | 모든 브랜치의 git 이력을 검사하고, 발견되면 알림 |
| Push protection for users | 계정 단위로 기본 활성화 | 공개 저장소에 비밀이 포함된 push를 차단 |
| Push protection for repositories | GitHub Secret Protection 필요 | 저장소 단위로 push를 차단 |
Secret Scanning은 Issue나 Pull Request 본문도 검사합니다. 제휴 서비스의 키가 발견되면 해당 서비스에도 알림이 갑니다.
다만 둘 다 기본적으로는 “형태로 알아볼 수 있는 비밀”이 대상입니다. 형태가 없는 값까지 맡겨 두는 건 조금 위험할 수 있습니다.
.env 분리와 .gitignore
가장 효과적인 방법은 비밀을 코드에 쓰지 않는 것입니다. GitHub 공식 문서도 환경 변수 사용을 권장합니다.
제가 설정을 하나로 모은 것도 같은 생각에서였습니다. 공개할 때의 구성은 다음과 같습니다.
| 파일 | 저장소에 넣는가 | 내용 |
|---|---|---|
| .env | 넣지 않음 | 실제 값 |
| .env.example | 넣음 | 키 이름과 더미 값 또는 설명 |
| .gitignore | 넣음 | .env와 credentials 폴더 제외 설정 |
| credentials 폴더 | 넣지 않음 | 키 파일 등 |
.env.example을 두면 어떤 키가 필요한지는 공개하면서 값은 숨길 수 있습니다.
다만 .gitignore에는 함정이 있습니다. git 공식 문서에 나와 있듯이, 이미 추적 중인 파일에는 효과가 없습니다. 한 번 커밋한 파일은 git rm --cached로 추적에서 빼야 합니다.
도구를 정리하듯이, 통신비 같은 고정비도 한 번 정리해 두면 전체가 한눈에 들어올 수 있습니다.
실제 값으로 대조하는 자체 점검
이번 글에서 가장 전하고 싶은 부분입니다. 스캐너는 “형태”로 찾습니다. 그래서 내가 가진 “값”으로 찾는 방법을 더합니다. 예전 .env의 값 목록이 저장소 안에 나타나는지 검사하는 방식입니다.
- 예전 .env에서 값 부분만 뽑아 한 줄에 하나씩 적은 파일을 만든다
- 빈 줄과, 너무 짧아서 오탐이 날 값(true나 1 등)을 뺀다
- 값 목록은 저장소 바깥에 둔다
grep -F로 정규 표현식이 아닌 고정 문자열로 대조한다- git 이력에도 같은 대조를 돌린다
값 목록을 ~/private/values.txt에 둔 경우의 예입니다.
# 작업 트리 전체를 대조(.git 제외)
grep -rnF -f ~/private/values.txt --exclude-dir=.git .
# 이력의 모든 커밋을 대조
git grep -nF -f ~/private/values.txt $(git rev-list --all)
-F를 붙이면 기호가 들어간 값도 문자 그대로 찾을 수 있습니다. 값 목록에 빈 줄이 있으면 모든 줄에 걸리니 주의하세요.
값 목록은 “공개하지 않을 곳”에 둡니다. 대조가 끝나면 지우거나, 잠금이 걸린 곳으로 되돌려 두면 안심입니다.
치환 스크립트로 이중 치환된 이야기
직접 손을 움직이다 걸려 넘어진 일이 하나 더 있습니다. 약 70개 파일의 참조를 치환 스크립트로 바꿨을 때입니다.
새 문자열이 옛 문자열을 포함하는 경우가 3건 있었습니다. 예를 들어 “config”를 “config_shared”로 바꾸는 식입니다. 이 경우 치환을 두 번 돌리면 “config_shared_shared”가 되어 버립니다.
저는 한 번 원래대로 되돌린 뒤 다시 했습니다. 교훈은 단순합니다. 치환하기 전에 “새 문자열 안에 옛 문자열이 들어 있지 않은지” 확인할 것. 들어 있다면 경계를 지정해서 한 번만 치환하는 게 좋겠습니다.
다시 한 뒤 로드 확인을 한 18개는 모두 정상이었습니다.
이력에 들어가 버렸을 때의 순서
혹시 비밀을 커밋해 버렸다면 순서가 중요합니다. GitHub 공식 문서는 먼저 키의 폐기·재발급(로테이션)을 요구합니다.
| 순서 | 할 일 | 이유 |
|---|---|---|
| 1 | 키나 비밀번호를 폐기·재발급한다 | 옛 값을 못 쓰게 되면 피해는 거기서 멈춤 |
| 2 | 코드를 .env에서 읽도록 고친다 | 새 값을 하드코딩하지 않기 위해 |
| 3 | 필요하면 git filter-repo로 이력을 다시 쓴다 | 옛 값을 이력에서 지우기 위해 |
| 4 | force push하고, 필요하면 GitHub 지원팀에 연락한다 | 캐시나 Pull Request의 참조를 지우기 위해 |
공식 문서에서는 로테이션만으로 충분한 경우도 있다고 합니다. 이력을 다시 쓰면 커밋 해시가 바뀌고, 포크에 남은 것은 지울 수 없기 때문입니다.
이력을 다시 쓸 때 쓰는 도구는 git filter-repo입니다. --sensitive-data-removal을 쓸 수 있는 버전(2.47 이상)이 필요하다고 합니다. 값을 치환하는 경우는 다음과 같습니다.
git-filter-repo --sensitive-data-removal --replace-text ../passwords.txt
공개 저장소에 한 번이라도 노출된 비밀은 누군가 이미 가져갔다고 전제하고 생각합니다. 이력을 지우기보다 먼저 키를 폐기하고 새것으로 바꾸는 것이 공식 순서입니다.
흔한 함정
- gitleaks의 “no leaks”를 비밀이 하나도 없다는 증명으로 여긴다
gitleaks git만 쓰고, 아직 커밋하지 않은 파일 검사를 잊는다- .gitignore에 적었는데, 이미 추적 중인 파일이 남아 있다
- 값 목록이나 치환 목록을 저장소 안에 둔다
- 치환의 새 문자열과 옛 문자열이 부분 일치해서 이중으로 치환된다
- 이력만 다시 쓰고 키 로테이션을 잊는다
첫 번째가 바로 이번 제 이야기입니다. 스캐너는 정말 믿음직한 도구입니다. 하지만 보는 건 “형태”뿐입니다. 값 대조와 함께 쓰는 게 안심이라고 생각합니다.
규칙을 정하기만 하지 않고 직접 세어서 확인한 이야기는 164건의 태스크를 세어 보고 알게 된 것에도 썼습니다. 비밀 점검도 같은 구도일지 모릅니다.
시작하기 전에 확인할 것
- 비밀이 어느 파일에 몇 개 있는지 목록으로 정리했는가
- .env와 키 파일을 한곳에 모았는가
- .gitignore에 .env와 credentials 폴더가 들어 있는가
- 이미 추적 중인 비밀 파일을 git rm --cached로 뺐는가
- gitleaks의 git과 dir을 모두 돌렸는가
- 예전 .env 값 목록으로 작업 트리와 이력을 대조했는가
- 값 목록을 저장소 바깥에 두었는가
- GitHub의 Push protection for users가 켜져 있는가
제 공개 저장소 작업은 아직 진행 중입니다.
자주 묻는 질문
Q. gitleaks에서 “no leaks”가 나오면 공개해도 괜찮나요?
형태로 알아볼 수 있는 비밀에 대해서는 꽤 안심해도 된다고 봅니다. 다만 짧은 코드나 내부 파일명은 잡히지 않을 수 있습니다. 값 목록 대조를 함께 하는 걸 추천합니다.
Q. gitleaks detect와 gitleaks git은 무엇이 다른가요?
detect는 v8.19.0에서 사용 중단된 옛 명령어입니다. 지금은 이력을 검사할 때는 git, 폴더를 검사할 때는 dir을 씁니다. 옛 명령어도 동작은 하지만 도움말에는 표시되지 않습니다.
Q. 개인 공개 저장소에서도 Secret Scanning을 쓸 수 있나요?
쓸 수 있습니다. 공식 문서에 따르면 공개 저장소에서는 무료로 자동 실행됩니다. 모든 브랜치의 이력과 함께 Issue나 Pull Request 본문도 검사 대상입니다.
Q. Push Protection은 직접 설정해야 하나요?
계정 단위의 Push protection for users는 기본으로 켜져 있습니다. 공개 저장소에 비밀이 포함된 push를 막아 줍니다. 설정의 “Code security”에서 상태를 확인할 수 있습니다.
Q. .gitignore에 .env를 적으면 충분한가요?
아직 커밋하지 않았다면 그것으로 추적되지 않습니다. 하지만 이미 추적 중인 파일에는 효과가 없습니다. git rm --cached로 뺀 다음, 이력에 남아 있지 않은지도 확인해야 합니다.
Q. 비밀을 커밋해 버렸다면 무엇부터 해야 하나요?
키나 비밀번호의 폐기·재발급이 먼저입니다. 이력을 다시 쓸지는 그다음에 판단합니다.
참고한 공식 자료
- gitleaks(GitHub 저장소·README)
- About secret scanning(GitHub Docs)
- About push protection(GitHub Docs)
- Push protection for users(GitHub Docs)
- 저장소에서 민감한 데이터 제거(GitHub Docs)
- git-filter-repo(GitHub 저장소)
- gitignore(Git 공식 문서)
- pre-commit(공식 사이트)
마무리
gitleaks는 형태로 알아볼 수 있는 비밀을 찾는 데 정말 믿음직한 도구였습니다. 반면 형태에 특징이 없는 값은 “no leaks” 뒤에 남아 있을 수 있습니다.
그러니 스캐너와 실제 값 대조를 함께 쓰는 게 좋겠습니다. 중복 구독 찾기와 마찬가지로, 관점을 하나 더하면 보이는 것이 달라집니다. 공개 버튼을 누르기 전에 손에 있는 .env를 한 번 열어 보세요.
이 글에는 제휴 광고가 포함되어 있습니다. 이 사이트의 링크를 통해 상품·서비스에 신청하시면 제휴사로부터 보상을 받을 수 있습니다. 또한 운영자는 라쿠텐 그룹 소속 직원으로, 사원 추천 프로그램을 통해 보상을 받는 경우가 있습니다. 기사 내용과 평가는 광고 유무와 관계없이 운영자의 실제 경험과 조사를 바탕으로 작성했지만, 위 관계를 이해하신 후 읽어 주시기 바랍니다. 자세한 내용은 면책조항·제휴 링크 안내를 참고해 주세요. 면책조항·제휴 링크 안내
이 글에는 기계 번역이 포함되어 있습니다. 정확한 조건은 라쿠텐 모바일 공식 사이트의 다국어 페이지에서 확인해 주세요.
라쿠텐 모바일의 통신 지역·전파 상황이 궁금하신 분은, 공식 전파 개선·조사 요청 양식 을 통해 문의하실 수 있습니다.