diff --git a/history/2026-04-25-netbis-npm-vector-msg-rewrite.md b/history/2026-04-25-netbis-npm-vector-msg-rewrite.md new file mode 100644 index 0000000..6eeccb1 --- /dev/null +++ b/history/2026-04-25-netbis-npm-vector-msg-rewrite.md @@ -0,0 +1,119 @@ +--- +date: 2026-04-25 +topic: NPM Vector `_msg` 재합성 — proxy_v2 → 표준 nginx combined +areas: [services/netbis.md, infra/security/crowdsec-safeline.md, infra/platform/victorialogs.md] +tags: [netbis, vector, vrl, crowdsec, nginx-logs, observability] +--- + +## 배경 + +[[2026-04-23-netbis-npm-vl-collection|NPM → zlambda → VL 파이프라인]] + [[2026-04-23-netbis-npm-client-ip-fix|client_ip 정비]] 완료 후 CrowdSec 파서 통과율 확인 결과 `child-crowdsec/nginx-logs` unparsed 84%. 원인: NPM `proxy_v2` raw 포맷(`[Client x] [Real y] [XFF z] ...`)이 hub 파서의 `NGINXACCESS` grok과 매칭되지 않음. cscli explain으로 100% 실패 확인. + +이전 정본에 `nginx-logs parser가 NPM proxy format도 호환 처리 (lenient)` 라고 잘못 기록되어 있던 부분 정정 — 호환 안 됨, Vector 측에서 표준 포맷으로 재합성하는 방식 채택. + +## 결정 + +NPM Vector `parse_npm_access` remap 끝부분에 `_msg` 재작성 블록 추가. 이미 추출된 메타필드를 활용해 표준 nginx combined 포맷을 합성하여 `.message` 에 덮어쓰기. 원본은 `.original_message` 에 백업. + +대안 비교: +- (A) **Vector 측 재합성** ← 선택. 모든 다운스트림(VL, CrowdSec, anomaly-detect 등)에 표준 포맷 일괄 공급 +- (B) CrowdSec 측 custom 파서 작성 — child-nginx-logs를 NPM proxy_v2용으로 분기. 분기 추가시마다 유지보수 부담 +- (C) NPM nginx 측 log_format을 표준 combined로 변경 — Real/XFF 메타정보 손실, IP 추적 불가 + +## 변환 로직 + +```vrl +lf = .log_format +if lf != "raw" { + ip_v = .client_ip + if ip_v == null { ip_v = .remote_addr } + ip_s = to_string(ip_v) ?? "-" + if ip_s == "" { ip_s = "-" } + m_s = to_string(.method) ?? "-" + ... + ts_s = to_string(.log_time) ?? "" # NPM $time_local + if ts_s == "" { + ts_v = .timestamp # fallback: Vector ingest time + if is_nullish(ts_v) { ts_v = now() } + fmt, fmt_err = format_timestamp(ts_v, "%d/%b/%Y:%H:%M:%S %z") + if fmt_err == null { ts_s = fmt } else { ts_s = format_timestamp!(now(), "%d/%b/%Y:%H:%M:%S %z") } + } + .original_message = .message + .message = ip_s + " - - [" + ts_s + "] \"" + m_s + " " + p_s + " HTTP/1.1\" " + st_s + " " + b_s + " \"" + r_s + "\" \"" + u_s + "\"" +} +``` + +각 parse 분기에서 `.log_time = m.ts` 도 추가해 NPM 의 `$time_local` 보존 (재합성 시 `[$time_local]` 자리에 사용). + +raw 포맷(파싱 실패)은 재작성 안 함 — `_msg` 그대로 둬서 디버깅 단서 보존. + +## Vector 검증 시 함정 + +1. **YAML 주석 내 `$variable` 표기** → Vector env-var interpolation 트리거 → 검증 실패. `$$variable` 로 escape 필요. `$time_local`, `$body_bytes_sent` 등 nginx 변수 표기 모두 영향 +2. **infallible 표현식에 `??` 사용** → VRL 컴파일 에러 (`E651: unnecessary error coalescing operation`). 예: `to_string(.log_format)` 가 모든 분기에서 string으로 설정되었음을 컴파일러가 추론 → `??` 가 dead code → 직접 `.log_format` 참조로 변경 + +## npm-1 단독 검증 + +합성 라인 주입: +``` +[25/Apr/2026:06:10:56 +0000] - 200 200 - GET http heimdall.test "/transform-verify" [Client 203.0.113.55] [Real 172.71.24.1] [XFF 203.0.113.55] [Length 1234] [Gzip 1.5] [Sent-to 42.0.0.1] "heimdall-transform-test/1.0" "https://heimdall.test/ref" +``` + +VL 도달 후 `_msg`: +``` +203.0.113.55 - - [25/Apr/2026:06:10:56 +0000] "GET /transform-verify HTTP/1.1" 200 1234 "https://heimdall.test/ref" "heimdall-transform-test/1.0" +``` + +`cscli explain --type nginx` (jp1:crowdsec): +``` +├ s01-parse +| └ 🟢 crowdsecurity/nginx-logs (+23 ~2) +├ s02-enrich +| ├ 🟢 crowdsecurity/dateparse-enrich (+2 ~2) +| ├ 🟢 crowdsecurity/geoip-enrich (+9) +| ├ 🟢 crowdsecurity/http-logs (+7) +├-------- parser success 🟢 +├ Scenarios + ├ 🟢 crowdsecurity/http-crawl-non_statics + ├ 🟢 custom/apisix-high-rate-per-ip + └ 🟢 custom/apisix-single-path-flood +``` + +s01-parse 통과 + 3개 시나리오 매칭. + +## 6대 롤링 배포 + +npm-1 → 5대(npm-2..6) 순차. 각 호스트 `vector validate` 통과 후 `systemctl kill -SIGKILL && reset-failed && start`. + +VL 샘플 확인 (host별): + +| Host | 샘플 _msg (앞 150자) | +|---|---| +| npm-1 | `203.0.113.55 - - [25/Apr/2026:06:10:56 +0000] "GET /transform-verify HTTP/1.1" 200 1234 ...` | +| npm-2 | `255.95.21.9 - - [25/Apr/2026:06:13:12 +0000] "POST /main/betIt HTTP/1.1" 200 56 ...` | +| npm-3 | `170.187.231.160 - - [25/Apr/2026:06:13:12 +0000] "POST /vinus/cback?bet_..." 200 85 "-" "-"` | +| npm-4 | `254.5.9.221 - - [25/Apr/2026:06:13:14 +0000] "POST /wel/login.html HTTP/1.1" 200 90 "https://fall-vip.com/wel.html" ...` | + +전부 표준 nginx combined 포맷. + +## 30분 후 검증 + +baseline 캡처 (06:13:40Z): +``` +victorialogs source: 977.24k read / 514.72k parsed / 462.52k unparsed (47.3% unparsed) +child-crowdsec/nginx-logs: 3.29M hits / 514.72k parsed / 2.78M unparsed (84.5% unparsed) +``` + +30분 후 cscli metrics 재캡처 → Δ window 비율 계산 → Discord webhook 푸시 (백그라운드 스크립트 예약). + +## 운영 주의 + +- **`.original_message` 보존** — 디버깅·재처리 용도. 재합성 결과가 이상하면 original 로 진단 가능 +- **로그 회전 안전성** — VRL 변경은 새 라인부터 적용. 기존 unparsed 누적은 그대로 (cscli metrics는 cumulative). delta 비교에서만 효과 보임 +- **Vector restart 전략** — 기존 inflight ES sink 때문에 graceful shutdown 60s 대기. 빠른 전환 필요 시 SIGKILL → reset-failed → start +- **추가 영향** — VL 인덱스에 `_msg` 가 표준 포맷으로 들어가서 LogsQL 쿼리 패턴이 단순해짐. 동시에 `.original_message` 가 별도 필드로 추가되어 retention 비용 약간 증가 (~5-10%) + +## 후속 + +- [ ] 30분 metrics 비교 결과 (백그라운드 스크립트 자동 푸시) +- [ ] 안정화 후 CrowdSec acquisition에 VL `service:npm log_type:access` 추가 → APISIX 파이프라인과 동일하게 시나리오 매칭 → bouncer 결정 diff --git a/infra/security/crowdsec-safeline.md b/infra/security/crowdsec-safeline.md index 3fcca40..fe256b3 100644 --- a/infra/security/crowdsec-safeline.md +++ b/infra/security/crowdsec-safeline.md @@ -89,7 +89,13 @@ NPM-1..6 (Linode Tokyo public) Vector 0.55 file→http Netbis 오리진(NPM) nginx access/error 로그 수집. VL 은 LAN-only(192.168.9.53) 이므로 public NPM이 도달할 수 없어 zlambda 를 HTTP 중계로 투입. 상세: [[../../services/netbis#로그-수집-vector-→-zlambda-→-victorialogs|netbis]], [[../../history/2026-04-23-netbis-npm-vl-collection|history]] -**2026-04-25 CrowdSec 연동 완료.** 통합 acquisition `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml` 의 query에 `(program:npm AND log_type:access)` 추가. nginx-logs parser가 NPM proxy format도 호환 처리 (grok이 충분히 lenient하여 핵심 필드 추출). `cscli explain --type nginx` 로 한 줄 시뮬 결과 s00-raw → s01-parse(nginx-logs +22 ~2) → s02-enrich → 시나리오(`custom/apisix-high-rate-per-ip`, `custom/apisix-single-path-flood`) 매칭 확인. +**2026-04-25 CrowdSec 연동 완료.** 통합 acquisition `/etc/crowdsec/acquis.d/victorialogs-nginx.yaml` 의 query에 `(program:npm AND log_type:access)` 추가. **단**: 초기 `cscli explain` 결과로 "nginx-logs parser가 NPM proxy format도 lenient 호환"이라 적었던 부분은 잘못된 검증이었음 — 실제로는 `child-crowdsec/nginx-logs` unparsed 84%로 grok 미매칭. 정정 사항은 아래 `_msg` 재합성 서브섹션 참조. + +#### `_msg` 재합성 (proxy_v2 → nginx combined, 2026-04-25) + +CrowdSec `crowdsecurity/nginx-logs` Hub 파서는 표준 `NGINXACCESS` grok 만 매칭한다. NPM 의 `proxy_v2` raw 포맷(`[Client x] [Real y] ...`)은 grok 미지원이라 `child-crowdsec/nginx-logs` unparsed 84% 발생. **해결**: NPM Vector remap에 `_msg` 재작성 블록 추가 — 이미 추출된 메타필드(client_ip/method/path/status/bytes/user_agent/referer/log_time)로 표준 nginx combined 포맷을 합성해 `.message` 에 덮어쓰기. 원본은 `.original_message` 에 보존. 검증: `cscli explain --type nginx` 결과 `s01-parse: crowdsecurity/nginx-logs (+23 ~2)` 통과 + 시나리오(`crowdsecurity/http-crawl-non_statics`, `custom/apisix-high-rate-per-ip`, `custom/apisix-single-path-flood`) 매칭 확인. 상세: [[../../history/2026-04-25-netbis-npm-vector-msg-rewrite|history]] + +`_msg` 가 이제 표준 nginx combined 포맷이라 hub 파서 그대로 동작 — 통합 acquisition의 `(program:npm AND log_type:access)` 분기에서 LAPI decision까지 자연스럽게 흐름. ### SafeLine → CrowdSec (실시간, PG LISTEN/NOTIFY)