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 제보)
This commit is contained in:
HeuJung committed 2026-08-28 18:09:27 +09:00
1 parent 46ada4dddb
commit b6c5e1f323
55 files changed
+2861 -38

No files matched your search

+2 -2
View File
@@ -200,7 +200,7 @@ location ~* \.(js|css|json)$ { expires max; access_log off; }
## 코어 API 레퍼런스
<!-- @generated:start:api-readme-index -->
- **문서 수**: 36 · **엔드포인트 수**: 324
- **문서 수**: 36 · **엔드포인트 수**: 325
| 문서 | 도메인 | 엔드포인트 |
| --- | --- | --- |
@@ -235,7 +235,7 @@ location ~* \.(js|css|json)$ { expires max; access_log off; }
| [schedules.md](schedules.md) | `schedules` | 12 |
| [search.md](search.md) | `search` | 1 |
| [seo.md](seo.md) | `seo` | 5 |
| [settings.md](settings.md) | `settings` | 15 |
| [settings.md](settings.md) | `settings` | 16 |
| [system.md](system.md) | `system` | 2 |
| [templates.md](templates.md) | `templates` | 57 |
| [users.md](users.md) | `users` | 12 |
+3 -3
View File
@@ -141,13 +141,13 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
_목록 응답: `data` 배열 항목의 필드. `data` 는 `core.dashboard.alerts` 필터 훅의 결과이며, 알릴 항목이 없으면 빈 배열(`[]`)입니다. 코어 기본 리스너(`ExtensionCompatibilityAlertListener`, `StaticPublishFailureAlertListener`)가 주입하는 항목의 필드는 다음과 같습니다._
_목록 응답: `data` 배열 항목의 필드. `data` 는 `core.dashboard.alerts` 필터 훅의 결과이며, 알릴 항목이 없으면 빈 배열(`[]`)입니다. 코어 기본 리스너(`ExtensionCompatibilityAlertListener`, `StaticPublishFailureAlertListener`, `TrustedProxyAlertListener`)가 주입하는 항목의 필드는 다음과 같습니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | string | `compat_plugins_sirsoft-gdpr` | 알림 식별자 (`compat_{type}_{identifier}` = 자동 비활성화, `recover_{type}_{identifier}` = 재호환. 알림 닫기(dismiss) 상태 판정 키) |
| type | string | `warning` | 알림 등급 (`warning`: 코어 비호환 자동 비활성화, `info`: 재호환 복구 가능) |
| subtype | string | `incompatible_core` | 알림 세부 분류 (`incompatible_core`: 코어 버전 비호환으로 자동 비활성화됨, `recovery_available`: 코어 업그레이드 후 다시 활성화 가능, `static_publish_parent_not_writable` · `static_publish_write_failed` · `static_publish_lock_unavailable`: 초기 화면 파일 생성이 2회 이상 연속 실패 — 각각 폴더 권한 / 디스크 공간 / 캐시 저장소가 원인) |
| type | string | `warning` | 알림 등급. **화면 배치를 결정합니다** — `warning` 은 관리자 대시보드 **상단 배너**로, 그 외(`info` 등)는 하단 「시스템 알림」 카드로 렌더됩니다. 같은 알림이 두 곳에 중복 노출되지 않습니다 |
| subtype | string | `incompatible_core` | 알림 세부 분류 (`incompatible_core`: 코어 버전 비호환으로 자동 비활성화됨, `recovery_available`: 코어 업그레이드 후 다시 활성화 가능, `static_publish_parent_not_writable` · `static_publish_write_failed` · `static_publish_lock_unavailable`: 초기 화면 파일 생성이 2회 이상 연속 실패 — 각각 폴더 권한 / 디스크 공간 / 캐시 저장소가 원인, `trusted_proxy_missing`: 리버스 프록시 헤더를 수신 중인데 신뢰 프록시가 설정되지 않음) |
| icon | string | `exclamation-triangle` | 아이콘 식별자 (warning: `exclamation-triangle`, info: `check-circle`) |
| title | string | `플러그인 "sirsoft-gdpr" 자동 비활성화됨` | 알림 제목 (다국어 문구 — `extensions.alerts.incompatible_deactivated` / `recovered_title`) |
| message | string | `필요 버전: 7.0.0-beta.9, 현재 설치됨: 7.0.0-beta.8` | 알림 본문 (다국어 문구 — `extensions.alerts.incompatible_message` / `recovered_body`) |
+76
View File
@@ -1124,6 +1124,82 @@ HTTP/1.1 200
서버 실행 환경 정보를 한 번에 조회합니다. OS/웹서버/PHP/DB/Laravel/코어 버전, CPU·메모리·디스크 사용량, PHP 주요 설정값(memory_limit·max_execution_time·upload_max_filesize), 주요 경로, PHP 확장 로드 상태, DB 연결 구성 요약 등을 포함합니다. 관리자 시스템 정보 화면과 요구사항 점검용 진단 데이터로 사용됩니다.
### GET /api/admin/settings/trusted-proxy
<!-- @generated:start:api.admin.settings.trusted-proxy -->
- **라우트명**: `api.admin.settings.trusted-proxy`
- **컨트롤러**: `App\Http\Controllers\Api\Admin\SettingsController@trustedProxy`
- **인증/권한**: `auth:sanctum` + `permission:core.settings.read`
**요청 파라미터**
_요청 파라미터 없음._
**요청 예시**
```http
GET /api/admin/settings/trusted-proxy HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
```
**응답 필드** (`data` 내부)
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| forwarded_headers | array | `["X-Forwarded-For","X-Forwarded-Proto"]` | 이 요청이 수신 중인 `X-Forwarded-*` 계열 헤더 이름 목록. 비어 있으면 앞단에 프록시가 없는 직접 노출 구성이다 |
| trusted_configured | boolean | `false` | `TRUSTED_PROXIES` 가 지정되어 있는지. 빈 문자열은 미설정과 같게 판정된다 |
| configured_proxies | string\|null | `null` | 지정된 신뢰 프록시 값 (미설정이면 `null`) |
| is_secure | boolean\|null | `false` | 요청이 HTTPS 로 인식되었는지. 요청이 없는 맥락에서는 `null` |
| client_ip | string\|null | `10.0.0.5` | 방문자 IP 로 인식된 값. 신뢰 프록시가 없으면 프록시 자신의 주소가 된다 |
| remote_addr | string\|null | `10.0.0.5` | 직전 호출 IP(`REMOTE_ADDR`). `client_ip` 와 같으면서 `forwarded_headers` 가 비어 있지 않으면 모든 방문자가 한 사람으로 기록되고 있는 상태다 |
| status | string | `warning` | 진단 결과. `warning`(프록시 헤더 수신 중 + 신뢰 프록시 미설정) / `ok` / `not_applicable`(요청이 없는 맥락) |
**응답 예시**
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "성공적으로 처리되었습니다.",
"data": {
"forwarded_headers": [
"X-Forwarded-For",
"X-Forwarded-Proto"
],
"trusted_configured": false,
"configured_proxies": null,
"is_secure": false,
"client_ip": "10.0.0.5",
"remote_addr": "10.0.0.5",
"status": "warning"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 |
<!-- @generated:end -->
**설명**
리버스 프록시 뒤에서 구동 중인지, 그리고 신뢰할 프록시가 지정되어 있는지를 진단합니다. **읽기 전용이며 대응하는 쓰기 엔드포인트가 없습니다** — 값은 `.env` 의 `TRUSTED_PROXIES` 로만 변경합니다.
판정식은 "HTTPS 인식 실패" 가 아니라 `forwarded_headers 가 비어 있지 않음 AND trusted_configured 가 거짓` 입니다. HTTP 전용 사이트가 프록시 뒤에 있는 구성에서는 혼합 콘텐츠 차단이 없어 화면이 정상으로 보이지만, 방문자 IP 기록·통보 IP 화이트리스트·로그인 시도 제한은 그대로 어긋나기 때문입니다.
같은 판정을 관리자 대시보드 알림, 환경설정 > 고급 화면, 설치 마법사, `php artisan trusted-proxy:status` 가 공유합니다. 설정 방법과 도입 시 후속 조치는 [reverse-proxy.md](../reverse-proxy.md) 를 참고하세요.
### POST /api/admin/settings/test-driver
<!-- @generated:start:api.admin.settings.test-driver -->
- **라우트명**: `api.admin.settings.test-driver`