anomaly-detect: 3차 agentic 재설계 (OpenRouter + Grok-4-fast)

- 전환 이유와 OpenRouter 모델 벤치마크 결과 기록
- 새 아키텍처 + 서버측 guardrail 설명
- 폐기된 ollama/cohort 섹션 strikethrough 처리
- 환경변수, Vault 위치 문서화

코드 커밋 해시는 병렬 작업 완료 후 별도 추가
This commit is contained in:
2026-04-09 00:05:58 +09:00
parent 6abdb41d0e
commit c1eb9b9375

View File

@@ -1,13 +1,92 @@
---
title: anomaly-detect (VictoriaLogs + ollama 기반 이상 트래픽 감지)
updated: 2026-04-08 2차 리뷰 반영
updated: 2026-04-08 agentic 재설계
tags: [security, crowdsec, victorialogs, ollama, gemma, anomaly]
---
> 코드: `gitea.inouter.com/kaffa/anomaly-detect` (private)
> 아키텍처: OpenRouter agentic (Grok-4-fast) + VictoriaLogs tool + CrowdSec LAPI tool
# anomaly-detect
## 3차 재설계 (2026-04-08, agentic)
기존 per-IP 게이트 + cohort 탐지 + gemma4:e4b classifier 구조를 **전면 폐기**하고, OpenRouter의 `x-ai/grok-4-fast`에 tool 2개만 노출하는 agentic 구조로 전환.
### 전환 이유
사용자 원래 의도는 "시계열 DB를 AI에 연결해주면 AI가 알아서 공격을 찾는다"였는데, 1~2차 구현은 "Python이 축(path/UA/IP)을 미리 정의하고 LLM은 yes/no만" 파이프라인이라 원래 의도와 어긋났다. 새 공격 벡터가 등장할 때마다 코드에 축을 추가해야 하는 한계가 본질적이었다.
### OpenRouter 모델 벤치마크 (2026-04-08)
24시간 실트래픽에 대해 4개 모델을 tool-use agent로 돌린 결과:
| 모델 | 턴 | 지연 | in/out 토큰 | 비용 | 결과 |
|------|-----|------|-------------|------|------|
| x-ai/grok-4-fast | **4** | 17.5s | 12303/2166 | $0.0036 | ✅ 정답 (`211.211.28.97`) |
| qwen/qwen3-235b-a22b-2507 | 7 | 20.5s | 16623/522 | **$0.0012** | ✅ 정답 |
| google/gemini-2.0-flash-001 | 10(max) | 16.8s | 22981/886 | $0.0026 | ❌ 결론 없음 |
| deepseek/deepseek-chat-v3.1 | 10(max) | 95.7s | 78180/1316 | $0.0127 | ❌ 결론 없음 |
- **Grok-4-fast**: agentic 품질 최상 — 에러 path → 에러 IP → pivot → UA/admin 교차 확인 → 결론의 정석 흐름. LogsQL 문법 에러 0회.
- **Qwen3-235b-2507**: 정답 도달했지만 Grok보다 턴 수 많음. 비용은 가장 쌈.
- **Gemini 2.0 Flash**: `http_user_agent=~` (Prometheus 문법) 같은 잘못된 LogsQL을 반복, 24h 지시도 무시, 사설망 제외도 약함.
- **DeepSeek v3.1**: 추론은 좋지만 `sort by`, `path:~"..."` 같은 LogsQL 구문에 실패를 반복, MAX_TURNS 소진.
선택: **`x-ai/grok-4-fast`** 주 모델, **`qwen/qwen3-235b-a22b-2507`** fallback.
### 새 아키텍처
```
systemd timer (5분)
analyzer.py (Python oneshot)
├─ OpenRouter → x-ai/grok-4-fast (tools=[logsql_query, ban_ips])
├─ agent loop (max 10턴)
│ ├─ tool: logsql_query(query, start, limit) — VictoriaLogs 자유 조회
│ │ ※ 응답에서 사설망/Tailscale/RFC5737 IP 자동 제거 (서버측 guardrail)
│ └─ tool: ban_ips(ips, reason, scenario) — CrowdSec LAPI batch POST
│ ※ dedup 24h, MAX_BAN_PER_CYCLE cap, 사설망 거부
├─ DRY_RUN=1 (기본): ban_ips가 "would ban" 로그만 찍고 실제 POST 안 함
└─ dedup.json 갱신
```
### 환경변수
| 변수 | 기본값 | 비고 |
|------|--------|------|
| `OPENROUTER_API_KEY` | (from `/etc/anomaly-detect/openrouter.env`) | Vault `secret/ai/openrouter` |
| `OR_MODEL` | `x-ai/grok-4-fast` | 주 모델 |
| `OR_FALLBACK_MODEL` | `qwen/qwen3-235b-a22b-2507` | OR 장애 시 재시도 |
| `WINDOW_MIN` | 5 | 조사 윈도우 |
| `MAX_TURNS` | 10 | agent loop 상한 |
| `MAX_BAN_PER_CYCLE` | 100 | 한 사이클 ban 상한 (과도 차단 방지) |
| `BAN_DURATION` | 4h | |
| `DRY_RUN` | **1** | ⚠ 초기 안전장치 |
### 서버측 guardrail (중요)
LLM은 지시만 받고 강제할 수 없다. 따라서 `logsql_query``ban_ips` 두 tool 모두 **파이썬 코드 레벨에서** 다음을 enforce:
- `is_skippable_ip()`: `ipaddress.is_private` + Tailscale 100.64/10 + RFC5737 TEST-NET-2/3 포함
- Python 3.9의 `ipaddress.ip_address('203.0.113.42').is_private == True` — 문서 IP도 자동 차단됨 (2026-04-08 벤치마크에서 확인)
LLM 프롬프트가 무시되어도 실수로 사설망이 ban되지 않음.
### Vault 위치
- OpenRouter key: `secret/ai/openrouter` (`API_KEY` 키)
- 컨테이너 배포본: `/etc/anomaly-detect/openrouter.env` (mode 600, systemd `EnvironmentFile=`)
### 폐기된 구조
- 1차 구현 (2026-04-08 초반): gemma4:e4b + stats 파이프라인
- 2차 구현 (같은 날): cohort 탐지(`_cohort_path_candidates`, `_cohort_ua_candidates`) 추가
- 두 구현 모두 로직 자체가 "AI가 아닌 Python이 탐지"였다는 점에서 본질적 한계. 전면 폐기.
### 코드 커밋 해시
코드 재작성이 병렬로 진행 중 — 이 문서는 코드 완료 전에 커밋됨. 코드 커밋 해시는 별도 커밋으로 추가 예정.
[[crowdsec-safeline#~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)|폐기된 ddos-detect]] 후속. [[victorialogs|VictoriaLogs]]에 적재된 K3s 서울 APISIX access log를 5분마다 분석하여 봇/공격성 IP를 [[crowdsec-safeline|CrowdSec]]에 자동 ban으로 등록한다.
## 위치 / 사양
@@ -18,7 +97,7 @@ tags: [security, crowdsec, victorialogs, ollama, gemma, anomaly]
| 컨테이너 | `anomaly-detect` (default 프로젝트, Debian 13 trixie) |
| IP | 10.100.2.164 |
| 사양 | 1 vCPU, 512MB RAM, 5GB |
| 설치 경로 | `/opt/anomaly-detect/{venv,analyzer.py}`, `/etc/anomaly-detect/lapi.yaml`, `/var/lib/anomaly-detect/dedup.json` |
| 설치 경로 | `/opt/anomaly-detect/{venv,analyzer.py}`, `/etc/anomaly-detect/lapi.yaml`, `/etc/anomaly-detect/openrouter.env`, `/var/lib/anomaly-detect/dedup.json` |
| systemd | `anomaly-detect.service` (oneshot) + `anomaly-detect.timer` (`OnCalendar=*:0/5`, `Persistent=true`, `RandomizedDelaySec=20`) |
## 데이터 흐름
@@ -51,7 +130,7 @@ password: <vault: secret/apps/anomaly-detect>
> [!warning] cscli machines add 함정
> `cscli machines add NAME --auto`는 default로 `/etc/crowdsec/local_api_credentials.yaml`을 덮어씀 — 이건 jp1 crowdsec **daemon 자체의 LAPI 클라이언트 설정**이라 덮어쓰면 daemon이 새 password로 LAPI 인증을 시도하면서 동기화가 깨짐. 반드시 `--file <별도 경로>` 옵션을 줘야 한다. 만약 실수로 덮어썼다면 `cscli machines add <default-machine-name> --auto --force --file /etc/crowdsec/local_api_credentials.yaml`로 default machine 새 credentials 발급 후 `systemctl reload crowdsec`로 복구.
## 통계 게이트 (환경변수로 조정)
## ~~통계 게이트 (환경변수로 조정)~~ (폐기, 3차 재설계)
| 변수 | default | 의미 |
|------|---------|------|
@@ -73,7 +152,7 @@ password: <vault: secret/apps/anomaly-detect>
사설망 IP(10/8, 127/8, 192.168/16, 172.16/12)는 자동 제외.
## ollama prompt
## ~~ollama prompt~~ (폐기, 3차 재설계)
`format=json`으로 강제 + 명확한 schema:
@@ -83,7 +162,7 @@ password: <vault: secret/apps/anomaly-detect>
판단 근거는 system prompt에 명시 (반복 패턴, 머신 속도, 4xx/5xx 비율, path 열거, 알려진 스캐너 UA, 로그인 brute force, 비정상 rate). 정상 브라우저 패턴은 "no"로 분류.
## CrowdSec 자동 ban
## ~~CrowdSec 자동 ban~~ (폐기, 3차 재설계 — `ban_ips` tool로 대체)
profile 기반:
@@ -121,6 +200,8 @@ incus exec anomaly-detect -- sh -c 'echo "{}" > /var/lib/anomaly-detect/dedup.js
- `curl http://10.253.100.240:8080/v1/decisions` → 403 (인증 필요, 네트워크 OK)
- 더미 IP `198.51.100.99`로 alert POST → 201 + decision 등록 → cleanup 확인 (smoke test)
> 아래는 1~2차 구현의 리뷰 이력이다. 3차 재설계에서 해당 코드는 전면 폐기됐다.
## 초기 리뷰 수정 (2026-04-08)
코드 리뷰 결과 다음 버그/개선을 반영:
@@ -152,16 +233,14 @@ incus exec anomaly-detect -- sh -c 'echo "{}" > /var/lib/anomaly-detect/dedup.js
## 향후 작업
- [ ] 처음 1주는 dry_run 없이 자동 ban이지만 임계값 조정 필요 시 보수적으로 시작 → 모니터링 후 점진 강화
- [ ] **[Medium]** Discord webhook 알림 추가 (`secret/apps/discord` Vault에서 가져오기) + systemd `OnFailure=` drop-in
- [ ] **[Medium]** ollama 장시간 장애 시 하드 게이트 fallback (예: 5분 1000+ reqs + 4xx>80% 자동 ban)
- [ ] **[Medium]** `LLM no 판정` IP의 dedup 짧게 (1h) 따로 관리해 false negative 회수
- [ ] **[Low]** CrowdSec alert `origin``"crowdsec"``"anomaly-detect"`로 태깅
- [ ] **[Low]** sample 10건을 "처음 만난 10건" → "최근 10건"으로 변경 (LogsQL `_time` desc 정렬) — 부분 해결 (H1으로 sample 풀어 전송)
- [ ] **[Low]** `scenario_hash` 고정 해시 지정
- [ ] gemma4:e4b 한국어 reason 품질 평가 → 모델 변경 검토 (`gemma3:12b`, `qwen2.5:7b`, `llama3.1:8b` 등)
- [ ] 게이트 통과 후 후보 0건이 며칠 지속되면 임계값 완화
- [x] gemma4:e4b 한국어 reason 품질 평가 → 모델 변경 검토 — **Grok-4-fast로 전환** (3차 재설계)
- [x] 코드를 Gitea repo로 분리 (`gitea.inouter.com/kaffa/anomaly-detect`) — 2026-04-08 완료
- [-] ~~ollama 장시간 장애 시 하드 게이트 fallback~~ — 폐기 (ollama 미사용)
- [-] ~~`LLM no 판정` IP의 dedup 짧게 (1h) 따로 관리~~ — 폐기 (cohort/classifier 구조 폐기)
- [-] ~~sample 10건을 "최근 10건"으로 변경~~ — 폐기 (sample 구조 자체 폐기)
- [-] ~~`scenario_hash` 고정 해시 지정~~ — 폐기 (scenario는 LLM이 ban_ips tool로 직접 지정)
## 폐기된 전임자