대상: 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됨
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모든 배포에서 이를 확인하면 배치 실수를 일찍 잡습니다.
다음으로 읽기
- 사고: .env가 온 세상에 노출됨
- 용어집: .env란 무엇인가
- 기초: .env와 API 키의 무엇이 위험한가
FAQ
Q지금 당장 할 수 있는 가장 빠른 것은 무엇인가요?
웹 루트의 .htaccess에 .env, .git, SQL 덤프, credentials.json 등에 대한 거부 블록을 추가하세요. 되돌릴 수 있고 일반 페이지에 영향을 주지 않습니다 — 다만 응급처치이며, 진짜 해법은 재구성입니다.
Q.htaccess만으로는 왜 부족한가요?
블랙리스트는 두더지 잡기입니다 — 새 파일명이 새 구멍을 더합니다. 앱 본체를 웹 루트 밖으로 옮기면 .env가 애초에 닿을 수 없으며, 유지할 규칙도 없습니다.
Q그냥 public/을 심링크로 노출하면 안 되나요?
일부 환경에서는 등록된 서브도메인의 docroot를 심링크로 다른 트리에 다시 가리키면 따라가지 않고 500을 반환합니다. 본 사이트의 경험상, docroot를 실제 디렉터리로 두고 작은 index.php에서 절대 경로로 require하는('bootstrap-redirect' 형태)가 더 안정적입니다.