From 92294c1435453d5f30f585e7a6de9a059b46ab3b Mon Sep 17 00:00:00 2001 From: kappa Date: Sat, 28 Mar 2026 13:21:33 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20CrowdSec=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A7=91=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=84=EB=A9=B4=20=EC=A0=95=EB=A6=AC=20(Vector,?= =?UTF-8?q?=20=EB=A6=AC=EC=96=BCIP,=20bouncer)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/crowdsec-safeline.md | 207 ++++++++++++++++++++----------------- 1 file changed, 112 insertions(+), 95 deletions(-) diff --git a/infra/crowdsec-safeline.md b/infra/crowdsec-safeline.md index 9cd6029..96c3aff 100644 --- a/infra/crowdsec-safeline.md +++ b/infra/crowdsec-safeline.md @@ -3,35 +3,88 @@ title: CrowdSec 및 SafeLine WAF updated: 2026-03-28 --- -## DB 테이블 +## CrowdSec LAPI -DB 테이블은 blocklist (ip PK, reason, origin, expires_at), verified_ips, metadata +| 항목 | 값 | +|------|-----| +| 위치 | 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 (global_rules http-logger 플러그인) + → 배치 50건, 5초 buffer + → CrowdSec HTTP acquisition (:8085/apisix-logs) + → custom/apisix-json-logs 파서 +``` + +| 항목 | 값 | +|------|-----| +| 설정 | APISIX Admin API global_rules/1 | +| CrowdSec 포트 | 8085 | +| 인증 | `Authorization: apisix-crowdsec-log-2024` | +| 파서 | `custom/apisix-json-logs` (로컬) | + +### 리얼 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` ## 시나리오 -시나리오: safeline/xml-injection, safeline/command-injection, custom/safeline-waf-blocked (trigger), custom/safeline-waf-repeated (leaky bucket 3+/5min) +| 유형 | 시나리오 | 출처 | +|------|---------|------| +| 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) | 로컬 | +| CAPI | http:dos, http:scan, ssh:bruteforce (커뮤니티) | 자동 | ## Bouncer -Bouncer 목록: [[apisix]]-waf-bouncer, bunny-cdn-bouncer, cs-[[cloudflare|cf]]-worker-bouncer +### cs-cf-worker-bouncer (Cloudflare Worker) -### cs-cf-worker-bouncer 상세 +| 항목 | 값 | +|------|-----| +| 위치 | jp1 Incus `cs-cf-worker-bouncer` 컨테이너 | +| 설정 | `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` | +| 정본 | `gitea.inouter.com/kaffa/k3s-config` → `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, inouter.com, servidor.it.com | +| Turnstile | 5개 zone managed 모드, 168시간 secret key 로테이션 | -- **위치**: jp1 Incus `cs-cf-worker-bouncer` 컨테이너 -- **설정**: `/etc/crowdsec/bouncers/crowdsec-cloudflare-worker-bouncer.yaml` -- **정본**: `gitea.inouter.com/kaffa/k3s-config` → `crowdsec/crowdsec-cloudflare-worker-bouncer.yaml` -- **LAPI**: `http://10.253.100.240:8080` (jp1 `crowdsec` 컨테이너) -- **동기화 주기**: 10초 (decision stream polling) -- **방식**: CrowdSec LAPI → bouncer → Cloudflare Worker KV (bloom filter) → Worker에서 IP 차단/captcha -- **보호 zone**: keepanker.cv, actions.it.com, ironclad.it.com, inouter.com, servidor.it.com -- **Turnstile**: 5개 zone 모두 managed 모드, 168시간마다 secret key 자동 로테이션 +#### Decision 수동 관리 -#### Decision 수동 추가/삭제 - -bouncer의 `lapi_key`는 **읽기 전용** (decision stream polling 전용). decision 추가/삭제는 반드시 `crowdsec` 컨테이너에서 `cscli`로 실행: +bouncer의 `lapi_key`는 **읽기 전용**. decision 추가/삭제는 `crowdsec` 컨테이너에서 `cscli`로: ```bash -# ban 추가 (crowdsec 컨테이너에서) +# ban 추가 ssh incus-jp1 "incus exec crowdsec -- cscli decisions add --ip 1.2.3.4 --duration 2m --reason 'manual ban' --type ban" # 확인 @@ -41,113 +94,77 @@ 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`로 직접 넣으면 등록 안 됨 — bouncer API 키 권한 제한. +bouncer 컨테이너에서 `curl POST /v1/decisions`로 직접 넣으면 등록 안 됨 — API 키 권한 제한. #### 설정 변경 시 주의 -**`sed -i` 사용 금지** — Incus 컨테이너 내에서 `sed -i`로 파일 수정 시 이전 내용이 파일 끝에 남아 YAML이 깨짐. 반드시 전체 파일을 덮어쓰는 방식으로 변경: +**`sed -i` 사용 금지** — Incus 컨테이너 내에서 `sed -i`로 수정 시 이전 내용이 파일 끝에 남아 YAML 깨짐. 반드시 전체 파일 덮어쓰기: ```bash -# 올바른 방법: 로컬에서 수정 후 전체 파일 push 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" ``` -### bunny-cdn-bouncer 상세 +### bunny-cdn-bouncer (BunnyCDN Edge Script) -- **동기화**: jp1 `infra-tool` 컨테이너, `/opt/crowdsec-bouncer/bouncer.py` -- **크론**: 3분 간격 delta sync, 매시 정각 full sync (`/etc/cron.d/crowdsec-bunny-bouncer`) -- **방식**: CrowdSec LAPI → Bloom filter 생성 → BunnyCDN Edge Script 코드에 임베딩 → 퍼블리시 -- **Edge Script**: BunnyCDN Compute Script ID 64811 (`crowdsec-bouncer-middleware`) - - Bloom filter (FNV-1a) 기반 IP 차단 - - 차단 시 Cloudflare Turnstile CAPTCHA 챌린지 (false positive 대응) - - LibSQL DB로 verified IP 4시간 캐싱 + HMAC 서명 쿠키 - - Clean IP 네거티브 캐시 (최대 50K) -- **적용 풀존**: inouter (5316471) — actions (5330178)은 미적용 -- **API 키**: Vault `secret/infra/crowdsec-bunny-bouncer` (bouncer_key) -- **소스 코드**: `~/crowdsec-bunny-bouncer/` +| 항목 | 값 | +|------|-----| +| 동기화 | 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) | +| API 키 | Vault `secret/infra/crowdsec-bunny-bouncer` | +| 소스 | `~/crowdsec-bunny-bouncer/` | -## 3중 보안 구조 (KR존, 2026-03-15) +## 보안 구조 ``` 클라이언트 → BunnyCDN Edge Script (CrowdSec bouncer, 0차) - → BunnyCDN WAF (1차) - → APISIX + SafeLine WAF (2차) - → CrowdSec (분석/3차) → bunny-cdn-bouncer로 피드백 루프 + → 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 에지 (오리진 도달 전 차단) +- CDN 에지에서 오리진 도달 전 차단 - 차단: SQLi, XSS, CMDi, SSRF, Shellshock, Log4j -- 비활성화한 룰: DATA LEAKAGES SQL (id=911) — NocoDB API 응답 오탐 방지 -- 통과: Request Smuggling, NoSQLi, 일반 경로 스캔 +- 비활성화: DATA LEAKAGES SQL (id=911) — NocoDB 오탐 방지 +- 통과: Request Smuggling, NoSQLi, 경로 스캔 -### 2차: SafeLine WAF (chaitin-waf 플러그인) -- 위치: APISIX 내부 플러그인 → K3s safeline ns의 detector (safeline-detector.safeline.svc.cluster.local:8000) -- APISIX plugin_metadata로 detector 주소 설정 (etcd 저장) -- BunnyCDN을 통과한 공격 차단 -- 라우트별 개별 적용 (gitea는 `.git/` 경로 제외, 바이너리 프로토콜 파싱 불가) -- SafeLine v9.3.2, Helm chart yaencn/safeline 10.1.0 -- tengine 미사용 (replicas: 0), APISIX가 직접 detector 연동 -- 관리 콘솔: safeline.inouter.com (IngressRoute + ServersTransport insecureSkipVerify) -- API 토큰: [[vault]] `secret/infra/safeline` (api_token) -- API 문서: https://safeline.inouter.com/swagger/index.html -- API 헤더: `X-SLCE-API-TOKEN` -- 이전: kr2 Incus VM → K3s safeline ns로 이전 (2026-03-18) +### 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 (로그 분석) -- 위치: jp1 CrowdSec (10.253.100.240:8080) -- APISIX http-logger → CrowdSec HTTP acquisition (global_rules) -- 파서: custom/apisix-json-logs (APISIX http-logger JSON 파싱) -- 반복 공격자 패턴 탐지 (시나리오 매칭) -- 인증: `Authorization: apisix-crowdsec-log-2024` +### 3차: CrowdSec 로그 분석 +- Traefik 로그: Vector DaemonSet → `:8086` → `crowdsecurity/traefik-logs` +- APISIX 로그: http-logger → `:8085` → `custom/apisix-json-logs` +- HTTP 시나리오 매칭 → decision → bouncer 피드백 -### 공격 테스트 결과 (sandbox-tokyo → nocodb.inouter.com) +## waf-kr BunnyCDN Pull Zone -| 공격 | 결과 | 차단 위치 | -|------|------|----------| -| SQLi (`OR 1=1`) | 403 | BunnyCDN WAF | -| SQLi (대소문자 혼합) | 403 | BunnyCDN WAF | -| SQLi (더블 인코딩) | 403 | BunnyCDN WAF | -| SQLi (POST body) | 403 | BunnyCDN WAF | -| XSS (``) | 403 | reject (SafeLine) | - -### 다음 단계: CrowdSec 자동 차단 - -계획: SafeLine WAF 차단 로그 → APISIX http-logger → jp1 CrowdSec → 커스텀 시나리오 (3회 차단 시 ban) → bouncer로 APISIX ip-restriction 적용 - -### 참고 - BunnyCDN WAF 차단 시 오리진에 로그 안 옴 → CrowdSec에 미수신 -- 리얼 IP: 외부 트래픽은 `X-Forwarded-For`로 정상 전달, LAN은 `127.0.0.1` -- OpenWrt에 CrowdSec firewall bouncer 설치 가능하나 DNAT 구조라 리얼 IP 매칭 불가 -- chaitin-waf 플러그인은 `plugin_attr`이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음 — 주의 +- OpenWrt CrowdSec firewall bouncer는 DNAT 구조라 리얼 IP 매칭 불가 +- chaitin-waf 플러그인은 `plugin_attr`이 아닌 **`plugin_metadata`(etcd)**에서 detector 노드를 읽음 +- Incus 컨테이너에서 `sed -i`로 설정 수정 시 파일 손상 주의 (전체 파일 push 사용)