> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# Nginx 실제 IP가 프록시 주소로 나올 때: X-Forwarded-For와 신뢰 범위 확인
- URL: https://mlog.me/nginx-real-ip-x-forwarded-for-trusted-proxy/
- Published: 2026-10-04T06:44:56.000Z
- Updated: 2026-10-04T06:44:56.000Z
- Description: Nginx 로그에 프록시 IP가 남을 때 X-Forwarded-For와 real_ip_recursive를 구분합니다. 로컬 재현으로 헤더 위조, 신뢰 프록시 범위, NPM 적용 전 점검을 확인합니다.
- Author: mLog
- Tags: 서버·인프라, Nginx, Nginx Proxy Manager, 네트워크, 네트워크 보안

접속 로그의 모든 IP가 Nginx Proxy Manager나 내부 프록시 주소로 보인다면, 먼저 **어느 서버가 직접 연결했고, 어느 프록시가 헤더를 만들었는지** 확인해야 한다. `X-Forwarded-For`의 첫 값을 꺼내 쓰는 것만으로는 해결되지 않는다.

이 글은 실제 NGINX를 로컬에서 실행해 주소 선택과 헤더 전달을 비교한다. 운영 중인 mLog의 장애를 해결했다는 기록은 아니다. NPM 화면에서 설정 한 줄을 추가하면 모든 환경에서 해결된다는 방식으로도 접근하지 않는다.

**검증 범위:** 2026-10-04, Linux x86\_64, NGINX 1.30.5, Python 3.12.14\. HTTP realip 모듈을 포함한 테스트 빌드와 루프백 HTTP 요청으로 9개 검증을 수행했다. Docker·NPM·CDN·TLS·Spring 통합 동작은 이 실험에 포함하지 않았다.

## 1\. 연결한 주소와 전달받은 주소를 나누어 본다

사용자가 프록시를 거쳐 애플리케이션에 접근하면 애플리케이션의 직접 연결 상대는 프록시다. 이때 원래 방문자 정보를 전달하려고 사용하는 값 중 하나가 `X-Forwarded-For`다. 그러나 HTTP 헤더는 클라이언트도 보낼 수 있다.

점검할 값은 세 가지다.

| 값                        | 확인하려는 내용                       |
| ------------------------ | ------------------------------ |
| $remote\_addr            | NGINX가 현재 요청의 클라이언트 주소로 사용하는 값 |
| $realip\_remote\_addr    | realip 처리 이전의 연결 상대 주소         |
| $http\_x\_forwarded\_for | 요청에 들어온 X-Forwarded-For 원문     |

NGINX realip 모듈은 신뢰하도록 지정한 연결 상대가 보낸 헤더를 바탕으로 주소를 바꾼다. `set_real_ip_from`은 방문자 허용 목록이 아니라 **주소 정보를 대신 전달할 프록시의 신뢰 목록**이다. `real_ip_header`는 읽을 헤더를 정한다. [공식 realip 문서](https://nginx.org/en/docs/http/ngx%5Fhttp%5Frealip%5Fmodule.html?ref=mlog.me)

예를 들어 다음은 특정 중간 프록시만 신뢰하는 설정 조각이다. IP는 설명용이며 그대로 운영에 붙이지 않는다.

```nginx
# 이 주소의 프록시가 헤더를 올바르게 정리한다는 전제
set_real_ip_from 10.20.0.10;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

```

`0.0.0.0/0`이나 `::/0`을 넣으면 신뢰 대상을 전 세계로 넓히는 셈이다. 문제를 빨리 없애려고 이렇게 바꾸기보다 실제 직전 프록시의 주소와 경로부터 찾는다. Docker 네트워크 전체를 신뢰할 때도 그 안의 다른 컨테이너가 요청을 만들 수 있다는 점을 고려한다.

## 2\. real\_ip\_recursive는 “첫 IP를 선택”하는 옵션이 아니다

신뢰 프록시가 보낸 XFF가 다음과 같다고 가정한다.

```text
198.51.100.7, 10.0.0.20

```

로컬 실험에서는 직접 연결 상대 `127.0.0.1`과 헤더 속 중간 프록시 `10.0.0.20`을 신뢰하도록 설정했다. 후자는 실제 서버가 아니라 테스트 헤더의 모의 경유지다.

| 실험 조건                               | 관측한 $remote\_addr |
| ----------------------------------- | ----------------- |
| 연결 상대를 신뢰하지 않음                      | 127.0.0.1         |
| 신뢰 상대, recursive off                | 10.0.0.20         |
| 신뢰 상대, recursive on                 | 198.51.100.7      |
| 맨 왼쪽에 203.0.113.99 추가, recursive on | 198.51.100.7      |
| 연결 원본을 신뢰 목록 밖 127.0.0.2로 변경        | 127.0.0.2         |
| XFF 자체가 없음                          | 127.0.0.1         |

공식 정의에서 recursive가 꺼져 있으면 마지막 헤더 주소를 사용하고, 켜져 있으면 마지막 비신뢰 주소를 선택한다. 이 동작은 먼저 원래 연결 상대가 신뢰 목록에 들어오는 조건을 전제로 한다. [NGINX 정의](https://nginx.org/en/docs/http/ngx%5Fhttp%5Frealip%5Fmodule.html?ref=mlog.me#real%5Fip%5Frecursive)

실험에서 맨 왼쪽에 임의 값을 넣어도 결과가 바뀌지 않은 것은 오른쪽의 신뢰 경유지 다음에서 실제 선택 대상이 결정됐기 때문이다. 이것이 모든 토폴로지에서 위조를 막는다는 의미는 아니다. 실제 프록시가 기존 헤더를 어떻게 처리하는지도 함께 확인해야 한다.

## 3\. 헤더를 추가하는 것과 신뢰할 값으로 정리하는 것은 다르다

흔히 보이는 설정은 다음과 같다.

```nginx
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

```

`$proxy_add_x_forwarded_for`는 기존 XFF에 현재 `$remote_addr`를 이어 붙인다. 기존 값의 출처를 검증하거나 앞부분을 지우는 기능은 아니다. `proxy_set_header`는 상위 설정의 상속 조건도 있으므로, 실제 적용되는 location을 기준으로 읽어야 한다. [공식 proxy 모듈 문서](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fproxy%5Fmodule.html?ref=mlog.me#proxy%5Fset%5Fheader)

로컬에서 `203.0.113.99`라는 XFF를 보내자, 뒤쪽 HTTP 서버는 다음 값을 받았다.

```text
203.0.113.99, 127.0.0.1

```

반면 **사용자가 직접 연결하는 최초 신뢰 경계**에서 기존 XFF를 `$remote_addr`로 덮어쓴 실험에서는 `127.0.0.1`만 전달됐다. 이것은 두 가지 다른 설계다. 신뢰된 상위 프록시의 정보를 보존해야 하는 중간 프록시에 동일한 덮어쓰기를 적용하면 원래 방문자 정보가 사라질 수 있다.

다음은 CDN 없는 단일 프록시가 HTTP를 직접 받는 실험용 location 예시다. 비신뢰 클라이언트 헤더로 `$remote_addr`를 재작성하는 realip 설정이 없다는 전제다. 인증·TLS·Host 허용 정책까지 포함한 운영 완성 설정은 아니다.

```nginx
location / {
    proxy_pass http://127.0.0.1:18081;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header Forwarded "";
    proxy_set_header X-Forwarded-Prefix "";
    proxy_set_header X-Forwarded-Ssl "";
}

```

이 구성의 목적은 앱이 사용할 출처 정보를 경계에서 다시 만드는 것이다. 실제 앱이 읽는 별도 헤더가 있으면 그것도 검토해야 한다. 빈 값인 헤더를 업스트림에 전달하지 않는 동작은 NGINX 공식 문서에 설명돼 있다.

Spring 공식 문서 역시 경계 프록시에서 `Forwarded`와 `X-Forwarded-*`를 함께 다루도록 안내한다. XFF 하나를 정리했어도 다른 전달 헤더를 앱이 우선 읽으면 의도와 다른 결과가 생길 수 있다. 애플리케이션의 전달 헤더 처리 방식까지 대조한다. [Spring Forwarded Headers](https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html?ref=mlog.me#filters-forwarded-headers)

## 4\. Nginx Proxy Manager에서는 최종 생성 설정을 먼저 확인한다

NPM의 Advanced 입력란에 설정을 추가하기 전에 현재 적용된 server·location·include를 확인한다. 서비스명 `npm`은 예시이며 실제 Compose 서비스명으로 바꾼다.

```sh
docker compose exec -T npm nginx -V
docker compose exec -T npm nginx -t
# 전체 설정에는 내부 주소 등이 포함될 수 있으므로 비공개 파일로 보관
(umask 077; docker compose exec -T npm nginx -T > npm-nginx-effective.txt 2>&1)

```

`-V`로 빌드 옵션, `-t`로 문법과 참조 파일, `-T`로 구성 덤프를 확인한다. 문법 검사가 통과해도 IP 선택이나 인증 정책이 맞다는 뜻은 아니다. [NGINX 명령 옵션](https://nginx.org/en/docs/switches.html?ref=mlog.me)

NPM 공식 문서는 `/data/nginx/custom`의 파일별 삽입 위치를 구분한다. `http.conf`는 http 블록, `server_proxy.conf`는 모든 proxy server 블록에 들어간다. 호스트 하나의 문제를 해결하려고 전체 호스트 설정을 바꾸지 않도록 적용 범위를 먼저 정한다. [NPM Advanced Configuration](https://nginxproxymanager.com/advanced-config/?ref=mlog.me)

확인할 질문은 다음과 같다.

1. NPM에 직접 연결하는 상대는 방문자, CDN, 로드밸런서, 다른 프록시 중 누구인가?
2. 해당 경로가 헤더를 생성·교체·추가하는 방식은 무엇인가?
3. realip 설정과 `proxy_set_header`가 어느 블록에서 적용되는가?
4. 앱은 XFF, Forwarded, 별도 제공자 헤더 중 무엇을 읽는가?
5. 외부에서 앱 포트로 직접 들어오는 우회 경로가 남아 있는가?

네트워크 구조가 먼저 필요하다면 [NPM·Ghost·MySQL 네트워크 분리](https://mlog.me/docker-compose-network-isolation-npm-ghost-mysql/)부터 확인한다. 백엔드 연결 자체가 실패한다면 [NPM 502 진단](https://mlog.me/nginx-proxy-manager-502-bad-gateway-docker-network/)이 선행 단계다.

## 5\. CDN 뒤에서는 `$scheme`과 원본 접속 경로도 확인한다

방문자가 CDN에는 HTTPS로 연결해도 CDN이 원본에는 HTTP로 연결할 수 있다. 이런 경로에서 `$scheme`을 무조건 원래 방문자의 프로토콜이라고 해석하면 맞지 않는다. 단일 프록시용 예시를 다중 프록시에 그대로 옮기지 않는 이유다.

CDN을 사용한다면 제공자의 공식 헤더 정책, 실제 연결 원본, 허용해야 하는 IP 범위를 따로 확인한다. 또한 원본에 직접 접근할 수 있는 요청과 CDN 경유 요청을 구분해야 한다. 이 글은 특정 CDN의 전체 설정을 실행 검증하지 않았으므로 제공자별 CIDR이나 헤더 한 줄을 정답으로 제시하지 않는다.

IP는 공유될 수 있고 접속 경로에 따라 바뀔 수 있다. 로그에서 방문자 IP를 복원했다고 사용자 인증까지 된 것은 아니다. 인증·접근 정책은 [NPM·Authelia 접근 제어](https://mlog.me/server-security-check-npm-authelia-access-control/)와 별도로 점검한다.

## 6\. 변경 후에는 정상 요청과 임의 헤더 요청을 함께 보낸다

공개 서비스에 진단 응답을 새로 열지 말고, 로컬 또는 접근이 제한된 테스트 경로에서 확인한다. 다음은 테스트 서버의 예시 요청이다.

```sh
curl --max-time 5 http://127.0.0.1:18080/
curl --max-time 5 \
  -H 'X-Forwarded-For: 203.0.113.99' \
  -H 'Forwarded: for=203.0.113.99;proto=https' \
  http://127.0.0.1:18080/

```

이 주소와 포트에 테스트 서버를 먼저 준비해야 한다. 첨부 실행기는 사용 가능한 루프백 포트를 선택하므로 위 고정 포트 예시와 다르다.

진단 로그가 필요하면 사용자 입력을 구분해서 기록한다. `log_format`은 http 문맥에서 선언한다. 아래는 독립 NGINX용 예시이며 NPM에는 실제 include 위치와 기존 access\_log를 확인한 뒤 적용한다.

```nginx
log_format realip_probe escape=json
    '{"client":"$remote_addr","peer":"$realip_remote_addr",'
    '"xff":"$http_x_forwarded_for"}';
# 테스트 대상 server 블록 안:
# access_log /path/to/private/realip-probe.log realip_probe;

```

`escape=json`은 문자열 이스케이프를 제공한다. 로그에 남긴 헤더를 신뢰한다는 의미는 아니다. 테스트 뒤 불필요한 진단 로그를 제거하고 IP 보존 범위도 줄인다. [공식 log 모듈 문서](https://nginx.org/en/docs/http/ngx%5Fhttp%5Flog%5Fmodule.html?ref=mlog.me)

원래 설정을 보관한 후 좁은 범위에 변경을 적용하고, 문법 검사·정상 요청·임의 헤더 요청·로그를 대조한다. 결과가 예상과 다르면 백업 설정으로 복구하고 같은 검증을 반복한다. 직접 연결 우회 차단과 실제 NPM 통합은 운영 환경에서 별도로 확인해야 한다.

## 7\. 첨부 재현 자료로 확인한 것

재현 ZIP의 `verify.py`는 실제 NGINX와 로컬 HTTP 수신 서버를 띄운 뒤 9개 조건을 비교한다. 특정 IP 선택을 Python으로 흉내 낸 단위 테스트가 아니다. 설정 검사와 모든 비교가 성공했고, 생성한 NGINX 프로세스는 종료했다.

- 연결 상대 신뢰 여부와 recursive on/off 비교
- 헤더 왼쪽에 임의 주소를 넣는 경우
- 신뢰하지 않은 루프백 원본에서 직접 요청하는 경우
- XFF가 없는 경우
- XFF 추가와 덮어쓰기의 차이
- 대체 Forwarded 헤더 제거 및 프로토콜 재설정

실행 방법은 ZIP 안의 README에 있다. 이 결과는 여기서 사용한 단일 헤더·IPv4·루프백 조건의 증거다. 중복 헤더, 모든 비정상 형식, IPv6, NPM 관리 UI, CDN, TLS, 애플리케이션별 파싱을 포괄하는 인증은 아니다.

실제 IP 문제를 점검할 때에는 **연결 상대 → 신뢰 목록 → 헤더 처리 → 앱의 해석** 순서로 기록한다. 이 네 단계가 일치해야 로그와 접근 정책이 같은 요청을 같은 출처로 이해할 수 있다.

[NGINX 실제 IP 재현 코드·검증 결과nginx-realip-repro-2026-10-04.zip4 KBdownload-circle](https://mlog.me/content/files/2026/10/nginx-realip-repro-2026-10-04.zip "Download")