Files
obsidian/infra/anomaly-detect.md
kappa 2356b86d36 obsidian: 정본 문서에서 히스토리/인시던트 분리 완료
15개 정본 문서에서 날짜별 변경이력, 인시던트 기록, 폐기된 구현 상세를
history/ 디렉토리로 분리. 정본은 현재 상태만 기술하는 백서 형태로 정리.
각 정본에 history 위키링크 추가.

분리된 history 파일 12건:
- apisix git push 500, k3s postgresql migration, apisix→traefik 전환
- netbis DDoS 공격, gitea 이전/분리, usb 2.5g hang + NFS hard mount
- supabase→patroni, apisix etcd 통합/분리, anomaly-detect 재설계
- patroni failover incident, zlambda nixos migration, ops-agents setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:09:21 +09:00

172 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: anomaly-detect (VictoriaLogs + ollama 기반 이상 트래픽 감지)
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
## 아키텍처
OpenRouter `x-ai/grok-4-fast`에 tool 2개(logsql_query, ban_ips)만 노출하는 agentic 구조. fallback 모델 `qwen/qwen3-235b-a22b-2507`.
설계 반복 및 모델 벤치마크 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]
### 새 아키텍처
```
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` | 2000 | 한 사이클 ban 상한 (대규모 DDoS 대비) |
| `LAPI_BAN_CHUNK` | 500 | LAPI POST를 500건씩 쪼개 발송 (부분 실패 허용) |
| `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=`)
### 운영 중 주의사항
- **injected 로그 잔재**: E2E 테스트 중 주입한 270 rows가 vlogs retention 기간 동안 남음. `sim_e2e` 마커 필드로 식별 가능. 다음 cycle에서 다시 탐지될 수 있으나 dedup 24h으로 재ban 차단됨.
- **재테스트 시**: 테스트 후 반드시 `cscli decisions delete -s anomaly-detect/<scenario>` + `/var/lib/anomaly-detect/dedup.json` 업데이트 (해당 IP 추가하거나 리셋)
### 비용 모니터링
매 사이클 journalctl에 한 줄 요약 출력 (`48eb489` 이후):
```
cycle usage: turns=5 prompt=9142 completion=2670 total=11812 cost=$0.001866
```
집계 명령:
```bash
# 오늘 누적
ssh incus-hp2 "incus exec anomaly-detect -- bash -c '
journalctl -u anomaly-detect.service --since today --no-pager |
grep -oE \"cost=\\\$[0-9.]+\" |
awk -F\\\$ \"{sum+=\\\$2} END {printf \\\"today: \\\$%.6f (%d cycles)\\n\\\", sum, NR}\"
'"
# 사이클별 상세
ssh incus-hp2 "incus exec anomaly-detect -- journalctl -u anomaly-detect.service --since today --no-pager | grep 'cycle usage'"
```
**OpenRouter `/activity` API의 지연** (~2-3주)으로 실시간 비용 조회는 여기(journalctl) 경로를 써야 한다. dashboard는 https://openrouter.ai/activity 에서 UI 확인 가능 (더 빠름). management(provisioning) 키는 Vault `secret/ai/openrouter``PROVISIONING_KEY` 에 저장됨.
**월 비용 예상**: 평시 사이클당 ~$0.001-0.002, 5분 주기 × 288/일 × 30일 = **~$10-15/월**. DDoS 발생 시 사이클당 $0.01 수준 (massive_ddos 시나리오 기준)까지 튈 수 있으나 일시적.
[[crowdsec-safeline#~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08)|폐기된 ddos-detect]] 후속. [[victorialogs|VictoriaLogs]]에 적재된 K3s 서울 APISIX access log를 5분마다 분석하여 봇/공격성 IP를 [[crowdsec-safeline|CrowdSec]]에 자동 ban으로 등록한다.
## 위치 / 사양
| 항목 | 값 |
|------|-----|
| 호스트 | incus-hp2 |
| 컨테이너 | `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`, `/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`) |
## 데이터 흐름
```
[5분 주기 systemd timer]
analyzer.py
├─ 1) https://vl.inouter.com — LogsQL: program:apisix log_type:access 지난 5분
├─ 2) per-IP 통계 게이트 (count/4xx/5xx/499/distinct paths)
├─ 3) 후보 N개 (default max 5)
├─ 4) 각 후보 → http://100.87.221.126:11434/api/generate (kaffa-macmini ollama)
│ 모델: gemma4:e4b (Q4_K_M, 8.0B), format=json
├─ 5) verdict=yes → CrowdSec LAPI alert POST
│ http://10.253.100.240:8080/v1/alerts
│ profiles.yaml의 default_ip_remediation이 자동 ban 생성
└─ 6) dedup.json에 처리 IP + 타임스탬프 기록 (24h 내 재처리 안 함)
```
## CrowdSec LAPI 등록
`anomaly-detect`라는 watcher machine을 jp1 crowdsec에 등록하고, credentials를 컨테이너 안 `/etc/anomaly-detect/lapi.yaml`에 저장:
```yaml
url: http://10.253.100.240:8080
login: anomaly-detect
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`로 복구.
## 운영 명령
```bash
# 컨테이너 진입
ssh incus-hp2 'incus exec anomaly-detect -- bash'
# 수동 1회 실행
incus exec anomaly-detect -- /opt/anomaly-detect/venv/bin/python /opt/anomaly-detect/analyzer.py
# 상태
incus exec anomaly-detect -- systemctl status anomaly-detect.timer
incus exec anomaly-detect -- journalctl -u anomaly-detect.service --since "30 minutes ago"
# dedup 초기화 (모든 IP 재분석 허용)
incus exec anomaly-detect -- sh -c 'echo "{}" > /var/lib/anomaly-detect/dedup.json'
# 게이트/모델/주기 변경 → /etc/systemd/system/anomaly-detect.service의 Environment=
```
## 검증 (최초 배포)
- `cscli machines list``anomaly-detect` 등록 확인
- `curl https://vl.inouter.com/select/logsql/query?query=program:apisix\&limit=1` 200 OK (컨테이너 내부)
- `curl http://100.87.221.126:11434/api/tags``gemma4:e4b` 노출
- `curl http://10.253.100.240:8080/v1/decisions` → 403 (인증 필요, 네트워크 OK)
- 더미 IP `198.51.100.99`로 alert POST → 201 + decision 등록 → cleanup 확인 (smoke test)
## 향후 작업
- [ ] **[Medium]** Discord webhook 알림 추가 (`secret/apps/discord` Vault에서 가져오기) + systemd `OnFailure=` drop-in
- [ ] **[Low]** CrowdSec alert `origin``"crowdsec"``"anomaly-detect"`로 태깅
전임자 (`ddos-detect`, 1/2차 구현) 폐기 이력: [[../history/2026-04-08-anomaly-detect-iterations|history]]