카테고리 없음

웹 서버에 .git 디렉토리가 노출되면 벌어지는 일

바뿌 2026. 10. 27. 15:24

소스를 git으로 관리하면서 저장소를 웹 루트에 그대로 두면, 아무나 /.git/ 주소로 접근할 수 있습니다.

저도 설정을 바꾸다가 이 상태를 만들었고, 확인해보니 그대로 열려 있었습니다.

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.git/config
# 200    ← 열려 있다

결론부터 — 뭐가 털리나

.git 디렉토리에는 소스 전체와 모든 이력이 들어 있습니다. 노출되면 이것들이 나갑니다.

파일 내용
.git/config 저장소 주소, remote URL에 토큰이 박혀 있으면 그대로
.git/HEAD, refs/ 브랜치 구조
.git/objects/ 소스 코드 전부. 삭제한 파일까지
.git/logs/HEAD 커밋 이력, 작업자 이름·이메일

가장 위험한 건 objects입니다. 한 번이라도 커밋했다면 그 안에 남아 있습니다. 나중에 지운 설정 파일, 실수로 커밋했다가 되돌린 비밀번호까지 복원됩니다.

"커밋했다가 바로 지웠으니 괜찮다"는 통하지 않습니다. git은 지워도 남깁니다.

왜 생기나

의도적으로 열어두는 사람은 없습니다. 대개 이런 경로입니다.

  • 서버에서 git clone 으로 배포하고 웹 루트가 곧 저장소인 경우
  • git pull 로 업데이트하는 구조
  • 소스를 git으로 관리하려고 구조를 바꾸다가

로컬 개발 환경에서는 문제가 안 보입니다. 배포하고 나서야 생깁니다.

확인하는 법

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.git/config
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.git/HEAD

404나 403이어야 정상입니다. 200이 나오면 열려 있는 겁니다.

내용까지 보이는지 확인하려면:

curl -s https://example.com/.git/config

[remote "origin"] 같은 게 그대로 보이면 심각한 상태입니다.

같은 방식으로 /.env, /wp-config.php, /composer.json, /.github/ 도 확인해보세요.
하나가 열려 있으면 보통 다른 것도 열려 있습니다.

막는 법

Caddy

@forbidden {
    path /.git /.git/* /.env /wp-config.php /composer.json /package.json
}
respond @forbidden 404

Apache (.htaccess)

RedirectMatch 404 /\.git
<FilesMatch "^\.">
  Require all denied
</FilesMatch>
<Files wp-config.php>
  Require all denied
</Files>

nginx

location ~ /\. {
    deny all;
    return 404;
}

403보다 404가 낫습니다. 403은 "있는데 막혔다"를 알려주지만, 404는 존재 자체를 숨깁니다.

⚠️ 설정했는데 반영이 안 될 때

막는 규칙을 넣었는데 여전히 200이 나왔습니다. 설정이 틀린 줄 알고 한참 고쳤습니다.

원인은 프록시를 재시작하지 않은 것이었습니다. 설정 파일을 컨테이너에 마운트해 쓰고 있었는데, 파일을 고쳐도 프로세스가 다시 읽지 않습니다.

docker compose restart caddy    # 이게 필요했습니다

설정을 바꿨으면 반드시 다시 측정하세요. 바꿨다는 사실이 적용됐다는 뜻은 아닙니다.

비슷한 일을 도메인 설정에서도 겪었습니다.

설정 화면에서 값을 입력하고 창을 닫았는데, 저장 버튼을 누르지 않았습니다. 입력한 걸 봤으니 당연히 반영됐을 거라 생각하고 한참 결과만 확인했습니다. 당연히 아무것도 바뀌지 않았습니다.

"입력했다"와 "저장했다"와 "적용됐다"는 전부 다른 단계입니다. 그 사이 어디에서든 끊길 수 있습니다.

더 나은 구조 — 아예 두지 않기

막는 것보다 웹 루트에 저장소를 두지 않는 게 근본적입니다.

방식 설명
빌드 후 산출물만 배포 저장소는 빌드 서버에만. .git 이 서버에 없음
rsync 로 파일만 전송 --exclude=.git 으로 제외
웹 루트를 하위로 저장소는 상위, 공개는 public/ 하위만

저는 rsync 로 파일만 보내는 방식을 택했습니다. 서버에 git을 둘 이유가 없고, 버전 관리는 작업 PC에서 하면 됩니다.

rsync -az --delete \
  --exclude='.git/' --exclude='wp-config.php' --exclude='uploads/' \
  ./소스/ 서버:~/경로/

⚠️ --delete 를 쓸 때는 제외 목록을 반드시 검증하세요.
빠뜨리면 서버의 업로드 파일이나 설정이 지워집니다. --dry-run 으로 먼저 확인합니다.

정리

  • .git 이 노출되면 소스 전체와 삭제한 파일까지 복원됩니다
  • 커밋했다가 지운 비밀번호도 남아 있습니다
  • curl /.git/config 로 확인하세요. 404여야 정상입니다
  • 막을 때는 403보다 404로
  • 설정 후 반드시 재측정하세요. 프록시 재시작이 필요할 수 있습니다
  • 근본적으로는 웹 루트에 저장소를 두지 않는 구조가 낫습니다