--- title: CrowdSec 및 SafeLine WAF updated: 2026-04-04 --- ## CrowdSec LAPI | 항목 | 값 | |------|-----| | 위치 | jp1 Incus `crowdsec` 컨테이너 | | LAPI | `http://10.253.100.240:8080` | | 관리 | `ssh incus-jp1 "incus exec crowdsec -- cscli ..."` | ## 로그 수집 (Acquisition) ### Traefik → CrowdSec (Vector) ``` Traefik DaemonSet (stdout JSON accessLog) → Vector Agent DaemonSet (K3s logging ns, kubernetes_logs source) → VRL transform (access log만 필터, non-JSON abort) → HTTP sink (배치 50건, 5초) → CrowdSec HTTP acquisition (:8086/traefik-logs) → crowdsecurity/traefik-logs 파서 ``` | 항목 | 값 | |------|-----| | Vector | Helm `vector/vector` 0.51.0, Agent DaemonSet (3노드) | | Values | `~/k8s/vector/values.yaml` | | CrowdSec 포트 | 8086 | | 인증 | `Authorization: traefik-crowdsec-log-2024` | | 파서 | `crowdsecurity/traefik-logs` (Hub, JSON 모드) | ### APISIX → CrowdSec (http-logger) ``` APISIX K3s 서울 (apisix ns, global_rules http-logger 플러그인) → 배치 50건, 5초 buffer → CrowdSec HTTP acquisition (:8085/apisix-logs) → custom/apisix-json-logs 파서 ``` | 항목 | 값 | |------|-----| | 송신처 | **K3s 서울 APISIX 단독** (osaka는 송신 안 함, zlambda는 라우트 비어있음) | | 설정 | K3s APISIX Admin API `global_rules/http-logger` (`uri: http://10.253.100.240:8085/apisix-logs`) | | CrowdSec 포트 | 8085 | | 인증 | `Authorization: apisix-crowdsec-log-2024` | | 파서 | `custom/apisix-json-logs` (로컬) | | osaka의 처리 | 송신은 안 하고 `crowdsec-bouncer` 글로벌 플러그인으로 결정 **소비**만. `real-ip` 플러그인도 글로벌. | ### APISIX → log-collector → CrowdSec (sandbox-tokyo) ``` sandbox-tokyo APISIX http-logger → log-collector (jp1 crowdsec 컨테이너, :8087) → SQLite (/var/lib/log-collector/requests.db) → CrowdSec HTTP acquisition (:8085/apisix-logs) 포워딩 ``` | 항목 | 값 | |------|-----| | log-collector | Go HTTP 서버, `/usr/local/bin/log-collector` (jp1 crowdsec 컨테이너) | | 소스 | jp1 `~/log-collector/main.go` | | 수신 포트 | 8087 | | 인증 | `Authorization: apisix-crowdsec-log-2024` | | SQLite | `/var/lib/log-collector/requests.db` | | 기능 | APISIX 로그 수신 → SQLite 저장 → CrowdSec 포워딩, 타임스탬프 밀리초 보정 | ### SafeLine → CrowdSec (실시간, PG LISTEN/NOTIFY) ``` SafeLine WAF 차단 → mgt_detect_log_basic INSERT → PG 트리거 (notify_detect_log) → pg_notify('safeline_detect', JSON) → safeline-listener (kr2, Go) → detail 테이블 enrichment (x-real-ip, user-agent) → CrowdSec HTTP acquisition (:8088/safeline-logs) → custom/safeline-http-logs 파서 → custom/safeline-waf-blocked 시나리오 (trigger, 즉시 밴) ``` | 항목 | 값 | |------|-----| | safeline-listener | Go, `/usr/local/bin/safeline-listener` (kr2) | | 소스 | kr2 `~/safeline-listener/main.go` | | systemd | `safeline-listener.service` (kr2) | | PG DSN | SafeLine CE DB (10.43.8.243:5432, safeline-ce) | | CrowdSec 포트 | 8088 | | 인증 | `Authorization: safeline-crowdsec-2026` | | acquis 설정 | `/etc/crowdsec/acquis.d/safeline-http.yaml` | | 파서 | `custom/safeline-http-logs` (`/etc/crowdsec/parsers/s01-parse/safeline-http-logs.yaml`) | | 시나리오 | `custom/safeline-waf-blocked` (trigger 타입, 즉시 밴) | | enrichment | req_header에서 `x-real-ip`, `user-agent` 추출, detail 테이블에서 method/payload 조회 | PG 트리거 (SafeLine CE DB에 설치): ```sql CREATE OR REPLACE FUNCTION notify_detect_log() RETURNS trigger AS $$ BEGIN PERFORM pg_notify('safeline_detect', json_build_object( 'id', NEW.id, 'ts', NEW.created_at, 'src_ip', NEW.src_ip, 'host', NEW.host, 'url_path', NEW.url_path, 'attack_type', NEW.attack_type )::text); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_detect_log AFTER INSERT ON mgt_detect_log_basic FOR EACH ROW EXECUTE FUNCTION notify_detect_log(); ``` ### ~~ddos-detect (AI 행위 분석)~~ — 폐기 (2026-04-08) > [!warning] 폐기됨 > 2026-04-08 분석기 서비스 정지 + 삭제. "이 방식으로는 안 될 것 같다"는 판단 (60초 폴링 + Claude CLI 동기 호출 구조의 한계). 새로운 AI 분석 아키텍처는 [[victorialogs|VictoriaLogs]] 기반으로 재설계 예정. > > **제거된 항목**: `ddos-detect.service` (systemd), `/var/lib/log-collector/ddos-detect/` Go 바이너리 + 소스, `ddos-detect.sh`, `extract_behavior.py`, `ddos-logs/` (분석 결과 markdown). `requests.db`는 별개의 `log-collector` 데몬이 사용 중이라 보존. Gitea repo `kaffa/ddos-detect`는 보존 (코드 reference). 이전 동작 (참고): | 항목 | 값 | |------|-----| | 위치 | jp1 crowdsec 컨테이너 `/var/lib/log-collector/ddos-detect/` | | Gitea | `gitea.inouter.com/kaffa/ddos-detect` | | 기능 | SQLite 폴링 → IP별 행위 패턴 추출 → Claude AI 분석 → cscli 자동 밴 | | 설정 | `config.yaml` (poll 60s, Claude sonnet, ban 4h, max_workers 3) | ### 리얼 IP 처리 | 경로 | 설정 | source IP 추출 | |------|------|---------------| | BunnyCDN → HAProxy → **Traefik** | `forwardedHeaders.trustedIPs: 127/8, 10/8, 172.16/12, 192.168/16` + `externalTrafficPolicy: Local` | `ClientHost` (X-Forwarded-For에서 추출) | | BunnyCDN → HAProxy → **APISIX** | `real_ip_header: X-Forwarded-For` + `real_ip_from: 127/8, 10.42/16, 10.43/16, 192.168.9/24, 100.64/10` | `client_ip` (nginx real_ip 모듈) | Traefik values: `~/k8s/traefik/values.yaml` ## 시나리오 | 유형 | 시나리오 | 출처 | |------|---------|------| | HTTP | http-probing, http-crawl-non_statics, http-sensitive-files, http-backdoors-attempts, http-sqli-probing, http-xss-probing 등 | Hub | | SafeLine | custom/safeline-waf-blocked (trigger), custom/safeline-waf-repeated (3+/5min) | 로컬 | | **APISIX** (2026-04-08 신규) | custom/apisix-high-rate-per-ip, custom/apisix-499-burst, custom/apisix-single-path-flood, custom/apisix-5xx-burst | 로컬 | | CAPI | http:dos, http:scan, ssh:bruteforce (커뮤니티) | 자동 | ### APISIX 시나리오 (4종, 2026-04-08 작성) ddos-detect AI 분석기 폐기 후 deterministic 패턴 매칭으로 대체. 모두 leaky bucket, ban 4h. **K3s 서울 APISIX 트래픽만 보고 판단** (osaka/zlambda는 http-logger 송신 안 함). | 시나리오 | 매개변수 | 의도 | filter | |---|---|---|---| | `apisix-high-rate-per-ip` | capacity 1000, leakspeed 100ms (≈ sustained 10 req/s, burst 1000) | 일반 HTTP flood. APISIX 자체 limit-req(20 req/s)에 안 걸리는 sustained 패턴 | `log_type=http_access-log`, groupby `source_ip` | | `apisix-499-burst` | capacity 30, leakspeed 2s | connection exhaustion / Slowloris (클라이언트 강제 끊김) | `http_status == "499"` | | `apisix-single-path-flood` | capacity 50, leakspeed 600ms | 동일 path 반복 (CAPTCHA 우회, login bf, 동일 자원 폭격) | groupby `source_ip + ":" + http_path` | | `apisix-5xx-burst` | capacity 20, leakspeed 3s | 백엔드 부하 유발성 공격, 취약점 스캔 | `http_status startsWith "5"` | ⚠️ **운영 주의**: - `5xx-burst`: 백엔드 장애 시 false positive 가능. 운영 알람과 분리해서 검토. - `499-burst`: 모바일 클라이언트 끊김으로 정상 발생할 수 있어 30 임계값을 며칠 관찰 후 조정 권장. - `high-rate-per-ip`: APISIX `limit-req` 글로벌 룰(20 req/s burst 10)을 통과한 트래픽만 도달 → 1차 통과 후 sustained pattern을 잡는 2차 layer. - 현재 모두 `remediation: true` (즉시 ban). dry-run으로 시작 안 함 — false positive 발생 시 임계값 또는 ban duration 조정. - **2026-04-09 조정**: `high-rate-per-ip` 초기값(capacity 200, leakspeed 300ms) 사용 중 kappa 본인 트래픽이 지속 ≈3.3 req/s 한도를 넘어 오탐 ban 발생 → BunnyCDN 엣지 미들웨어가 캡챠 페이지 반환 → **POST /__captcha/verify 가 BunnyCDN 미들웨어에서 미처리 상태로 origin까지 흘러 404 반환하는 dead-end 버그**와 결합되어 outline/vault/n8n/jarvis/telegram-webhook/actions 전 호스트 접근 불가. 조치: (1) cscli로 ban decision 삭제 + BunnyCDN zone 캐시 퍼지, (2) 시나리오 완화 capacity 200→1000, leakspeed 300ms→100ms (sustained 10 req/s 허용), (3) **BunnyCDN 엣지 스크립트 64811 middleware.ts 수정** — `/__captcha/verify` 라우트를 `isBlocked` 블록 밖으로 끌어올려 bloom filter 업데이트 race와 무관하게 항상 인터셉트하도록 변경. POST는 handleCaptchaVerify, 그 외(method/ip 없음)는 302 to `/`. 소스: [gitea 7da39b2ac](https://gitea.inouter.com/kaffa/crowdsec-bunny-bouncer/commit/7da39b2acb91f5dd65e63b0ec078d37259bb2950). - **2026-04-09 별건**: `cs-cf-worker-bouncer.yaml` line 68-78 (anvil.it.com 블록) 들여쓰기 12칸 오타로 5일간 42,593회 systemd crash loop, Cloudflare Worker KV 동기화 중단. sed로 인덴트 4칸으로 정정, 5일 만에 정상 기동. 별건: K8s `default/cfb-ssh-key` 시크릿의 id_rsa 데이터가 libcrypto parse 실패 상태라 `cfb-manager` → `cs-cf-worker-bouncer` SSH 인증 불가, `/domains` `/status` API가 500 반환. 새 ed25519 키페어 생성·시크릿 교체·authorized_keys 등록·파드 재시작으로 복구. - **2026-04-09 Turnstile sitekey 교체**: BunnyCDN 엣지 스크립트 64811의 `TURNSTILE_SITE_KEY` env 변수가 `0x4AAAAAACbmaudAjITah7y7` (name `inouter`, allowed domain `['anvil.it.com']`)로 설정되어 있어 outline.inouter.com에서 Turnstile 위젯이 "웹 사이트에 연결할 수 없음" 에러. cs-cf-worker-bouncer가 managed 모드로 자동 생성한 `inouter.com` zone 위젯(`0x4AAAAAAC2cntUlRC3KKMKG`, secret `0x4AAAAAAC2cnp9fkaIBt3rixDBalNKfLZQ`)으로 교체. **Turnstile managed 위젯은 zone apex 도메인으로 등록해도 서브도메인을 암묵 허용함**이 확인됨 (outline.inouter.com 정상 동작). 단, iron-kr 풀존에는 `actions.it.com`·`iron-kr.b-cdn.net`도 포함되는데 이들은 inouter.com zone 외부라 캡챠 위젯 로드 불가. iron-jp 풀존(anvil.it.com 등)도 동일 제약. **TODO**: 스크립트를 수정해 `request.hostname`에 따라 zone별 sitekey/secret을 dispatch하거나, CF 대시보드에서 multi-domain 위젯 하나로 통합. ### 발견 사항: K3s APISIX 글로벌 limit-req ```json { "key_type": "var", "key": "remote_addr", "rate": 20, "burst": 10, "rejected_code": 429 } ``` `/apisix/global_rules/limit-req` (etcd). 모든 라우트에 적용. CrowdSec 시나리오는 이 1차 차단을 통과한 트래픽만 본다는 점을 고려해서 임계값 설계. ## Bouncer ### cs-cf-worker-bouncer (Cloudflare Worker) | 항목 | 값 | |------|-----| | 위치 | jp1 Incus `cs-cf-worker-bouncer` 컨테이너 | | 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` | | 정본 | `gitea.inouter.com/kaffa/k8s` → `configs/crowdsec/crowdsec-cloudflare-worker-bouncer.yaml` | | 동기화 | 10초 (decision stream polling) | | 방식 | LAPI → bouncer → Cloudflare Worker KV (bloom filter) → Worker에서 차단/captcha | | 보호 zone (현재) | keepanker.cv, actions.it.com, ironclad.it.com, servidor.it.com (4 zone, 2026-04-10 cf-audit-cleanup-2 후) | | 제거된 zone | inouter.com, anvil.it.com — DNS `proxied: false` 라 CF 엣지를 거치지 않아 enforce 안 되던 dead route. 보호는 BunnyCDN 미들웨어 64811 단독. 2026-04-10 cfb-manager DELETE + 002323 단계 destructive cleanup → 003943 수동 YAML 수리 | | Turnstile | bouncer-managed 4개 (rotate 168h), + 별도 수동 위젯 `inouter-bunny-middleware` (`0x4AAAAAAC3otPWhldI96Aks`) — BunnyCDN 미들웨어 64811 전용, bouncer 관리 외 freeze. [[cloudflare#Turnstile 토큰 권한 (2026-04-10 사실)]] | #### Decision 수동 관리 bouncer의 `lapi_key`는 **읽기 전용**. decision 추가/삭제는 `crowdsec` 컨테이너에서 `cscli`로: ```bash # ban 추가 ssh incus-jp1 "incus exec crowdsec -- cscli decisions add --ip 1.2.3.4 --duration 2m --reason 'manual ban' --type ban" # 확인 ssh incus-jp1 "incus exec crowdsec -- cscli decisions list --ip 1.2.3.4" # 삭제 ssh incus-jp1 "incus exec crowdsec -- cscli decisions delete --ip 1.2.3.4" ``` bouncer 컨테이너에서 `curl POST /v1/decisions`로 직접 넣으면 등록 안 됨 — API 키 권한 제한. #### 설정 변경 시 주의 **`sed -i` 사용 금지** — Incus 컨테이너 내에서 `sed -i`로 수정 시 이전 내용이 파일 끝에 남아 YAML 깨짐. 반드시 전체 파일 덮어쓰기: ```bash cat updated-config.yaml | ssh incus-jp1 "incus file push - cs-cf-worker-bouncer/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml" ssh incus-jp1 "incus exec cs-cf-worker-bouncer -- systemctl restart crowdsec-cloudflare-worker-bouncer" ``` ### netbis-cf (Cloudflare Worker — Netbis 계정) Kappa 계정용 `cs-cf-worker-bouncer`와 별도 컨테이너로 분리 운영. 상세: [[netbis]] | 항목 | 값 | |------|-----| | 위치 | jp1 Incus `netbis-cf-bouncer` 컨테이너 (10.253.103.33) | | 바운서 이름 | netbis-cf | | 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` | | 동기화 | 10초 | | CF 계정 | Netbis (netbis@netbis.io, Account ID 8fcf3c7876332aba33e974cbbfdad951) | | 보호 zone | fall-vip.com, fall-mvp.com, fall-vip7.com, psd777.com, rss-555.com, rss-7790.com | | Turnstile | 6개 zone managed 모드, 168시간 secret key 로테이션 | | 로그 소스 | sandbox-tokyo APISIX → CrowdSec http-logger (8085/apisix-logs) | ### bunny-cdn-bouncer (BunnyCDN Edge Script) | 항목 | 값 | |------|-----| | 동기화 | jp1 `infra-tool` 컨테이너, `/opt/crowdsec-bouncer/bouncer.py` | | 크론 | 3분 delta sync, 매시 full sync | | 방식 | LAPI → Bloom filter → BunnyCDN Edge Script 임베딩 → 퍼블리시 | | Edge Script | ID 64811 (`crowdsec-bouncer-middleware`), FNV-1a bloom filter | | 차단 시 | Cloudflare Turnstile CAPTCHA + LibSQL verified IP 캐싱(4h) | | 적용 풀존 | iron-jp (5555247), iron-kr (5555227) — 두 풀존 모두 `MiddlewareScriptId: 64811` (2026-04-09 실측) | | API 키 | Vault `secret/infra/crowdsec-bunny-bouncer` | | 소스 | `~/crowdsec-bunny-bouncer/` | ## 보안 구조 ``` 클라이언트 → BunnyCDN Edge Script (CrowdSec bouncer, 0차) → BunnyCDN WAF (OWASP CRS, 1차) → Traefik / APISIX + SafeLine WAF (2차) → CrowdSec 로그 분석 (3차) → bouncer 피드백 루프 ``` ### 0차: BunnyCDN Edge Script (CrowdSec bouncer) - Bloom filter로 악성 IP 즉시 차단 - false positive 시 Turnstile CAPTCHA로 구제 ### 1차: BunnyCDN WAF (OWASP CRS) - CDN 에지에서 오리진 도달 전 차단 - 차단: SQLi, XSS, CMDi, SSRF, Shellshock, Log4j - 비활성화: DATA LEAKAGES SQL (id=911) — NocoDB 오탐 방지 - 통과: Request Smuggling, NoSQLi, 경로 스캔 ### 2차: SafeLine WAF (chaitin-waf) - APISIX plugin_metadata → detector `10.43.253.244:8000` - SafeLine v9.3.2, K3s safeline ns - 관리: safeline.inouter.com, API 헤더 `X-SLCE-API-TOKEN` - 토큰: Vault `secret/infra/safeline` - tengine 미사용, APISIX 직접 연동 ### 3차: CrowdSec 로그 분석 - Traefik 로그: Vector DaemonSet → `:8086` → `crowdsecurity/traefik-logs` - APISIX 로그: http-logger → `:8085` → `custom/apisix-json-logs` - HTTP 시나리오 매칭 → decision → bouncer 피드백 ## iron-kr-waf BunnyCDN Pull Zone (구 waf-kr) APISIX 외부 인입 경로 (juiceshop SafeLine 통합 테스트 용도). APISIX 자체는 독립 LoadBalancer gateway (MetalLB 192.168.9.50) 이지 "SafeLine 전용" 이 아님 — 현재 1 route 가 SafeLine 용일 뿐. | 항목 | 값 | |------|-----| | Zone ID | 5555224 | | Name | iron-kr-waf | | Origin | https://220.120.65.245:9443 | | 경로 | 인터넷 → BunnyCDN(iron-kr-waf) → OpenWrt HAProxy(:9443) → MetalLB 192.168.9.50:443 → APISIX(K3s 서울) → chaitin-waf → SafeLine detector → K3s svc | 등록 호스트: `iron-kr-waf.b-cdn.net`, `juiceshop.keepanker.cv` (WAF 테스트용). ApisixRoute `juiceshop` (chaitin-waf block) → juiceshop:3000. > 옛 메모의 `waf-kr (5554681)` 는 더 이상 존재하지 않음 — `iron-kr-waf (5555224)` 로 통합·재명명됨. ## 참고 - BunnyCDN WAF 차단 시 오리진에 로그 안 옴 → CrowdSec에 미수신 - OpenWrt CrowdSec firewall bouncer는 DNAT 구조라 리얼 IP 매칭 불가 - chaitin-waf 플러그인은 `plugin_attr`이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음 - Incus 컨테이너에서 `sed -i`로 설정 수정 시 파일 손상 주의 (전체 파일 push 사용)