diff --git a/history/2026-04-23-netbis-npm-client-ip-fix.md b/history/2026-04-23-netbis-npm-client-ip-fix.md new file mode 100644 index 0000000..3202074 --- /dev/null +++ b/history/2026-04-23-netbis-npm-client-ip-fix.md @@ -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 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 만 사용 +``` diff --git a/services/netbis.md b/services/netbis.md index e71348a..696cef0 100644 --- a/services/netbis.md +++ b/services/netbis.md @@ -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 설정 | `/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`(파일명에서 추출) | -| 파싱 포맷 | NPM proxy log_format + standard log_format(fallback/letsencrypt). 실패 시 `log_format=raw` | +| 라벨 | `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/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 모듈 | `~/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` |