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

210 lines
10 KiB
Markdown

# 리버스 프록시 환경 (Reverse Proxy)
TLS 를 앞단에서 종단하고 앱에는 HTTP 로 전달하는 구성(AWS ALB, CloudFront, Cloudflare,
nginx/Apache 리버스 프록시, ngrok)에서 G7 이 접속 주소와 방문자 IP 를 올바르게 인식하도록
신뢰할 프록시를 지정하는 방법을 설명한다.
## TL;DR (5초 요약)
```text
1. 프록시 뒤에서는 요청이 스스로 스킴·IP 를 증명하지 못한다 — 신뢰할 프록시를 지정해야 한다
2. .env 에 TRUSTED_PROXIES 한 줄. 미설정이 기본값이고 기존 설치처 동작은 바뀌지 않는다
3. * 은 앱이 프록시 없이는 도달 불가할 때만 안전하다 (XFF 위조). ** 은 * 과 동일 동작
4. 미설정 시 증상: 화면 백지(Mixed Content) · webhook 403 · 방문자 IP 전원 동일 · 로그인 제한 붕괴
5. 도입 직후 1회성 후속 조치가 있다 — 기발송 서명 URL 무효화 / SEO 캐시·사이트맵 재생성
```
## 목차
- [1. 증상](#1-증상)
- [2. 설정](#2-설정)
- [3. `*` 과 `**` 의 전제](#3--과--의-전제)
- [4. 프록시측에서 함께 해야 할 것](#4-프록시측에서-함께-해야-할-것)
- [5. 세션 쿠키의 Secure 속성](#5-세션-쿠키의-secure-속성)
- [6. 도입 시 1회성 후속 조치](#6-도입-시-1회성-후속-조치)
- [7. 하면 안 되는 것](#7-하면-안-되는-것)
- [8. 확인 방법](#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` 를 지정한다. 코어 코드 수정은 필요 없다.
```dotenv
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 로만 서비스하는 사이트라면 스킴 판정과 무관하게 명시하는 편이 안전하다.
```dotenv
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()` 로 값을 읽지 않는다.
```php
// ❌ 조용히 아무 일도 하지 않는다
->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`
로만 한다
### 명령줄
관리자 화면 자체가 뜨지 않는 상태에서 쓰는 통로다.
```bash
php artisan trusted-proxy:status
```
콘솔에는 요청이 없으므로 설정값만 판정하고, 요청 기반 실측 항목은 `판정 불가` 로 구분해
표시한다.
### 직접 확인
프록시를 거친 요청의 HTML 에서 코어 엔진 스크립트의 스킴을 본다.
```bash
curl -s -H 'X-Forwarded-Proto: https' http://127.0.0.1/ | grep 'template-engine'
```
`https://` 로 시작하면 정상이고, `http://` 로 시작하면 신뢰 프록시가 적용되지 않은 상태다.
---
## 관련 문서
- [routing.md](routing.md) — 자산 URL 생성 규칙
- [../requirements.md](../requirements.md) — SSL/TLS 요구사항
- [../../INSTALL.md](../../INSTALL.md) — 설치·프로덕션 설정