TLS 가 앞단에서 종단되고 앱에는 HTTP 로 전달되는 구성에서 X-Forwarded-* 가 전부 무시되어 화면 백지·IP 왜곡·webhook 403 이 함께 발생했다. 코어에 신뢰 프록시 설정 지점이 아예 없던 것이 원인이다. - config/trustedproxy.php 로 내장 TrustProxies 미들웨어에 값을 공급한다. bootstrap/app.php 는 건드리지 않는다 — withMiddleware 클로저는 .env 로드 전에 평가되어 env 가 항상 null 이 되는 조용한 no-op 이다. - 판정은 App\Support\TrustedProxyDiagnostic 단일 SSoT 에서 계산하고 대시보드 알림·환경설정 고급 탭·설치 마법사·trusted-proxy:status 네 면이 소비한다. 판정식은 "HTTPS 인식 실패" 가 아니라 "X-Forwarded-* 수신 중 AND 신뢰 프록시 미설정" 이다 — HTTP 전용 사이트가 프록시 뒤에 있으면 화면은 정상인 채로 나머지만 조용히 어긋나기 때문이다. - 값 편집 UI 와 쓰기 엔드포인트는 두지 않는다(잠금 역설 + XFF 위조 경로). - 혼합 콘텐츠 차단을 부트스트랩 폴백의 별도 사유로 가른다. 새로고침으로 낫지 않으므로 버튼을 렌더하지 않고, 원인·조치는 콘솔로 운영자에게 보낸다. - 대시보드 알림을 심각도로 배치한다 — warning 은 상단 배너, 그 외는 하단 카드. 같은 알림이 두 곳에 뜨지 않으며, 여러 건은 간격을 두고 쌓인다. 공개 이슈: (@lyg-kaban 제보)
10 KiB
리버스 프록시 환경 (Reverse Proxy)
TLS 를 앞단에서 종단하고 앱에는 HTTP 로 전달하는 구성(AWS ALB, CloudFront, Cloudflare, nginx/Apache 리버스 프록시, ngrok)에서 G7 이 접속 주소와 방문자 IP 를 올바르게 인식하도록 신뢰할 프록시를 지정하는 방법을 설명한다.
TL;DR (5초 요약)
1. 프록시 뒤에서는 요청이 스스로 스킴·IP 를 증명하지 못한다 — 신뢰할 프록시를 지정해야 한다
2. .env 에 TRUSTED_PROXIES 한 줄. 미설정이 기본값이고 기존 설치처 동작은 바뀌지 않는다
3. * 은 앱이 프록시 없이는 도달 불가할 때만 안전하다 (XFF 위조). ** 은 * 과 동일 동작
4. 미설정 시 증상: 화면 백지(Mixed Content) · webhook 403 · 방문자 IP 전원 동일 · 로그인 제한 붕괴
5. 도입 직후 1회성 후속 조치가 있다 — 기발송 서명 URL 무효화 / SEO 캐시·사이트맵 재생성
목차
- 1. 증상
- 2. 설정
- 3.
*과**의 전제 - 4. 프록시측에서 함께 해야 할 것
- 5. 세션 쿠키의 Secure 속성
- 6. 도입 시 1회성 후속 조치
- 7. 하면 안 되는 것
- 8. 확인 방법
1. 증상
신뢰할 프록시가 하나도 지정되지 않으면 Laravel 은 X-Forwarded-* 헤더를 전부 무시한다.
그 결과 요청 객체가 평문 HTTP 요청으로 인식되고, 아래가 동시에 발생한다.
| 증상 | 원인 |
|---|---|
| 사이트가 아예 뜨지 않는다 (백지 + 콘솔에 Mixed Content 차단) | asset()/url() 이 http:// 절대 URL 을 만들어, HTTPS 페이지가 요청하는 순간 브라우저가 차단한다. 코어 엔진이 로드되지 못해 사용자·관리자 화면 양쪽이 백지가 된다 |
| 결제·메시지 webhook 이 전량 403 | 통보 수신 IP 화이트리스트가 프록시 IP 와 대조된다. 입금 통보·발송 리포트를 받지 못해 주문 상태가 영구히 갱신되지 않는다 |
| 본인인증·에스크로 복귀 실패 | 콜백 절대 URL 이 http:// 로 만들어져 부모 창과 origin 이 어긋난다 |
| 로그인 시도 제한 붕괴 | 전 방문자가 한 버킷으로 묶여, 한 사람이 한도를 채우면 나머지 전원의 로그인이 막힌다 |
| 활동 로그·약관 동의·주문자 IP 가 전부 같은 값 | 법적 증빙 가치가 사라지며, 이미 적재된 값은 사후 복구할 수 없다 |
canonical·og:url·sitemap 이 전부 http:// |
검색엔진에 HTTP 주소가 등록되고, 사이트맵은 데이터베이스에 그대로 저장된다 |
세션·영수증·동의 쿠키에 Secure 누락 |
HTTPS 사이트인데 쿠키가 평문 전송 가능한 상태로 발급된다 |
| 배송 국가·타임존이 전 방문자 동일 | IP 기반 판정이 프록시 소재지를 기준으로 삼는다 |
HTTPS 종단 구성에서는 첫 번째 증상 때문에 곧바로 드러난다. 그러나 HTTP 전용 사이트가 프록시 뒤에 있는 구성(Cloudflare flexible SSL, 사내 로드밸런서)에서는 혼합 콘텐츠가 없어 화면이 완전히 정상으로 보이면서 나머지 증상만 조용히 계속된다.
2. 설정
.env 에 TRUSTED_PROXIES 를 지정한다. 코어 코드 수정은 필요 없다.
TRUSTED_PROXIES=*
| 값 | 의미 | 적합한 구성 |
|---|---|---|
| (미설정) | 아무 프록시도 신뢰하지 않는다 — 기본값 | 앱이 인터넷에 직접 노출된 구성 |
* |
직전 호출 IP(REMOTE_ADDR)만 신뢰 |
같은 호스트의 nginx/Apache 프록시, AWS ALB, ngrok |
** |
Laravel 내장 미들웨어에서는 * 과 동일하게 동작한다 |
호환 표기 — 새로 설정할 때 굳이 고를 이유는 없다 |
10.0.0.0/8,192.168.1.5 |
콤마로 구분한 IP·CIDR 목록만 신뢰 | 프록시 주소가 고정되어 있는 구성 — 가장 안전 |
설정값은 config/trustedproxy.php 가 읽으며, Laravel 내장 TrustProxies 미들웨어가 요청
시점에 그 값을 참조한다. 미설정 상태가 기본값이므로, 값을 넣지 않은 기존 설치처의 동작은
바뀌지 않는다.
프록시가 여러 단인 구성
* 과 ** 은 둘 다 직전 호출 IP 하나만 신뢰한다. 그래서 CloudFront → ALB → 앱 처럼
프록시가 여러 단이면 X-Forwarded-For 체인의 마지막 프록시가 방문자로 기록된다. 실측은
다음과 같다 (REMOTE_ADDR = 10.0.0.5, X-Forwarded-For: 203.0.113.77, 10.1.1.1, 10.2.2.2).
| 설정 | HTTPS 인식 | $request->ip() |
|---|---|---|
| 미설정 | false |
10.0.0.5 (프록시) |
* |
true |
10.2.2.2 (마지막 프록시) |
** |
true |
10.2.2.2 (* 과 동일) |
10.0.0.5,10.1.1.1,10.2.2.2 |
true |
203.0.113.77 (실제 방문자) |
즉 다단계 구성에서는 체인의 모든 프록시 IP·CIDR 를 나열해야 최초 클라이언트 IP 가
해석된다. 스킴 인식(화면 표시)만 필요하다면 * 으로 충분하지만, IP 기록·IP 기반 제한·통보
화이트리스트가 정확해야 한다면 목록 방식을 쓴다.
3. * 과 ** 의 전제
* 과 ** 은 앱이 프록시를 거치지 않고는 도달할 수 없는 구성에서만 안전하다.
앱이 직접 노출된 상태에서 * 을 쓰면, 방문자가 요청에 X-Forwarded-For 를 직접 붙이는
것만으로 자기 IP 를 원하는 값으로 바꿀 수 있다. 실측상 직접 접속 클라이언트가
X-Forwarded-For: 8.8.8.8 을 보내면 활동 로그에 8.8.8.8 이 그대로 기록된다. 그러면
IP 기록·IP 기반 차단·통보 화이트리스트가 전부 위조 가능해진다.
따라서 다음 중 하나를 만족해야 한다.
- 앱이 사설망에만 있고 프록시만 공인망에 있다
- 방화벽/보안그룹이 프록시 주소에서 오는 요청만 앱에 도달시킨다
- 위 둘 다 아니라면
*대신 IP·CIDR 목록을 쓴다
** 는 Laravel 내장 미들웨어에서 * 과 같은 코드 경로를 타므로 전제도 같다. 과거 별도
패키지에서 "모든 프록시 신뢰" 를 뜻했던 표기라 그렇게 이해되기 쉬우나, 현재 동작은
직전 호출 IP 만 신뢰하는 것이다.
4. 프록시측에서 함께 해야 할 것
신뢰 설정을 켜면 X-Forwarded-Proto 뿐 아니라 X-Forwarded-For, X-Forwarded-Host,
X-Forwarded-Port, X-Forwarded-Prefix, X-Forwarded-Aws-Elb 도 함께 신뢰된다. 그중 X-Forwarded-Host 는
절대 URL 의 호스트를 바꾸므로, 클라이언트가 보낸 값을 프록시가 그대로 흘려보내면 안 된다.
- 프록시에서 클라이언트가 보낸
X-Forwarded-*를 제거하고 프록시가 직접 설정한다 - 웹서버의
server_name(Apache 는ServerName)을 실제 서비스 호스트로 엄격히 둔다 - 가능하면
*대신 프록시의 IP·CIDR 목록을 지정한다
5. 세션 쿠키의 Secure 속성
세션 쿠키의 Secure 속성은 SESSION_SECURE_COOKIE 가 설정되어 있지 않으면 요청 스킴으로
자동 판정된다. 신뢰 프록시를 지정하면 이 자동 판정이 정상 동작하므로 별도 설정 없이도
Secure 가 붙는다.
HTTPS 로만 서비스하는 사이트라면 스킴 판정과 무관하게 명시하는 편이 안전하다.
SESSION_SECURE_COOKIE=true
6. 도입 시 1회성 후속 조치
TRUSTED_PROXIES 를 켜면 절대 URL 의 스킴·호스트가 바뀐다. 그 결과 아래는 자동으로
낫지 않으므로 도입 직후에 한 번 처리한다.
| 항목 | 조치 |
|---|---|
| 이미 발송된 서명 URL (메일 인증 링크, 첨부 다운로드 링크 등) | 서명은 URL 문자열 전체에 대해 이루어지므로 기존 링크는 전부 만료된 것으로 처리된다. 재발송이 필요하다 |
SEO 캐시에 박제된 http:// canonical |
php artisan seo:clear |
데이터베이스에 저장된 사이트맵의 http:// 주소 |
php artisan seo:generate-sitemap --sync |
| 이미 적재된 프록시 IP (활동 로그, 동의 IP, 주문자 IP 등) | 복구 불가. 원본 방문자 IP 가 남아 있지 않다 |
7. 하면 안 되는 것
bootstrap/app.php 의 withMiddleware 클로저에서 env() 로 값을 읽지 않는다.
// ❌ 조용히 아무 일도 하지 않는다
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: env('TRUSTED_PROXIES'));
})
이 클로저는 .env 가 로드되기 전에 평가되므로 env() 가 항상 null 을 돌려준다. 오류도
경고도 나지 않고, 설정한 것처럼 보이는데 실제로는 아무 프록시도 신뢰하지 않는 상태가 된다.
config/trustedproxy.php 의 'proxies' => env('TRUSTED_PROXIES') 만 사용한다.
$middleware->trustHosts() 도 호출하지 않는다. 호출 자체가 모든 설치처에서 Host 검증을
켜서, 목록에 없는 호스트로 들어온 요청이 400 이 된다.
8. 확인 방법
관리자 화면
- 대시보드 — 프록시 헤더를 받고 있는데 신뢰 프록시가 설정되지 않았으면 경고 알림이 표시된다. HTTP 전용 사이트가 프록시 뒤에 있는 조용한 구성도 이 경고로 드러난다
- 환경설정 > 고급 — 수신 중인 프록시 헤더, HTTPS 인식 여부, 방문자 IP 로 인식된 값과
직전 호출 IP 를 나란히 보여 준다. 두 IP 가 같으면서 프록시 헤더를 받고 있다면 모든
방문자가 한 사람으로 기록되고 있는 상태다. 이 화면은 읽기 전용이며 값 편집은
.env로만 한다
명령줄
관리자 화면 자체가 뜨지 않는 상태에서 쓰는 통로다.
php artisan trusted-proxy:status
콘솔에는 요청이 없으므로 설정값만 판정하고, 요청 기반 실측 항목은 판정 불가 로 구분해
표시한다.
직접 확인
프록시를 거친 요청의 HTML 에서 코어 엔진 스크립트의 스킴을 본다.
curl -s -H 'X-Forwarded-Proto: https' http://127.0.0.1/ | grep 'template-engine'
https:// 로 시작하면 정상이고, http:// 로 시작하면 신뢰 프록시가 적용되지 않은 상태다.
관련 문서
- routing.md — 자산 URL 생성 규칙
- ../requirements.md — SSL/TLS 요구사항
- ../../INSTALL.md — 설치·프로덕션 설정