보안 가이드

공개 디렉터리에 시크릿 파일을 남겨두지 않았나요? 웹루트를 점검하라

웹 공개 디렉터리(웹루트)에 남긴 토큰, .env, 백업 파일은 누구나 URL로 가져갈 수 있습니다. 공유 템플릿이 같은 파일을 모든 사이트에 복사하는 문제와 서버 점검 방법을 정리합니다.

게시 2026-06-11 업데이트 2026-10-06 12분 읽기
이 페이지에서

대상: 공유/임대 호스팅이나 VPS에서 여러 사이트를 운영하며 공개 디렉터리(public/, public_html/, htdocs/ 등)로 배포하는 모든 사람. 공격 절차는 없습니다 — 오직 자기 서버를 점검해 문제를 막는 것뿐입니다. .env 자체를 보호하는 법은 공유 호스팅에서 .env 보호하기를 보세요.

본 사이트의 견해: 어디에 두느냐가 곧 접근 제어다

시크릿은 교묘한 공격보다 배치 사고로 더 많이 샙니다. 웹루트는 "누구나 URL로 와서 가져갈 수 있는 곳"입니다. 거기에 시크릿을 두면 아무리 강한 서버 비밀번호도 무의미합니다. 본 사이트의 기본 원칙은 시크릿을 웹루트와 git 양쪽에서 빼두는 것입니다(→ 시크릿을 안전하게 보관하기). 발상은 단순합니다: 공개 디렉터리에는 누가 봐도 괜찮은 것만 두세요.

public/ = 누구나 URL로 가져갈 수 있음

두어도 OK

이미지, CSS, JS — 공개 에셋

절대 두지 말 것

.env, token.json, .sql, .git, .bak

앱 루트 위 = 서빙 안 됨

여기에 둘 것

시크릿, 토큰, 자격증명, 백업. 권한 600(소유자 읽기만). URL로 닿을 수 없음.

공개 디렉터리의 파일은 누구나 URL로 가져갈 수 있다. 시크릿은 그 밖에 두세요(서빙 안 됨, 권한 600).

"공개 디렉터리의 잔여물"이 생기는 이유

흔한 패턴은 편의상 둔 파일, 혹은 보일러플레이트 안에 딸려 온 파일이 조용히 공개된 채 남는 것입니다.

URL 하나
웹루트 파일은 누구나 가져감
수년
눈치채지 못한 채 방치됨
모든 사이트
템플릿의 잔여물이 모든 사이트로 퍼짐
즉시 폐기
샜다고 가정하고 갱신

예를 들어 제3자 연동용 액세스 토큰을 담은 JSON이 public/ 아래에 오래 놓여 있고 — 공유 템플릿에서 왔다면 같은 파일이 많은 사이트의 같은 자리에 복사되어 모두 같은 문제를 갖습니다. 한 사이트만 고쳐도 다른 사이트에 파일이 남아 있으면 무의미합니다. 똑같이 위험한 것: 남겨진 .env, 데이터베이스 백업(.sql), .git 디렉터리, 에디터 백업(*.bak), 단축 파일(관련: 경로 순회 · .env 전체가 노출됨).

남겨지기 쉬운 파일과 각각이 위험한 이유

"시크릿 파일" 하면 보통 .env를 떠올리지만, 실제로 남겨지는 것은 훨씬 다양합니다. 웹 보안을 다루는 비영리 단체 OWASP의 테스트 가이드도 파일의 이전 버전, 백업, 어디서도 링크되지 않은 파일을 점검 대상으로 꼽습니다.

종류대표적인 이름위험한 것있어야 할 곳
설정 파일.env, config.php.bak, settings.json평문으로 적힌 데이터베이스 비밀번호와 API 키앱 루트 위(서빙 안 됨)
토큰과 자격증명token.json, credentials.json, *.pem, *.key외부 서비스나 다른 서버에 바로 로그인웹루트 밖, 권한 600
수동 또는 에디터 백업login.php.old, index.php~, .index.php.swp, *.orig확장자가 바뀌어 코드로 실행되지 않고, 소스가 평문으로 보일 수 있음운영 환경에서 만들지 않는다. 로컬에서 편집
데이터베이스 덤프backup.sql, dump.sql.gz사용자의 개인정보와 비밀번호 해시가 파일 하나에 전부전용 백업 장소, 가능하면 서버 밖
사이트 압축 파일site.zip, www.tar.gz.env와 전체 소스 코드를 포함압축을 푼 직후 삭제
버전 관리 데이터.git/과거 커밋의 시크릿을 복원할 수 있음공개 디렉터리 밖
빌드 시 삽입된 값assets/index-xxxx.js 내부VITE_나 NEXT_PUBLIC_으로 시작하는 변수가 브라우저용 JavaScript에 들어감시크릿은 서버 측 변수에만 둔다

마지막 행은 놓치기 쉽습니다. 파일을 직접 두지 않았더라도 빌드가 시크릿을 공개 디렉터리로 밀어 넣을 수 있습니다. Vite 공식 문서는 VITE_ 변수에 API 키 같은 민감한 정보를 넣으면 안 된다고 분명히 적고 있습니다.

자기 서버를 점검하라

먼저 "내 공개 디렉터리에 시크릿 같은 파일이 있는가"를 기계적으로 드러내세요. 이는 자기 자산을 점검하는 것이지 남의 사이트를 엿보는 것이 아닙니다.

1

공개 디렉터리 아래에서 시크릿 같은 파일을 검색한다

자기 홈 전체에서, public/에 흘러든 시크릿 후보(토큰/자격증명 JSON, .env, 키, 단축 파일)를 드러내세요.
2

다른 곳에도 같은 파일이 있다고 가정하고 — 모든 호스트와 사이트를 확인한다

같은 보일러플레이트로 만든 사이트는 같은 것을 같은 자리에 갖고 있습니다; 모든 호스트와 사이트에 같은 점검을 돌리세요. 하나가 발견되면 보통 더 있습니다.
3

파일마다 정말 공개가 필요한지 판단한다

결과 하나하나를 "공개해도 안전" vs "시크릿"으로 분류하세요. 시크릿은 아래의 제거/폐기/이동 단계를 거칩니다.
# audit under your own public directories (inspecting your own assets)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
      -o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# an exposed .git directory is dangerous too
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/null

내 환경을 점검하는 법

위의 find는 출발점입니다. 아래 다섯 가지 점검을 차례로 하면 파일 이름 검색만으로는 놓치는 잔여물까지 잡을 수 있습니다. 반드시 자기 서버와 자기 도메인에만 실행하세요. .env에 특화된 점검(응답의 첫 바이트 확인, .htaccess가 실제로 작동하는지 확인)은 공유 호스팅에서 .env 보호하기에서 다룹니다.

1

백업과 압축 파일도 검색한다

공개 디렉터리 이름은 호스팅마다 다르므로 public_html, htdocs, www도 포함하세요. 예를 들어 find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null을 실행하고, 결과를 하나씩 정말 공개해도 되는 것인지 확인하세요.

2

디렉터리 목록 표시가 꺼져 있는지 확인한다

index.html이 없는 디렉터리(예: /uploads/)를 브라우저로 열어 보세요. 파일 이름 목록이 보이면 웹 서버의 디렉터리 목록 기능이 켜져 있는 것입니다. 목록이 있으면 이름을 몰라도 잔여물을 찾아낼 수 있습니다. 403이나 404가 나와야 정상입니다.

3

빌드된 JavaScript에서 시크릿을 검색한다

프런트엔드를 빌드한다면, 배포하는 결과물 폴더(dist/, .next/static/ 등)를 검색하세요. grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/ 같은 명령에, 쓰는 서비스의 키 접두어도 더해 찾습니다. 발견되면 그 변수가 브라우저용 이름(VITE_, NEXT_PUBLIC_)인지 확인하세요.

4

웹루트 밖의 시크릿을 다른 사용자가 읽을 수 없는지 확인한다

시크릿을 웹루트 밖으로 옮겨도, 권한이 느슨해 같은 서버의 다른 계정이 읽을 수 있다면 소용없습니다. find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null은 소유자 외의 누구나 읽을 수 있는 시크릿을 나열합니다. chmod 600으로 고치세요. 아무것도 나오지 않으면 괜찮습니다.

5

검색 엔진에 색인된 것이 있는지 확인한다

Google에서 site:yourdomain(yourdomain은 내 도메인)으로 검색해 모르는 파일(.sql, .zip, .txt 등)이 없는지 보세요. Search Console을 쓴다면 페이지 보고서에 색인된 URL이 나열됩니다. 검색 결과에 아직 남아 있을 때의 대처는 아래 흔한 실수에서 다룹니다.

찾았을 때 할 일

세 가지를 한 묶음으로 하세요 — 제거, 폐기, 이동. 하나라도 빠뜨리면 대응이 완전하지 않습니다.

1

URL로 가져갈 수 없도록 웹루트에서 제거한다

파일을 공개 디렉터리 밖으로 옮기거나 삭제하고, URL이 이제 404를 반환하는지 확인하세요.
2

샜을 수 있는 키나 토큰을 폐기/재발급한다

"아마 괜찮겠지"로 두지 마세요. 노출되었으므로 샜다고 가정하고 그 토큰/키를 갱신하거나 폐기하세요.
3

시크릿은 웹루트 밖에 권한 600으로 둔다

앞으로는 시크릿을 공개 디렉터리 밖(예: 앱 루트 위)에 권한 600(소유자 읽기만)으로 두세요. 설정으로 민감한 확장자의 서빙을 거부할 수 있다면 그것도 더하세요.

서버별: 이런 파일의 서빙을 거부하는 설정(보조 수단으로)

진짜 해결책은 파일을 거기 두지 않는 것입니다. 그 위에, 실수로 둔 것도 서빙되지 않도록 웹 서버에 거부 규칙을 더하세요. 쓸 수 있는 방법은 환경에 따라 다릅니다.

환경거부 방법디렉터리 목록주의할 점
Apache(VPS)<Directory> 블록이나 .htaccess에서 <FilesMatch> + Require all deniedOptions -Indexes로 끈다Apache 2.4에서 AllowOverride의 기본값은 None이므로, 허용하지 않으면 .htaccess는 무시된다
Nginx(VPS)location 안에 deny all;autoindex는 기본값이 off.htaccess를 읽지 않는다. 정규식 location 블록은 적힌 순서대로 확인되고 처음 일치한 것이 적용된다
공유 호스팅보통 .htaccess(Apache 호환)관리 화면이나 .htaccess로시크릿이 없는 더미 파일로 작동을 확인한다
정적 호스팅 / CDN결과물 폴더에 있는 것이 그대로 공개된다서비스에 따라 다름거부 규칙이 아니라 결과물 폴더에 시크릿을 넣지 않는 것에 의존한다

Apache 예시(.htaccess가 활성화된 경우):

Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
    Require all denied
</FilesMatch>

Nginx 예시(server 블록 안):

# Allow the path used for automatic certificate renewal (Let's Encrypt) first
location ^~ /.well-known/acme-challenge/ { }
 
# Deny files and directories starting with a dot (.env, .git, ...)
location ~ /\. { deny all; }
 
# Deny backups and archives
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }

Nginx에서는 요청이 ^~가 붙은 location과 일치하면 정규식 location은 확인하지 않습니다. 그래서 점으로 시작하는 경로를 거부하는 규칙이 있어도 인증서 갱신에 쓰이는 /.well-known/acme-challenge/는 계속 서빙됩니다. 설정을 바꾼 뒤에는 nginx -t로 문법을 확인하고, 다시 불러온 다음, 더미 파일이 403을 반환하는지 확인하세요. 사이트에서 ZIP 파일 등을 다운로드로 제공한다면 그 확장자는 거부 목록에서 빼세요.

사람들이 빠지는 배치(위험)

  • 연동용 JSON 토큰이 public/에 놓임
  • 보일러플레이트에 딸려 온 시크릿 파일이 공개된 채 남음
  • 백업(.sql/.bak)이나 .git이 공개 디렉터리 아래에 남음
  • 한 사이트만 고치고 "처리 완료"라고 함

올바른 배치

  • 공개 디렉터리엔 공개해도 되는 것만
  • 시크릿은 웹루트 밖에 권한 600으로
  • 민감한 확장자는 서빙이 거부됨
  • 하나 찾으면 모든 호스트와 사이트를 점검

'공개해도 안전한 것만'을 상시 규칙으로

규칙이 일회성 수정을 이깁니다. 공개 디렉터리에는 공개해도 되는 파일만 둔다고 정하고, 규칙상 시크릿·백업·버전 관리 데이터(.git)·설정 파일을 절대 거기 두지 마세요. 보일러플레이트를 갱신할 때는 템플릿 자체에서 시크릿을 제거하세요 — 그렇지 않으면 앞으로 대량 생산하는 모든 사이트가 같은 시크릿 파일을 계속 복사합니다.

흔한 실수와 고치는 법

  • 거부 규칙에 의존하고 파일은 그대로 둔다: 서버 이전이나 설정 재작성으로 규칙이 빠지면 그날부터 파일이 공개됩니다. Nginx는 .htaccess를 전혀 읽지 않습니다. 규칙은 보조 수단으로 보고, 파일을 공개 디렉터리 밖으로 옮기세요.
  • 운영 서버에서 파일을 직접 편집한다: 에디터가 ~나 .swp로 끝나는 백업을 남깁니다. OWASP 테스트 가이드도 웹 서버에서 파일을 직접 편집하지 말라고 권합니다. 로컬에서 편집하고 배포를 통해 반영하세요.
  • 프로젝트 폴더 전체를 공개 디렉터리에 복사한다: .env, .git, 테스트 데이터가 함께 나갑니다. 빌드 결과물 폴더만 서빙하고, 복사할 대상을 정확히 지정하세요.
  • 점으로 시작하는 경로를 모두 거부해 인증서 갱신이 깨진다: Let's Encrypt는 확인용 파일을 /.well-known/acme-challenge/ 아래에 둡니다. 위의 Nginx 예시처럼 그 경로를 먼저 허용하세요.
  • VITE_나 NEXT_PUBLIC_ 변수에 시크릿을 넣는다: 빌드된 JavaScript에 삽입되어 누구나 읽을 수 있습니다. 서버 측 코드에서만 쓰고, 이미 이렇게 배포한 키는 폐기하세요.
  • 파일을 삭제하고 거기서 멈춘다: 검색 엔진이 색인했다면 결과에 남아 있을 수 있습니다. URL이 404나 410을 반환하는지 확인한 뒤, 재크롤링으로 사라질 때까지 Google Search Console의 삭제 도구로 일시적으로(약 6개월) 숨기세요.

실제 사례: 공개 보고서에 숨어 있던 개인정보

워싱턴 D.C.에서 Medicaid(저소득층을 위한 공적 의료보험)를 운영하는 보건의료재정국(DHCF)은, 2023년부터 2026년 7월까지 웹사이트에 게시한 통계 보고서 두 건에 원본 개인정보가 "숨겨진 필드"로 들어 있었다고 밝혔습니다. 보고서는 집계 수치만 보이도록 만들어졌지만, 파일에는 수급자의 Medicaid 번호, 생년월일 등이 남아 있었습니다. 미국 보건복지부(HHS)에 제출한 신고에서 피해자 수는 399,086명이었습니다. DHCF는 2026년 7월 21일 이 문제를 알게 되어 보고서를 내렸습니다.

잊힌 시크릿 파일 사례는 아니지만, 뿌리는 같습니다.

  • 공개 디렉터리의 파일은 화면에 보이는 부분만이 아니라 통째로 가져갈 수 있다
  • 게시자가 "보이지 않는다"고 생각하는 정보가 검토에서 가장 놓치기 쉽다
  • 누군가 알아채기까지 몇 년 동안 게시된 채로 남을 수 있다

게시 전에 "이 파일 안에 실제로 무엇이 들어 있는가"를 확인하는 단계가 잔여 파일과 숨은 데이터를 모두 막습니다. 전체 경위는 워싱턴 D.C. Medicaid 데이터 노출(영문)에 있습니다.

본 사이트는 직접 어떻게 하는가

본 사이트의 기본 원칙은 시크릿 — 키, 토큰, 접속 문자열 — 을 공개 디렉터리와 코드 저장소 양쪽에서 빼두는 것입니다. 배포 산출물에는 빌드된 공개 에셋만 들어가고, 시크릿은 서빙되지 않는 곳에 런타임 환경 변수로 보관합니다. 이유는: 이 글의 사고 부류가, 남겨진 파일 하나가 그 URL을 요청한 누구에게나 전부를 유출하는 전형적인 사례이기 때문입니다. 우리는 배치 자체를 접근 제어로 설계하고, 이 점검을 취약점 대응 인벤토리와 같은 주기로 돌립니다.

다음으로 읽기

출처

  • IPA, 「안전한 웹사이트 운영 관리를 위한 20가지 점검 포인트」(일본어; 공개하면 안 되는 파일, 검색 엔진 캐시): ipa.go.jp
  • OWASP, "Web Security Testing Guide: Review Old Backup and Unreferenced Files for Sensitive Information"(.old, ~, .swp 백업; 서버에서 직접 편집하지 말 것): wstg.owasp.org
  • Apache HTTP Server 2.4, "Upgrading to 2.4 from 2.2"(AllowOverride의 기본값은 None) 및 "mod_autoindex"(Options +Indexes가 목록을 생성): httpd.apache.org, httpd.apache.org
  • nginx, "ngx_http_core_module: location"(일치 순서와 ^~) 및 "ngx_http_autoindex_module"(autoindex의 기본값은 off): nginx.org, nginx.org
  • Let's Encrypt, "Challenge Types"(HTTP-01은 /.well-known/acme-challenge/를 사용): letsencrypt.org
  • Vite, "Env Variables and Modes"(VITE_ 변수는 클라이언트 코드에 노출됨): vite.dev
  • Next.js, "Environment Variables"(NEXT_PUBLIC_ 변수는 브라우저용 JavaScript에 삽입됨): nextjs.org
  • Google Search Console 고객센터, "Removals tool"(일시 삭제는 약 6개월 유지): support.google.com
  • District of Columbia DHCF, "Notice of Data Incident": dhcf.dc.gov; HIPAA Journal(HHS에 보고된 인원): hipaajournal.com

FAQ

Q공개 디렉터리에 파일을 두는 게 왜 위험한가요?
A

웹 공개 디렉터리(웹루트 — public/, public_html/ 등)의 모든 것은 그 URL을 친 누구든 가져갈 수 있습니다. 남겨진 토큰/자격증명 JSON, .env, 백업, 키 파일은 아무도 눈치채기 전에 회수되어 즉각적 피해로 이어집니다. 공개 디렉터리에는 공개되어도 안전한 것만 두어야 합니다.

Q한 사이트에서 하나 찾으면 다른 사이트도 확인해야 하나요?
A

그렇습니다. 공유 템플릿이나 보일러플레이트로 사이트를 대량 생산한다면, 같은 잔여물이 보통 전부에 복사되어 있습니다. 하나 찾으면 같은 출처의 모든 사이트와 호스트를 점검하세요.

Q찾으면 무엇을 해야 하나요?
A

1) 웹루트에서 파일을 제거해 URL로 404가 나오게 합니다. 2) 샜을 수 있는 토큰이나 키를 갱신/폐기합니다(샜다고 가정). 3) 앞으로는 시크릿을 웹루트 밖에 권한 600(소유자 읽기만)으로 둡니다. 셋을 한 묶음으로 하세요.

Q공개 웹 디렉터리에 절대 두면 안 되는 파일은 무엇인가요?
A

대표적인 것은 .env 같은 설정 파일, 토큰이나 자격증명 JSON 파일, 데이터베이스 덤프(.sql), 사이트 전체를 묶은 ZIP이나 tar.gz 압축 파일, .git 디렉터리, 그리고 손으로 또는 에디터가 만든 백업(.bak, .old, .orig, .swp, 이름 끝이 ~인 파일)입니다. OWASP 테스트 가이드는 login.php를 login.php.old로 남겨 두면 서버가 그것을 코드로 실행하지 않고 소스 코드를 평문으로 반환할 수 있다고 설명합니다.

Q파일을 삭제했는데 아직 검색 결과에 나옵니다. 어떻게 하나요?
A

먼저 그 URL이 404나 410을 반환하는지 확인한 뒤, Google Search Console의 삭제 도구로 결과에서 일시적으로 숨기세요. Google 도움말에 따르면 일시 삭제는 약 6개월 유지되며, 파일 삭제 같은 영구적인 조치와 함께 쓰도록 되어 있습니다. 일본 IPA도 검색 엔진에 캐시 삭제를 요청하라고 권합니다. 검색 결과에서 사라졌는지와 상관없이, 파일에 들어 있던 키나 토큰은 폐기하세요.

Q프런트엔드 환경 변수(VITE_나 NEXT_PUBLIC_)에 시크릿을 넣어도 되나요?
A

안 됩니다. Vite는 VITE_로 시작하는 변수를, Next.js는 NEXT_PUBLIC_으로 시작하는 변수를 빌드 시 브라우저로 보내는 JavaScript에 삽입합니다. 공개 디렉터리에 파일을 두는 것과 같아서 누구나 읽을 수 있습니다. API 키 같은 시크릿은 서버 측 코드만 쓰는 변수에 두세요.