netbis: NPM client_ip 실 IP 추출 정비 (nginx real_ip_header + Vector VRL)
This commit is contained in:
140
history/2026-04-23-netbis-npm-client-ip-fix.md
Normal file
140
history/2026-04-23-netbis-npm-client-ip-fix.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
date: 2026-04-23
|
||||||
|
topic: Netbis NPM 로그 파이프라인 — client_ip 실 IP 추출 정비
|
||||||
|
areas: [services/netbis.md, infra/security/crowdsec-safeline.md]
|
||||||
|
tags: [netbis, nginx, vector, victorialogs, real-ip, cloudflare]
|
||||||
|
---
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
[[2026-04-23-netbis-npm-vl-collection|오전 구축한 NPM→zlambda→VL 파이프라인]] 의 VL 샘플을 확인한 결과 client IP 필드가 **CF edge IP** 또는 **CF Pseudo IPv4(254.x)** 로 찍혀 AI 분석·rate 집계·LAPI ban 후보 선정에 사용 불가. 실 IP 추출로 정비.
|
||||||
|
|
||||||
|
샘플 (정비 전):
|
||||||
|
```
|
||||||
|
[Client 254.152.57.112] [Real 172.64.213.21] [XFF 254.152.57.112]
|
||||||
|
Client = 254.x (CF Pseudo 또는 CF edge) — 익명화/CF 노드
|
||||||
|
```
|
||||||
|
|
||||||
|
## 현황 조사
|
||||||
|
|
||||||
|
`docker exec <npm_container> nginx -T | grep -E "log_format|real_ip_header"`:
|
||||||
|
|
||||||
|
| Host | log_format (before) | real_ip_header (before) | 비고 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| npm-1 | `[Client $remote_addr]` 만 | X-Real-IP | Rewrite 무효 (CF는 X-Real-IP 안 보냄) |
|
||||||
|
| npm-2 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 — psd777 트래픽 Client=CF edge |
|
||||||
|
| npm-3 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 |
|
||||||
|
| npm-4 | `[Client $remote_addr] [Real $realip_remote_addr] [XFF $http_x_forwarded_for]` | **CF-Connecting-IP** | 이미 정상. 이 포맷을 기준으로 통일 |
|
||||||
|
| npm-5 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 |
|
||||||
|
| npm-6 | `[Client $remote_addr]` 만 | X-Real-IP | 동일 (lepresidente image) |
|
||||||
|
|
||||||
|
`set_real_ip_from`: 6대 전부 Cloudflare + CloudFront + Docker 사설 대역 포함 (총 258 entries). **trusted proxy 쪽은 문제 없음**. 문제는 `real_ip_header` 가 X-Real-IP (CF 미매칭) 이라는 점 + log_format 에 Real/XFF 필드가 빠져있다는 점.
|
||||||
|
|
||||||
|
## 수정 — nginx (npm-1/2/3/5/6 5대)
|
||||||
|
|
||||||
|
npm-4 기준으로 `/etc/nginx/nginx.conf` 패치 + `nginx -s reload`. 패치 로직 (idempotent sed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# log_format proxy/standard에 Real+XFF 삽입
|
||||||
|
sed -i 's|\[Client \$remote_addr\] \[Length|\[Client \$remote_addr\] \[Real \$realip_remote_addr\] \[XFF \$http_x_forwarded_for\] \[Length|g'
|
||||||
|
# real_ip_header 교체
|
||||||
|
sed -i 's|real_ip_header X-Real-IP;|real_ip_header CF-Connecting-IP;|g'
|
||||||
|
```
|
||||||
|
|
||||||
|
실행: `docker cp` out → sed → `docker cp` in → `docker exec nginx -t && nginx -s reload`.
|
||||||
|
|
||||||
|
**주의**: NPM 컨테이너의 `/etc/nginx/nginx.conf` 는 이미지 파일 레이어에 있어 컨테이너 재생성 시 원복됨. **영속화 필요 시** docker-compose `volumes:` 에 custom nginx.conf 를 host bind mount 로 추가해야 함. 본 작업은 runtime-only 패치 (재부팅/재생성 후 재적용 필요).
|
||||||
|
|
||||||
|
재실행 안전 — 패치 스크립트는 `[Real $realip_remote_addr]` 마커 체크로 중복 실행 방지.
|
||||||
|
|
||||||
|
## 수정 — Vector VRL (6대 전부)
|
||||||
|
|
||||||
|
`/etc/vector/vector.yaml` 의 `parse_npm_access` transform 재작성. 핵심:
|
||||||
|
|
||||||
|
1. **정규식 4단 fallback**: `proxy_v2` (Real+XFF 포함) → `proxy_v1` (legacy) → `standard_v2` → `standard_v1` → `raw`
|
||||||
|
2. **필드 추출**: `remote_addr`(raw Client), `cf_edge_ip`(Real = `$realip_remote_addr`, TCP peer), `xff_chain`(XFF 원본)
|
||||||
|
3. **client_ip 유도**: nginx real_ip 재작성 결과인 Client 값을 CF IP 대역(15개 CIDR)과 대조. 대역 밖이면 `client_ip = remote_addr`, 대역 안이면 `client_ip = null` + `client_ip_is_cf_edge=true` + `client_ip_source="cf_edge_rewrite_failed"` (rewrite 실패 케이스 명시)
|
||||||
|
4. CF Pseudo IPv4 `254.0.0.0/8` 은 CF CIDR 리스트에 **포함 안 함** — IPv6 방문자에 대한 CF 매핑 ID로 동작하므로 식별자로 유효
|
||||||
|
|
||||||
|
CF IPv4 CIDR 리스트 (VRL 내 inline):
|
||||||
|
```
|
||||||
|
173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22,
|
||||||
|
141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20,
|
||||||
|
197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13,
|
||||||
|
104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22
|
||||||
|
```
|
||||||
|
|
||||||
|
배포: 6대 전부 `vector.yaml` 재작성(전체 파일 푸시), `systemctl kill -s SIGKILL vector && reset-failed && start` (기존 inflight retry 회피).
|
||||||
|
|
||||||
|
## 검증
|
||||||
|
|
||||||
|
LogsQL (VL):
|
||||||
|
|
||||||
|
```
|
||||||
|
service:npm log_format:proxy_v2 _time:3m # 총 1000+ 건
|
||||||
|
service:npm log_format:proxy_v2 client_ip_is_cf_edge:true _time:3m # 0 건
|
||||||
|
```
|
||||||
|
|
||||||
|
샘플 (npm-2 psd777.com, 정비 후):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "npm-2",
|
||||||
|
"domain": "psd777.com",
|
||||||
|
"remote_addr": "182.208.67.53",
|
||||||
|
"client_ip": "182.208.67.53",
|
||||||
|
"cf_edge_ip": "172.70.123.209",
|
||||||
|
"xff_chain": "182.208.67.53",
|
||||||
|
"client_ip_is_cf_edge": "false",
|
||||||
|
"client_ip_source": "remote_addr",
|
||||||
|
"log_format": "proxy_v2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
상위 client_ip (5분 윈도우):
|
||||||
|
```
|
||||||
|
77 106.101.11.143 (KR)
|
||||||
|
62 106.101.2.46 (KR)
|
||||||
|
44 247.138.125.110 (CF Pseudo, IPv6)
|
||||||
|
42 251.48.101.40 (CF Pseudo)
|
||||||
|
37 254.205.147.150 (CF Pseudo)
|
||||||
|
36 182.225.199.226 (KR)
|
||||||
|
35 247.242.90.88 (CF Pseudo)
|
||||||
|
34 251.166.27.129 (CF Pseudo)
|
||||||
|
33 250.160.168.101 (CF Pseudo)
|
||||||
|
30 121.131.161.96 (KR)
|
||||||
|
```
|
||||||
|
|
||||||
|
전부 **실 클라이언트** (KR IPv4 또는 CF Pseudo). CF edge (172.64.x, 162.158.x, …) 하나도 없음.
|
||||||
|
|
||||||
|
호스트별 proxy_v2 수신량(3분 창):
|
||||||
|
```
|
||||||
|
npm-1: 0 (shared/no traffic)
|
||||||
|
npm-2: 317 (psd777 실트래픽)
|
||||||
|
npm-3: 510 (rss-555/7790)
|
||||||
|
npm-4: 562 (fall-vip/mvp/vip7)
|
||||||
|
npm-5: 0
|
||||||
|
npm-6: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 운영 주의
|
||||||
|
|
||||||
|
- **NPM 컨테이너 재생성 시 nginx 패치 소실** — `docker-compose down && up` 후에는 log_format + real_ip_header 원복. 영속 대응: docker-compose에 nginx.conf custom mount 추가 (후속 과제)
|
||||||
|
- **Vector VRL에서 `ip_cidr_contains` 호출** — VRL 0.12+ 에서 제공. Vector 0.55 기준 사용 가능
|
||||||
|
- **`client_ip_is_cf_edge=true` 모니터링**: 이 플래그가 진실이면 nginx real_ip 재작성 실패 의미. 플래그 상승 추이 감지 시 원인 조사 (CF IP 대역 추가 / `real_ip_recursive` off / CF-Connecting-IP 헤더 누락 등)
|
||||||
|
- **Pseudo IPv4 254/247/250/251.x**: CF Enterprise 없는 플랜에서 IPv6 방문자 → IPv4 역양식. 동일 방문자에 대해 stateful하므로 rate limit/ban 대상으로 사용 가능
|
||||||
|
- **CF Pseudo 차단 시 부수효과**: CF Pseudo IP 는 같은 CF 지역 방문자 여럿이 공유할 수 있음 (매핑 해시에 따라). 차단 결정 시 XFF chain 교차 확인 권장
|
||||||
|
|
||||||
|
## 후속 과제
|
||||||
|
|
||||||
|
- [ ] NPM docker-compose에 custom nginx.conf bind mount 추가 (재생성 영속화)
|
||||||
|
- [ ] `client_ip_is_cf_edge=true` 알람 — VictoriaMetrics LogsQL alert (임계: 시간당 10건)
|
||||||
|
- [ ] CrowdSec scenarios/parser에 `client_ip` 필드 매핑 — VL `service:npm` acquisition 추가 시 IP 기반 bouncer 판단 가능
|
||||||
|
- [ ] `xff_chain` 다중 IP 케이스 (여러 proxy 경유) 전용 파싱 — 현재는 문자열 그대로 저장
|
||||||
|
|
||||||
|
## 롤백
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# nginx original:
|
||||||
|
# sed -i '[Real $realip_remote_addr] [XFF $http_x_forwarded_for] ' 제거 + real_ip_header CF-Connecting-IP → X-Real-IP
|
||||||
|
# Vector: vector.yaml에서 client_ip 관련 블록 제거, parse_regex 를 v1 만 사용
|
||||||
|
```
|
||||||
@@ -242,8 +242,10 @@ NPM-1..6 호스트 Vector(0.55)
|
|||||||
|------|-----|
|
|------|-----|
|
||||||
| NPM Vector 설치 | `sh.vector.dev` 공식 스크립트 → `/usr/local/bin/vector`, systemd unit `vector.service` |
|
| NPM Vector 설치 | `sh.vector.dev` 공식 스크립트 → `/usr/local/bin/vector`, systemd unit `vector.service` |
|
||||||
| NPM Vector 설정 | `/etc/vector/vector.yaml` (mode 600, bearer 평문 포함), checkpoints `/var/lib/vector/npm_{access,error}/` |
|
| NPM Vector 설정 | `/etc/vector/vector.yaml` (mode 600, bearer 평문 포함), checkpoints `/var/lib/vector/npm_{access,error}/` |
|
||||||
| 라벨 | `host=npm-1..6`, `service=npm`, `log_type=access|error`, `zone=<서빙 zone CSV>`(npm-1/5/6 은 `shared`), `relay=zlambda`, `program=npm`, `proxy_host_id`(파일명에서 추출) |
|
| 라벨 | `host=npm-1..6`, `service=npm`, `log_type=access|error`, `zone=<서빙 zone CSV>`(npm-1/5/6 은 `shared`), `relay=zlambda`, `program=npm`, `proxy_host_id`(파일명에서 추출), `client_ip`(실 IP), `cf_edge_ip`, `xff_chain`, `client_ip_is_cf_edge`, `client_ip_source`, `remote_addr`(raw Client) |
|
||||||
| 파싱 포맷 | NPM proxy log_format + standard log_format(fallback/letsencrypt). 실패 시 `log_format=raw` |
|
| 파싱 포맷 | NPM proxy/standard v2 (Real+XFF 포함, 2026-04-23 이후) 우선, v1(Real+XFF 없음) legacy fallback. 모두 실패 시 `log_format=raw` |
|
||||||
|
| nginx 설정 | 6대 전부 `real_ip_header CF-Connecting-IP`, `real_ip_recursive on`, `set_real_ip_from` 에 CF + CloudFront IP 대역 포함. log_format에 `[Client $remote_addr] [Real $realip_remote_addr] [XFF $http_x_forwarded_for]` 3필드 기록 (2026-04-23 일괄 패치) |
|
||||||
|
| client_ip 의미 | nginx real_ip 재작성 후의 실 고객 IP (CF-Connecting-IP 경유). IPv6 방문자의 경우 CF Pseudo IPv4 254.x 범위로 나타남 — 시나리오/AI 분석에 식별자로 사용 가능. Rewrite 실패(Client == Real) 시 `client_ip_is_cf_edge=true` + `client_ip=null` + `client_ip_source=cf_edge_rewrite_failed` |
|
||||||
| zlambda relay | [[zlambda]] NixOS container `vector-relay` (Docker `timberio/vector:0.45.0-debian`, net `vector-net`, port 9999/tcp) |
|
| zlambda relay | [[zlambda]] NixOS container `vector-relay` (Docker `timberio/vector:0.45.0-debian`, net `vector-net`, port 9999/tcp) |
|
||||||
| zlambda 모듈 | `~/nixos-infra/vector.nix` — 전용 render/env systemd + Docker oci-container |
|
| zlambda 모듈 | `~/nixos-infra/vector.nix` — 전용 render/env systemd + Docker oci-container |
|
||||||
| bearer token | zlambda agenix `secrets/vector-bearer-token.age` (kaffa + zlambda host key 복호화). NPM config 에는 평문, Vault 백업은 `secret/cloud/vector-relay-netbis` |
|
| bearer token | zlambda agenix `secrets/vector-bearer-token.age` (kaffa + zlambda host key 복호화). NPM config 에는 평문, Vault 백업은 `secret/cloud/vector-relay-netbis` |
|
||||||
|
|||||||
Reference in New Issue
Block a user