보안 가이드

공유 호스팅에서 .env를 공개 웹에 노출하지 않기

Laravel류 앱을 공유 호스팅에 배포할 때 .env, .git, vendor가 웹에 노출되지 않게 하는 법. 앱 본체를 웹 루트 밖에 두고 public/만 노출하는 배치와 .htaccess 응급처치, 자가 점검.

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

대상: Apache/.htaccess 공유 호스팅에서 Laravel류 앱(public/만 노출되어야 하는)을 돌리는 모든 사람. 실제 사고에서 추출했습니다(→ .env가 세상에 노출됨) — 당신 자신의 배포를 지키기 위해.

본 사이트의 견해: 이건 '한 사람의 실수'가 아니다

초기 공유 호스팅 문화는 모든 것을 public_html 바로 아래에 두었습니다. (오직 public/만 노출되길 기대하는) Laravel이 그 흐름에 들어가면서, 위험한 레이아웃이 사실상의 표준이 되었습니다 — 벤더 자신의 문서조차 프로젝트를 public_html 안에 둡니다. 그러니 해법은 경계심이 아니라 프로세스입니다: 배포 기본값을 바꾸세요.

배치를 바로잡기(도식)

✗ 위험(본체가 공개 루트에)

public_html/
├─ .env   ← 읽힘!
├─ .git/  vendor/
└─ public/

✓ 안전(본체는 밖, public만)

app-laravel/   ← 웹 루트 밖
├─ .env  .git/  ← 닿을 수 없음
└─ public/ → public_html에서 require됨
본체를 docroot 밖에, public/만 노출. 그것만으로 .env가 닿을 수 없게 됩니다.

환경별로 맞는 해법

".env를 웹에 노출하지 않기"의 구체적인 방법은 서버 종류와 권한에 따라 달라집니다. 먼저 자신에게 해당하는 행을 찾은 뒤 아래 단계를 진행하세요.

환경영구 해법응급처치주의할 점
공유 호스팅(SSH 있음)앱 본체를 공개 폴더 밖으로 옮기고 작은 index.php만 남긴다(2단계).htaccess로 거부공개 폴더 자체를 심링크로 만들면 일부 환경에서 500이 반환된다
공유 호스팅(FTP만)FTP 클라이언트로 앱 본체를 공개 폴더(public_html 등)보다 한 단계 위에 다시 업로드한다.htaccess로 거부대부분의 공유 호스팅은 공개 폴더 위에 쓸 수 있다. 안 된다면 지원팀에 문의한다
관리 패널에서 공개 폴더를 지정할 수 있음공개 폴더를 app-laravel/public로 직접 지정한다불필요(확인은 한다)전환 후 예전 공개 폴더에 남은 앱 사본을 삭제한다
VPS(Apache)VirtualHost의 DocumentRoot를 public/로 지정한다서버 설정에 거부 규칙AllowOverride None(Apache 2.4 기본값)이면 .htaccess는 읽히지 않는다
VPS(Nginx)root를 public/로 지정하고 점으로 시작하는 파일을 거부한다서버 설정에 거부 규칙Nginx는 .htaccess를 읽지 않는다

목표는 어디서나 같습니다. 공개 폴더 안에 .env, .git, vendor/, storage/가 없어야 합니다. Laravel 공식 문서도 index.php를 프로젝트 루트로 옮겨 그곳에서 서비스하면 민감한 설정 파일이 대거 인터넷에 공개된다고 경고합니다.

1단계: 응급처치(빠르고, 되돌릴 수 있음)

웹 루트의 .htaccess 맨 위에 거부 블록을 추가하세요(방어적 설정 예시). 일반 페이지는 영향받지 않습니다.

# === SECURITY BLOCK ===
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
<FilesMatch "(?i)^(phpinfo|info)\.php$">
    Require all denied
</FilesMatch>
 
# if Laravel internals are visible at /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

응급처치가 목표는 아니다

블랙리스트는 반드시 무언가를 놓칩니다 — composer.lock이나, 새 파일명이 그렇습니다. 2단계까지 시간을 버는 수단으로만 여기세요.

2단계: 재구성(영구 해법)

앱 본체를 웹 루트 밖으로 옮기고, 그저 그것을 로드하는 작은 진입점만 남기세요.

# 1. Move the body outside the web root (same filesystem → mv is instant, atomic)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. Put only a minimal entry in the web root
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
 
# 3. Copy Laravel public/.htaccess, symlink static assets
# 4. Clear config cache
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.php

본 사이트 운영에서 얻은 교훈: 심링크보다 bootstrap-redirect

public_html 자체를 public/로 향하는 심링크로 만들면, 일부 환경에서 등록된 서브도메인의 docroot에서 500을 반환할 수 있습니다(따라가지 않음). docroot를 실제 디렉터리로 두고 작은 index.php에서 절대 경로로 require하는('bootstrap-redirect' 형태)가 범위 제약이 있는 공유 호스팅에서 더 안정적입니다. 또한 opcache가 실패한 로드를 기억해 계속 500을 반환할 수 있으니 — 최후의 수단은 진입 파일명을 바꾸는 것(entry.php)입니다.

3단계: 키 교체(이미 봤다고 가정)

노출 창이 조금이라도 있었다면, .env 키를 이미 봤다고 보고 교체하세요. 순서: 외부 API / OAuth 시크릿 → 암호화 키 → 메일 → DB. .env 용어집 참고. OAuth 시크릿 재발급은 refresh_token을 무효화할 수 있으니 — 재인증 흐름을 먼저 준비하세요.

4단계: 자가 점검(습관으로)

마지막으로, 자기 사이트에서 실제로 아무것도 노출되지 않았는지 확인하세요(소유한 도메인에 대해서만).

# A 200 with a body means it's exposed. 403/404 is okay for now.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1

모든 배포에서 이를 확인하면 배치 실수를 일찍 잡습니다.

직접 확인하는 방법(더 자세히)

4단계의 두 줄 외에도, 아래 확인으로 흔히 놓치는 부분을 더 잡을 수 있습니다. 반드시 자신의 서버와 자신의 도메인에 대해서만 실행하세요.

1

상태 코드가 아니라 내용을 본다

모르는 URL에 홈 화면과 200을 돌려주는 앱도 있어서, 상태 코드만으로는 판단할 수 없습니다. curl -s https://your-domain/.env | head -c 120처럼 앞부분만 출력해 APP_KEY=나 DB_PASSWORD= 같은 설정 줄이 나오는지 보세요. .git/HEAD는 ref: refs/heads/로 시작하는 줄이 나오면 노출된 상태입니다.

2

앱 하나가 아니라 전부 확인한다

메인 도메인만이 아니라 서브도메인이나 하위 디렉터리의 앱마다 같은 확인을 반복하세요. 배치 실수는 앱을 추가할 때 그대로 복사되기 쉬워서, 하나를 고쳤다고 나머지도 고쳐진 것은 아닙니다.

3

서버의 공개 폴더를 검색한다

SSH가 있다면 find ~/public_html -maxdepth 4 \( -name ".env*" -o -name ".git" -o -name "*.sql" -o -name "*.zip" \)처럼 위험한 파일을 직접 찾으세요. 아무것도 출력되지 않는 것이 원하는 결과입니다. FTP만 쓸 수 있다면 FTP 클라이언트에서 "숨김 파일 표시"를 켜고 목록을 살펴보세요.

4

.htaccess가 실제로 적용되는지 확인한다

비밀 정보가 없는 무해한 더미 파일(예: test만 적힌 check.bak)을 공개 폴더에 두고 URL로 열어 보세요. 403이 나와야 합니다. 열린다면 .htaccess가 읽히지 않는 것이므로 서버 종류(Nginx 등)나 설정을 확인하세요. 확인이 끝나면 더미 파일은 삭제하세요.

5

디버그 모드가 꺼져 있는지 확인한다

.env에서 APP_ENV는 production, APP_DEBUG는 false여야 합니다. Laravel 문서는 운영 환경에서 APP_DEBUG=true이면 민감한 설정 값이 사용자에게 노출될 위험이 있다고 설명합니다. SSH가 있다면 php artisan about --only=environment로도 같은 내용을 볼 수 있습니다.

흔한 실수와 고치는 법

  • .env는 지웠지만 .git은 남겨 둠: 노출된 .git에서 예전 .env나 키를 포함한 과거 커밋이 복원될 수 있습니다. .git도 공개 폴더 밖으로 옮기고, 한 번이라도 커밋된 시크릿은 재발급하세요.
  • .env.bak, .env.old 같은 사본을 공개 폴더에 남겨 둠: 실제 값이 든 사본은 .env만큼 민감합니다. 밖으로 옮기거나 삭제하세요. .env.example에는 자리표시 값만 두세요.
  • 프로젝트 전체를 ZIP으로 올리고 압축을 푼 뒤 ZIP을 남겨 둠: ZIP 안에 .env가 들어 있습니다. 압축을 풀면 바로 삭제하세요.
  • 운영 환경을 APP_DEBUG=true로 돌림: 오류 페이지에 설정 값이 드러날 수 있습니다. 운영에서는 반드시 false로 두세요.
  • .htaccess를 썼으니 안전하다고 생각함: Nginx나 AllowOverride None 환경에서는 아예 읽히지 않습니다. 위의 더미 파일 확인으로 검증하세요.
  • APP_KEY를 갑자기 바꿈: Laravel 문서에 따르면 키를 바꾸면 로그인한 모든 사용자가 로그아웃되고, 예전 키로 암호화된 데이터는 복호화할 수 없게 됩니다. Laravel 11 이상에서는 전환 전에 예전 키를 APP_PREVIOUS_KEYS에 넣으세요.

실제 사례: 노출된 .env를 대규모로 수집한 공격

2024년 8월, 한 보안 기업의 연구팀은 웹 서버에 노출된 .env 파일을 자동으로 수집하고, 그 안의 클라우드 자격 증명으로 데이터를 빼낸 뒤 몸값을 요구한 공격을 보고했습니다. 보고서에 따르면 약 11만 개 도메인이 대상이 되었고 9만 개가 넘는 환경 변수가 수집되었으며, 그중 약 7,000개는 클라우드 서비스 자격 증명, 약 1,500개는 소셜 미디어 계정 자격 증명이었습니다.

보고서는 원인을 클라우드 쪽 취약점이 아니라, 실수로 .env를 공개한 피해 조직의 설정 오류로 보았습니다. 여기서 얻을 교훈은 두 가지입니다.

  • 노출된 .env는 우연히 발견되는 것이 아니라 프로그램이 대규모로 찾아낸다
  • .env 하나로 클라우드 스토리지나 이메일 같은 다른 서비스까지 피해가 번질 수 있다

작은 사이트라서 발견되지 않을 거라고 생각하지 마세요. 같은 배치 실수의 익명화된 사례는 .env가 온 세상에 노출됨에 있습니다.

다음으로 읽기

출처

  • Laravel 문서, "Deployment"(요청은 public/index.php로 향하게 하고, 프로젝트 루트에서 서비스하지 말 것, 운영에서는 APP_DEBUG=false): laravel.com
  • Laravel 문서, "Configuration"(.env를 소스 관리에 커밋하지 말 것, Debug Mode): laravel.com
  • Laravel 문서, "Encryption"(APP_KEY 교체와 APP_PREVIOUS_KEYS): laravel.com
  • Apache HTTP Server 2.4, "core - AllowOverride"(None이면 .htaccess를 읽지 않으며, 기본값은 None): httpd.apache.org
  • Apache HTTP Server 2.4, ".htaccess files"(사용할 때와 주의점): httpd.apache.org
  • OWASP, "WSTG-CONF-04 Review Old Backup and Unreferenced Files for Sensitive Information": owasp.org
  • Unit 42(Palo Alto Networks), "Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments"(2024년 8월 15일): unit42.paloaltonetworks.com

FAQ

Q지금 당장 할 수 있는 가장 빠른 것은 무엇인가요?
A

웹 루트의 .htaccess에 .env, .git, SQL 덤프, credentials.json 등에 대한 거부 블록을 추가하세요. 되돌릴 수 있고 일반 페이지에 영향을 주지 않습니다 — 다만 응급처치이며, 지속적인 해법은 재구성입니다.

Q.htaccess만으로는 왜 부족한가요?
A

블랙리스트는 새 파일명마다 새 규칙이 필요해 반드시 무언가를 놓칩니다. 앱 본체를 웹 루트 밖으로 옮기면 .env가 애초에 닿을 수 없으며, 유지할 규칙도 없습니다.

Q그냥 public/을 심링크로 노출하면 안 되나요?
A

일부 환경에서는 등록된 서브도메인의 docroot를 심링크로 다른 트리에 다시 가리키면 따라가지 않고 500을 반환합니다. 본 사이트의 경험상, docroot를 실제 디렉터리로 두고 작은 index.php에서 절대 경로로 require하는('bootstrap-redirect' 형태)가 더 안정적입니다.

Q내 사이트의 .env가 외부에서 보이는지 어떻게 확인하나요?
A

자신이 소유한 도메인에 대해 curl이나 브라우저로 /.env와 /.git/HEAD를 열고, HTTP 상태와 내용을 모두 확인하세요. 403이나 404라면 일단 괜찮습니다. 200이면서 'APP_KEY=' 같은 설정 줄이 보이면 노출된 상태입니다. 서브도메인이나 하위 디렉터리에 앱을 여러 개 운영한다면 각각 확인하세요. SSH가 있다면 서버의 공개 폴더 안에 .env나 .git이 있는지도 검색하세요.

Q서버가 Nginx이거나 .htaccess가 적용되지 않으면 어떻게 하나요?
A

Nginx는 .htaccess를 읽지 않습니다. Apache에서도 AllowOverride가 None이면 .htaccess는 무시됩니다. 이 경우 서버 설정에서 웹 루트(root 또는 DocumentRoot)를 Laravel의 public/로 지정하세요. Laravel 공식 문서의 Nginx 예시에는 점으로 시작하는 파일(.well-known 제외)을 거부하는 규칙도 들어 있습니다.

Q.env가 노출됐다면 파일을 지우는 것으로 충분한가요?
A

아닙니다. 노출된 동안 누군가 가져갔다면, 파일을 지운 뒤에도 그 안의 키는 계속 작동합니다. API 키, OAuth 시크릿, DB·메일 비밀번호, APP_KEY를 재발급하고, 각 서비스의 활동 기록에서 기억나지 않는 작업이 없는지 확인하세요. .git도 노출됐다면 과거 커밋에 한 번이라도 들어간 시크릿은 모두 유출된 것으로 보세요.