Files
Gnuboard7/docs/backend/reverse-proxy.md
T
HeuJung b6c5e1f323 feat(core): 리버스 프록시 신뢰 설정 지원 및 미설정 진단
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 제보)
2026-08-28 18:09:27 +09:00

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. 증상

신뢰할 프록시가 하나도 지정되지 않으면 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:// 로 시작하면 신뢰 프록시가 적용되지 않은 상태다.


관련 문서