210 lines
10 KiB
Markdown
210 lines
10 KiB
Markdown
---
|
||
title: Netbis Sigmatch — VL 기반 자동 공격 탐지 + CF 차단
|
||
updated: 2026-04-24
|
||
tags: [netbis, security, ai-defense, wip]
|
||
---
|
||
|
||
## 개요
|
||
|
||
Netbis NPM 로그(VictoriaLogs)를 실시간 분석해 **사람 개입 없이 공격을 자동 탐지**하고 CF IP Access Rules에 challenge/block 반영. LAPI·CrowdSec·LLM·사전 정의 룰 모두 사용 안 함.
|
||
|
||
- **저장소**: https://gitea.inouter.com/kaffa/netbis-sigmatch
|
||
- **로컬 개발**: `~/netbis-sigmatch/` (Mac)
|
||
- **배포 예정**: jp1 Incus `ai-sigmatch` 컨테이너 (systemd timer 1분)
|
||
- **책임자**: kappa 직접 개발 (Heimdall 위임 X, 개발 단계)
|
||
|
||
## 핵심 설계 (v2.3, 2026-04-24 CF Access Rules LIVE 모드 추가)
|
||
|
||
### 탐지 트리거 (OR 조건, 하나라도 충족 시 attack mode ON)
|
||
|
||
**A1. Static threshold** (급격한 볼륨 이상)
|
||
```
|
||
현재 5분 총 req > baseline_시간대(UTC hour).p95 × 2.0
|
||
OR > baseline_시간대.max × 1.5
|
||
```
|
||
|
||
**A2. CUSUM Page-Hinkley** (점진적 ramp-up 공격)
|
||
```
|
||
g_t = max(0, g_{t-1} + (x_t - μ_hour - δ)) , δ = μ × 0.3
|
||
g_t > μ × 2.0 → trigger → reset g=0
|
||
```
|
||
- μ는 시간대별 롤링 mean. static threshold를 못 넘게 살살 올리는 low-rate DDoS 커버
|
||
- 전역 g_t 1개 유지 (hour 경계 reset 문제 회피)
|
||
|
||
**A3. Global src IP entropy drop** (소수 IP 집중 공격)
|
||
```
|
||
현재 5분 트래픽의 Shannon(Counter(client_ip)) < baseline_entropy_p10(시간대별)
|
||
```
|
||
- entropy 계산은 `min_entropy_baseline_samples=20` 확보 후부터
|
||
- `uniq_ips < entropy_min_uniq_ips=30` 이면 판정 스킵 (트래픽 자체가 너무 적을 때)
|
||
|
||
**A4. Matrix Profile discord** (패턴 이상)
|
||
```
|
||
stumpy.stump(최근 7일 total_reqs 시계열, m=12)
|
||
현재 서브시퀀스의 MP > 과거 MP 분포의 p99 → discord
|
||
```
|
||
- m=12 (1시간 subsequence length)
|
||
- 완전 비파라메트릭. 학습 불필요, 자기 과거와 직접 비교
|
||
- z-normalized euclidean distance라 스케일 차이 무시, **형태 차이**만 봄
|
||
- CUSUM은 "크기 이상"만, MP는 "패턴/형태 이상" 담당 → 상보적
|
||
- 첫 사이클은 numba JIT로 ~7s, 이후 ms 단위
|
||
|
||
→ attack mode ON: top N contributor IP 일괄 challenge
|
||
|
||
**B. 개별 극단 (항상 동작)**
|
||
```
|
||
Scanner-shape: uniq_paths/reqs ≥ 0.8 AND path_entropy ≥ 7 AND reqs ≥ 300
|
||
(수백 경로를 각 1회씩 = 취약점 스캐너)
|
||
Raw extreme: reqs ≥ 1500 / 5min
|
||
(폴링 유저 상한의 약 7배)
|
||
```
|
||
|
||
### 롤링 baseline (자동 갱신)
|
||
|
||
- 매 사이클 현재 윈도우 샘플을 `baseline_samples`에 누적 (total_reqs + src_ip_entropy + uniq_ips)
|
||
- 시간대(hour_utc)별 mean/p95/max/entropy_p10/entropy_p50을 최근 7일 롤링 기준으로 실시간 계산
|
||
- **공격 판정된 윈도우 샘플은 baseline에 안 들어감** (baseline 오염 방지)
|
||
- 매 60 사이클마다 7일 초과 샘플 자동 prune
|
||
- 초기 seed는 `baseline_aggregate.py`로 1회 수집, 롤링 12+ 샘플 쌓이면 seed 대체
|
||
|
||
## 오탐 방지 특성
|
||
|
||
| 상황 | 동작 |
|
||
|------|------|
|
||
| 평상시 정상 트래픽 | 액션 0 (heavy user·폴링 유저 다 통과) |
|
||
| 평상시 섞인 취약점 스캐너 | 개별 극단 트리거 (확정 공격) |
|
||
| DDoS·대규모 공격 (급격) | static threshold (p95×2.0 / max×1.5) 발동 |
|
||
| Low-rate / 점진 ramp-up DDoS | CUSUM Page-Hinkley 누적으로 발동 |
|
||
| 소수 IP 집중 공격 (같은 볼륨) | global src IP entropy drop으로 발동 |
|
||
| 지속되는 비정형 shape (평소 한 번도 안 본 패턴) | Matrix Profile discord로 발동 |
|
||
| 가짜 공격 (baseline 경계선) | samples ≥ 10 확보 후에만 판정 (초기 1~2일은 판정 유예) |
|
||
|
||
**사람이 쓴 공격 지식 없음**:
|
||
- 특정 경로·ASN·UA 리스트 없음
|
||
- scanner-shape는 행동 통계 (uniq_paths/reqs 비율, entropy)
|
||
- attack mode는 트래픽 볼륨 이상 — 자기 과거와의 편차만 봄
|
||
|
||
## 조치 레벨
|
||
|
||
- **challenge** (Cloudflare `managed_challenge`, CAPTCHA, 내부 TTL 30분): 정상 유저는 한 번 풀고 통과
|
||
- **block** (CF `block`, 내부 TTL 24시간): challenge 통과 후에도 같은 IP가 5+ 사이클 연속 공격 시
|
||
- 내부 TTL이 지나면 loop이 CF rule을 DELETE (CF Access Rules 자체 TTL 없음, sigmatch가 관리)
|
||
|
||
## CF 연동 (Phase 11, v2.3)
|
||
|
||
### 엔드포인트
|
||
- Account-level: `POST /accounts/{id}/firewall/access_rules/rules` (6개 zone 동시 적용)
|
||
- 인증: `X-Auth-Email` + `X-Auth-Key` (global_api_key). API token은 Rulesets/Firewall 403
|
||
- Vault: `secret/cloud/cloudflare-netbis` (`account_id`, `email`, `global_api_key`)
|
||
|
||
### 중복 호출 최소화
|
||
상태 변화 있을 때만 CF API 호출:
|
||
- `없음 → challenge/block`: `create_rule` → `rule_id` 저장
|
||
- `challenge → block`: `update_rule (PATCH mode)` (같은 rule_id 유지)
|
||
- `같은 action 지속`: **skip-same** (CF 호출 없음, sqlite state만 갱신하여 TTL 연장)
|
||
- `expire_actions`: 내부 TTL 만료 시 `delete_rule` + state clear
|
||
|
||
### Rate Limit 대응
|
||
- `min_interval_sec=0.3` (sleep), 429 시 exponential backoff
|
||
- CF API 계정 제한 1200 req/5min — 지속 공격 IP 10개 × 사이클당 skip-same 로직이라 여유
|
||
|
||
### Rule 태깅
|
||
notes에 `netbis-sigmatch` prefix. startup 시 `list_sigmatch_rules()`로 우리가 만든 rule만 필터 캐시.
|
||
|
||
### CLI 유틸 (`cf_client.py`)
|
||
```bash
|
||
# env: CF_ACCOUNT_ID, CF_EMAIL, CF_GLOBAL_API_KEY
|
||
uv run python cf_client.py list # 현재 sigmatch rules 확인
|
||
uv run python cf_client.py create --ip X --mode managed_challenge --note "..."
|
||
uv run python cf_client.py delete --rule-id XXX
|
||
uv run python cf_client.py purge [--yes] # sigmatch prefix 전체 삭제
|
||
```
|
||
|
||
## 개발 단계
|
||
|
||
- [x] Phase 1: feature 추출 (`fetch_features.py`)
|
||
- [x] Phase 2: 24h retrospective baseline (`collect_baseline.py`)
|
||
- [x] Phase 3-5: (폐기) IsolationForest+DBSCAN+persistence 기반
|
||
- [x] Phase 6 (v2): 집계 기반 공격 모드 + 개별 극단 시그니처
|
||
- [x] Phase 7: 롤링 baseline 자동 갱신
|
||
- [x] Phase 8 (v2.1): CUSUM Page-Hinkley + global src IP entropy drop
|
||
- [x] Phase 9 (v2.2): Matrix Profile discord (stumpy.stump) 추가
|
||
- [x] Phase 10: bootstrap_baseline.py — 과거 24h seed로 모든 hour samples_ok 즉시 충족
|
||
- [x] **Phase 11 (v2.3): CF Access Rules 호출 (managed_challenge/block)** ← 현재 DRY로 검증
|
||
- [ ] Phase 11 LIVE 전환: `--live` 플래그로 실 운영 (환경변수 CF credentials 주입)
|
||
- [ ] Phase 12: jp1 Incus 배포 (systemd timer)
|
||
|
||
## 파라미터 (사람 조정 가능)
|
||
|
||
| 파라미터 | 기본값 | 의미 |
|
||
|---------|-------|------|
|
||
| `attack_p95_multiplier` | 2.0 | 현재 req가 시간대 p95의 몇 배면 attack mode |
|
||
| `attack_max_multiplier` | 1.5 | 또는 max의 몇 배면 |
|
||
| `attack_top_n` | 20 | attack 시 challenge할 상위 IP 수 |
|
||
| `attack_contributor_min_reqs` | 200 | top IP 중 이 이상인 것만 |
|
||
| `cusum_drift_pct` | 0.3 | CUSUM 허용 드리프트 δ = μ × pct |
|
||
| `cusum_threshold_mult` | 2.0 | CUSUM 트리거 h = μ × mult |
|
||
| `min_entropy_baseline_samples` | 20 | entropy baseline 샘플 부족 시 판정 스킵 |
|
||
| `entropy_min_uniq_ips` | 30 | uniq IP 부족 시 entropy 판정 스킵 |
|
||
| `mp_subseq_len` | 12 | Matrix Profile subsequence length (5분×12=1h) |
|
||
| `mp_threshold_pct` | 0.99 | 과거 MP 분포의 이 percentile 초과 시 trigger |
|
||
| `scanner_uniq_ratio` | 0.8 | uniq_paths/reqs 임계 |
|
||
| `scanner_min_entropy` | 7.0 | path entropy 임계 (per-IP) |
|
||
| `scanner_min_reqs` | 300 | 스캐너 최소 요청 수 |
|
||
| `extreme_reqs` | 1500 | 단일 IP 극단 rate 임계 |
|
||
| `persistence_for_block` | 5 | challenge → block 승급 사이클 |
|
||
| `challenge_ttl_sec` | 1800 | 30분 |
|
||
| `block_ttl_sec` | 86400 | 24시간 |
|
||
| `baseline_rolling_days` | 7 | 롤링 윈도우 |
|
||
| `min_baseline_samples` | 10 | baseline 샘플 부족 시 판정 유예 |
|
||
|
||
## 검증 결과 (simulate.py)
|
||
|
||
- 평상시 24h 데이터: static threshold 0회 발동, **CUSUM hour별 μ 적용 시 0회 오탐**
|
||
- 마지막 5개 윈도우에 1.5x→3.5x 점진 ramp 주입: CUSUM 3회 발동 (idx 198/199/200 연속)
|
||
- CUSUM 유닛 테스트: 평상시 g=0 유지, ramp-up 시 g 누적 → h 초과 시 trigger + reset 확인
|
||
- **Matrix Profile**: 평상시 201 윈도우에서 p99 초과 ~1%. 단기 3-point spike는 MP 안 튀고 (CUSUM 담당), **1시간 sustained max×3 형상은 MP=3.46 > p99=3.19로 trigger** 확인. 역할 상보적
|
||
- 배포 후 cycle 101에서 `mp=1.87/thr=2.88` 안정 관찰 (현재 패턴이 과거와 유사 → trigger X)
|
||
|
||
## 폐기된 이전 설계
|
||
|
||
- ~~IsolationForest + DBSCAN per-IP anomaly~~ → 정상 폴링 유저를 outlier로 잡아 오탐 위험
|
||
- ~~Persistence 단독 트리거~~ → 페이지 오래 열어둔 유저 6 사이클 지속 시 오탐
|
||
- ~~사전 정의 hard rule (R1~R6)~~ → 공격 패턴 종속, 자동 시그니처 생성 취지 어긋남
|
||
|
||
## 폐기 후보 (검토 후 불채택)
|
||
|
||
- **Prophet / LSTM / Bi-LSTM** — 라벨 없음, 학습·추론 무거움, jp1 작은 컨테이너 오버킬
|
||
- **CatBoost / Random Forest 지도학습** — 라벨 필요
|
||
- **per-IP Isolation Forest / OC-SVM** — 정상 폴링 유저 오탐 (이미 폐기 이유 동일)
|
||
|
||
향후 데이터 2주+ 쌓이면 검토 예정:
|
||
- **Holt-Winters** — level+trend+seasonality. 현재 static p95×mult 대체 가능
|
||
- **River HalfSpaceTrees** — streaming Isolation Forest, 다변량 윈도우 feature 학습-예측 동시
|
||
- **ADWIN (concept drift)** — baseline 자동 window 조정 (서비스 성장 대응)
|
||
|
||
## 파일 구조
|
||
|
||
```
|
||
~/netbis-sigmatch/
|
||
├── fetch_features.py — feature 추출 (단발 조회)
|
||
├── collect_baseline.py — retrospective seed baseline 수집
|
||
├── baseline_aggregate.py — 시간대별 seed 통계 수집 (1회성)
|
||
├── inspect_baseline.py — baseline DB 탐색
|
||
├── state.py — state DB (ip_state + cf_rule_id, baseline_samples, cusum_state 등)
|
||
├── matrix_profile.py — stumpy MP discord 판정 (v2.2)
|
||
├── cf_client.py — CF Access Rules API 래퍼 + CLI 유틸 (v2.3)
|
||
├── bootstrap_baseline.py — 과거 N시간 seed 주입 (cold start 해소)
|
||
├── loop.py — 메인 실시간 루프 (--live로 CF 호출)
|
||
├── simulate.py — 과거 데이터로 로직 검증
|
||
├── baseline.db — seed snapshot 24h
|
||
├── state.db — 운영 상태 + 롤링 baseline
|
||
└── logs/ — 사이클 로그
|
||
```
|
||
|
||
## 연관 정본
|
||
|
||
- [[../services/netbis]] — NPM → VL 파이프라인
|
||
- [[../infra/security/cloudflare#Pseudo IPv4 (Class E 240/4)]] — 240/4 대역 해석
|
||
- [[../history/2026-04-24-cf-pseudo-ipv4-discovery]] — CF Pseudo IPv4 규명
|