배포된 .git 경로로 인증키가 유출된 사건 추적기
들어가며
운영을 하다 보면 내 손으로 짠 것도 아니고, 우리 팀이 담당하는 것도 아닌 사건이 옆에서 터지는 걸 목격할 때가 있다. 그럴 때가 오히려 공부가 된다. 남의 불이라 마음은 급하지 않은데, 원인을 뜯어보면 “어? 이거 우리도 똑같이 당할 수 있는데?” 싶은 것들이 나오기 때문이다.
이번 글은 다른 팀에서 운영하던 서비스의 인증키가 유출된 사건을 곁에서 지켜보며 추적하고 정리한 기록이다. 결론부터 말하면, 소스 코드에 하드코딩된 키가 새어나간 것도 아니고, .env 파일이 털린 것도 아니었다. 배포 과정에서 .git 디렉터리가 웹 루트에 그대로 딸려 올라간 것이 원인이었다.
이 글에서 정리하고 싶은 건 세 가지다.
.git경로가 왜 유출될 수밖에 없었는지 (배포 방식의 구조적 문제)- 그 노출된 경로를 누가, 어떻게 긁어가는지 (자동화된 스캐닝 봇의 동작)
- 당시 어떻게 조치했고, 앞으로 무엇을 막아야 하는지
이 사건은 우리 팀 소관이 아니었고, 실제 조치는 해당 서비스 팀과 팀장님이 진행했다. 이 글은 그 과정을 곁에서 추적하며 학습한 내용을 재구성한 것으로, 구체적인 서비스명·계정·키 값 등 민감 정보는 모두 배제했다.
사건의 시작: Cloud Insight 알림
시작은 NCP Cloud Insight에서 날아온 알림 메일이었다. 내용 중에 낯선 GitHub 저장소 경로가 하나 박혀 있었다. 특정 커밋 해시가 포함된 blob URL 형태였고, “노출된 것으로 탐지된 경로”라는 맥락으로 붙어 있었다.
그 URL을 따라 들어가 보니, 평범한 오픈소스 저장소가 아니었다.
- 저장소 설명은
scoopin fish from the sea— “바다에서 물고기 건지기”. 인터넷을 훑어 노출된 무언가를 긁어모은다는 은유였다. - 이름부터
dragnet(저인망, 수사망). - 언어는 Go 100%, 스캐너로 보이는 단일 실행 파일과, 긁어온 대상 목록으로 추정되는 압축 CSV 파일.
.github/workflows아래에 스케줄 워크플로우 — 즉 GitHub Actions로 주기적으로 자동 실행되는 구조.
정리하면, 이건 공개된 웹사이트들을 주기적으로 훑으면서 노출된 민감 경로를 자동으로 수집하는 봇이었다. 우리 쪽 서비스가 그 그물에 걸린 것이다.
이런 스캐닝 봇 저장소는 대개 공개(public)로 대놓고 운영된다. 소스를 여기 옮기지는 않았다 — 공격 도구를 재현·배포하는 셈이 되기 때문이다. 이 글은 철저히 방어자 관점에서, “이런 봇이 무엇을 노리는가”만 다룬다.
스캐닝 봇은 무엇을 노리나
이런 유형의 봇이 노리는 건 화려한 취약점이 아니다. 설정 실수로 웹에 그대로 노출된, 있어서는 안 될 경로다. 대표적인 표적이 두 가지다.
1) .env
애플리케이션 루트에 놓인 환경변수 파일. DB 접속 정보, API 키, 시크릿이 평문으로 들어있는 경우가 많다. 웹 루트에 .env가 그대로 배포되고 웹서버가 이를 정적 파일로 서빙하면, https://사이트/.env 한 방으로 전부 새어나간다.
2) .git/
이번 사건의 진범. .git 디렉터리는 저장소 전체를 복원할 수 있는 모든 정보를 담고 있다.
| 경로 | 담긴 정보 |
|---|---|
.git/config | remote URL (토큰이 박힌 경우도 있음), 사용자 설정 |
.git/HEAD, .git/refs | 브랜치·커밋 참조 |
.git/logs/HEAD | 커밋 히스토리, 작성자 이메일, 시각 |
.git/index | 추적 중인 전체 파일 목록 |
.git/objects/ | 압축된 소스 객체 — 소스 코드 전체 복원 가능 |
즉 .git이 통째로 노출되면 공격자는 git clone에 준하는 방식으로 소스 전체 + 커밋 이력 + 설정값을 그대로 가져갈 수 있다. 소스 안에 하드코딩된 키가 하나라도 있으면 그것까지 딸려 나간다.
이번 사건에서 개발자 확인 결과 .env에는 실제로 아무것도 들어있지 않았다. 그래서 유출 경로는 소거법으로 .git으로 좁혀졌다.
핵심: .git은 왜 유출될 수밖에 없었나
여기가 이 글에서 가장 중요한 부분이다. 어떤 배포 방식은 구조적으로 .git을 노출시킨다.
왜 어떤 배포는 괜찮고, 어떤 배포는 새는가
핵심은 단 하나다. “소스 저장소를 통째로 웹 루트에 뿌리느냐, 빌드 산출물만 올리느냐.”
상대적으로 안전한 방식 — 산출물만 배포
Jenkins 같은 도구로 빌드할 때는 보통 이런 흐름을 탄다.
- 빌드 서버의 워크스페이스에서 소스를 checkout ← 여기엔
.git이 있다 - 빌드/컴파일 수행
- 결과물(artifact)만 골라서 배포 대상에 전달 (
git archive,rsync --exclude=.git, 산출물만 tar 등)
이 경우 .git은 빌드 서버 워크스페이스에만 남고, 실제 서비스가 서빙되는 웹 루트에는 올라가지 않는다. 그래서 외부에서 .git에 접근할 방법이 없다.
위험한 방식 — 저장소를 통째로 checkout 해서 배포
문제는 “소스 저장소 자체를 배포 대상 경로에 그대로 펼치는” 파이프라인이다. NCP의 SourceCommit → SourceDeploy(CodeDeploy) 조합을 이런 식으로 구성하면 이렇게 흘러간다.
- SourceCommit(또는 GitHub)에서 저장소를 통째로 clone/checkout
- 그 디렉터리를 그대로 배포 대상 경로(= 웹 문서 루트)에 복사
- 이때
.git/디렉터리가 소스와 함께 웹 루트 안으로 딸려 들어감 - 웹서버(Nginx/Apache)는
.git/이하를 그냥 정적 파일로 서빙 - 외부에서
https://사이트/.git/config,/.git/HEAD등에 접근 가능
즉 “배포 경로 = 웹 루트”이고, 그 경로에 .git이 포함된 채로 올라간 것이 근본 원인이다. 코드 한 줄 잘못 짠 게 아니라, 배포 파이프라인의 구조가 통째로 소스를 뿌리도록 되어 있던 것이 문제였다.
정리하면: Jenkins라서 안전하고 SourceDeploy라서 위험한 게 아니다. “산출물만 배포하느냐 / 저장소를 통째로 배포하느냐”가 갈림길이다. 어떤 도구든 저장소를 웹 루트에 통째로 펼치면
.git은 샌다.
노출을 유발하는 전형적인 배포 스크립트 형태
말로만 하면 와닿지 않으니, “이렇게 하면 샌다”는 안티패턴을 짚어둔다. (실제 유출 스크립트가 아니라, 이 사건과 같은 결과를 만드는 전형적 형태를 재구성한 것이다.)
1
2
3
4
5
6
7
# ❌ 안티패턴: 저장소 전체를 웹 루트로 그대로 복사
git clone https://source.repo/service.git /var/www/service
# → /var/www/service/.git/ 이 그대로 웹으로 노출됨
# ❌ 안티패턴: 체크아웃 디렉터리를 통째로 rsync (.git 미제외)
rsync -av ./checkout/ /var/www/service/
# → checkout/.git 이 함께 넘어감
1
2
3
4
5
6
# ❌ 안티패턴: 배포 설정에서 소스 루트를 통째로 목적지로 지정
# (appspec 류 설정에서 source: / → destination: 웹 루트)
files:
- source: /
destination: /var/www/service
# → 저장소 루트의 .git 까지 destination 으로 복사됨
공통점은 “복사 대상에서 .git을 골라내는 단계가 없다”는 것이다. 이 한 단계의 부재가 저장소 전체 유출로 이어진다.
누가 이걸 긁어가나 — 자동화된 수집
여기서 무서운 건, 이런 실수를 노리는 자동화가 이미 상시 돌고 있다는 점이다. 사람이 하나하나 찍어보는 게 아니다.
이번에 걸린 dragnet 류 봇의 동작을 방어자 관점에서 재구성하면 대략 이렇다.
- 어딘가에서 확보한 도메인/호스트 목록을 대상 풀로 둔다.
- GitHub Actions 스케줄러로 주기적으로(수십 분 단위) 배치를 돌린다.
- 각 대상에 대해
/.env,/.git/config,/.git/HEAD같은 정해진 노출 경로들을 순서대로 찔러본다. - 응답이 오면(= 노출되어 있으면) 그 내용을 파싱해 민감 정보를 수집·적재한다.
핵심은 이 전 과정이 완전 자동이고, 저비용이며, 24시간 돈다는 것이다. 내 서비스가 특별히 표적이 된 게 아니라, “노출된 채로 인터넷에 떠 있으면 언젠가는 반드시 걸린다”에 가깝다. 노출과 수집 사이의 시간차는 생각보다 짧다.
그래서 “커밋에서 지웠으니 됐다”는 통하지 않는다. 노출되어 있던 그 시점에 이미 긁혔다고 가정해야 한다.
실제 조치 — 계정 교체와 배포 수정
당시 대응은 두 갈래로 진행됐다.
1) 팀장님: 유출된 키의 계정 자체를 폐기·재생성
노출된 키 값을 “그 자리에서 무효화”하는 걸 넘어서, 해당 키가 물려 있던 계정 자체를 삭제하고 새 계정을 만들어 교체했다. 이게 정석이다. 이유는 명확하다.
- 노출된 시점에 봇이 이미 키를 가져갔다고 봐야 한다 → 키 값 변경만으로는 불안하다.
.git이 통째로 샜다면 그 키 하나만 샜는지 확신할 수 없다. 히스토리·설정에 다른 크레덴셜이 남아있었을 수 있으니, 영향받은 자격증명 단위를 통째로 갈아엎는 게 안전하다.
원칙: 한 번 노출된 크레덴셜은 되돌릴 수 없다. “값만 바꾸기”가 아니라 “폐기 후 재발급”, 필요하면 계정 단위 교체까지가 기본이다.
2) 개발자: 배포 시 .git이 안 올라가도록 수정
원인이 배포 파이프라인이었으니, 근본 수정도 거기서 이뤄졌다. 배포 산출물에서 .git(및 불필요한 숨김 파일)을 제외하도록 배포 과정을 바꿨다.
재발 방지 — 세 겹의 방어
이 사건을 우리 팀에도 대입해보며 정리한, 세 겹의 방어선이다. 하나만 해도 이번 사건은 안 났겠지만, 보안은 겹으로 쌓아야 한다.
1겹. 배포 산출물에서 .git 제외 (근본 원인 차단)
가장 중요한 방어선. 애초에 웹 루트에 .git이 올라가지 않게 한다.
1
2
3
4
5
# ✅ rsync 시 .git / .env 등 제외
rsync -av --exclude='.git' --exclude='.env' ./build/ /var/www/service/
# ✅ 아카이브로 추적 파일만 배포 (.git 자체가 포함되지 않음)
git archive --format=tar HEAD | tar -x -C /var/www/service/
1
2
3
# ✅ 부득이 저장소를 통째로 배포한다면, 배포 후 훅에서 제거
# (SourceDeploy/CodeDeploy hooks 등)
rm -rf /var/www/service/.git
2겹. 웹서버에서 숨김 경로 접근 차단 (2차 안전망)
만에 하나 .git이 올라가더라도, 외부에서 못 읽게 막는다. Nginx 예시:
1
2
3
4
5
# .git, .env, .svn, .hg 등 숨김 경로 접근 차단
location ~ /\.(git|env|svn|hg) {
deny all;
return 404;
}
Apache라면 .htaccess 또는 vhost 설정에서 동일하게 .git 이하를 거부하도록 둔다. 이건 배포 실수를 덮어주는 안전망이지, 1겹을 대체하지 않는다.
3겹. 노출 시 크레덴셜 즉시 폐기 + 탐지 체계
이미 새어나간 뒤라면:
- 노출됐던 키·토큰은 값 변경이 아니라 폐기·재발급. 영향 범위가 불확실하면 계정 단위 교체.
- 커밋에서 지우는 것으로 끝내지 말 것. 필요하면
git filter-repo/ BFG로 히스토리까지 제거하되, 그것과 무관하게 키는 이미 노출된 것으로 간주. - 사전 예방으로 secret scanning(GitHub push protection,
gitleaks,git-secretspre-commit 훅)을 걸어 애초에 키가 커밋되지 않게 한다. - Cloud Insight 같은 외부 노출 탐지 알림을 살려두기 — 이번에도 결국 이 알림이 사건을 가장 먼저 잡아냈다.
마치며
이 사건의 교훈은 의외로 단순하다.
민감 정보가 담긴 경로는, 외부에서 절대 보이면 안 된다.
.env도,.git도.
그런데 이 단순한 원칙이 깨지는 지점은 코드가 아니라 배포 파이프라인의 구조였다. “소스를 통째로 웹 루트에 펼치는” 배포는, 겉으로는 잘 돌아가는 것처럼 보여도 .git이라는 문을 열어둔 채 서비스되고 있었던 셈이다.
남의 팀 사건이었지만, 나는 이걸 우리 파이프라인을 다시 점검하는 계기로 삼았다. 배포 대상에 .git이 섞여 나가진 않는지, 웹서버에서 숨김 경로가 막혀 있는지 —.
인터넷에는 지금 이 순간에도 저인망이 돌고 있다. 그물에 걸리지 않는 유일한 방법은, 애초에 바다에 미끼를 흘리지 않는 것이다.
참고
- Exposed Git Configurations: A Rising Threat (LinkedIn, MSP/Cloud Security)
.git폴더 노출로 인한 소스코드 유출 사례 정리 (sparkit.tistory.com/47)