XAI 기반 DDoS 탐지 플랫폼 — 정확도 62%로 시작해서 솔직하게 적기
CIC-DDoS2019로 17-class XGBoost 분류기를 학습해서 SHAP으로 설명까지 붙였다. 합성 데이터에선 99%, 진짜 데이터셋에선 62%. 차이가 정직한 곳.
목차
ML 글의 99%는 99% 정확도로 끝난다. 내 모델은 **62%**로 끝났다. 왜인지 적어둔다.
직접 만든 시스템은 xai.lab-it.net에서 볼 수 있다 (Basic Auth 막아둠).
만든 것
pcap 파일 업로드 → 패킷 흐름 분석 → 각 흐름이 어떤 종류의 DDoS인지 분류 → SHAP으로 “왜 그렇게 판단했는지” 설명 → 대응 권고(BLOCK/RATE_LIMIT/ALLOW)까지.
pcap 업로드
│
▼
CICFlowMeter (subprocess) ← 패킷 → 흐름 단위 62 feature
│
▼
XGBoost predict (17-class) ← Benign/Syn/UDP/NTP/DNS/...
│
▼
SHAP TreeExplainer ← 왜 이 예측인지 top-10 feature
│
▼
Mitigation 매핑 ← SHAP 기여도 기반 권고
│
▼
FastAPI + WebSocket → React 대시보드
데이터셋 — CIC-DDoS2019
캐나다 UNB CIC가 공개한 DDoS 데이터셋이다. Kaggle에 미러가 있다.
kaggle datasets download dhoogla/cicddos2019
# 30GB 다운로드, 압축 풀면 29GB, 18개 CSV
첫 번째 함정 — 디스크. Proxmox VM 기본 디스크 100GB로는 부족. growpart + pvresize + lvextend + resize2fs로 300GB까지 확장하고 다시 받았다.
두 번째 함정 — RAM. 1.6M 행 × 62 컬럼을 그냥 로드하면 8GB RAM에 안 들어간다. 파일당 100K 행씩 샘플링하는 식으로 1.6M 행 dataframe을 만들어 학습.
samples = []
for csv in CSVS:
df = pd.read_csv(csv).sample(n=100_000, random_state=42)
samples.append(df)
data = pd.concat(samples, ignore_index=True)
17개 공격 유형 multi-class.
모델 — XGBoost 한 발
다른 모델 시도 안 했다. 모델 비교 실험은 미니 프로젝트 범위 밖이라 판단.
import xgboost as xgb
clf = xgb.XGBClassifier(
n_estimators=300,
max_depth=8,
objective='multi:softprob',
num_class=17,
n_jobs=-1,
)
clf.fit(X_train, y_train)
학습 시간 30.5초 (CPU만). XGBoost는 tabular 데이터에 강해서 GPU가 큰 이득 없다. GPU 패스스루도 BIOS 벽에 막혀 어차피 못 썼다.
SHAP 0.45.1 multi-class 포맷 변경 — 4시간 삽질
이게 이 프로젝트에서 제일 시간 잡아먹은 부분이다.
옛날 SHAP API는 multi-class TreeExplainer 결과를 list[ndarray] 로 반환했다. 클래스마다 ndarray 하나, 길이는 클래스 수.
# 옛날 SHAP (~0.40)
shap_values = explainer.shap_values(X)
# shap_values[pred_class][sample_idx] ← 이게 동작
0.45.x부터 포맷이 바뀌었다. ndarray(n_samples, n_features, n_classes) 의 3D 텐서를 반환한다.
# 신버전 SHAP
shap_values = explainer.shap_values(X)
# shap_values.shape == (n_samples, n_features, n_classes)
# shap_values[pred_class][0] ← IndexError: index 13 out of bounds for axis 0 with size 1
처음에 이 에러가 떴을 때 한참 헤맸다. 검색하면 옛날 코드가 다 나오는데 다 안 됨. 결국 분기 처리.
shap_vals = explainer.shap_values(X)
if isinstance(shap_vals, list):
# 옛날 API
contribution = shap_vals[pred_class][0]
else:
# 신 API: 3D ndarray
if shap_vals.ndim == 3:
contribution = shap_vals[0, :, pred_class]
else:
contribution = shap_vals[0]
라이브러리 버전 업그레이드 노트는 정말 봐야 한다. 안 보면 4시간 잃는다.
정확도 — 합성 데이터 99%, 진짜 데이터 62%
처음 시연용으로 데모를 만들 땐 합성 트래픽으로 했다. 99.7% accuracy. 차트가 예뻤다.
진짜 CIC-DDoS2019로 평가하니 62~65%. 떨어지는 이유는 명확했다.
precision recall f1-score support
BENIGN 0.84 0.79 0.81 ...
Syn 0.99 1.00 0.99 ...
TFTP 1.00 0.99 0.99 ...
NTP 0.97 0.96 0.97 ...
LDAP 0.42 0.31 0.36 ...
MSSQL 0.38 0.29 0.33 ...
NetBIOS 0.51 0.45 0.48 ...
Portmap 0.22 0.18 0.20 ...
...
클래스 불균형. Syn/TFTP/NTP는 풍부한 샘플 + 시그니처 특성이 뚜렷해서 거의 100%. 반면 LDAP/MSSQL/Portmap 같은 건 샘플이 상대적으로 적고 feature가 BENIGN과 겹쳐서 분리가 안 된다.
해결책 후보:
- SMOTE로 소수 클래스 oversampling
- class_weight balanced
- 17개를 그룹핑(예: reflection 계열 합치기) → 5~7개 클래스로 축소
이건 다음 작업으로 미뤄놨다. 미니 프로젝트라 여기서 멈춤.
기록할 가치는 있다고 본다. “진짜 데이터에선 모델이 무너지는 곳이 어디인지”가 99% 자랑보다 정직하다.
인프라
vm2.lab-it.net (Proxmox VM, Ubuntu 24.04, 8GB RAM, 300GB).
Docker Compose로 묶임:
| 컨테이너 | 역할 |
|---|---|
ddos-backend | FastAPI + XGBoost + SHAP, port 8201 |
ddos-postgres | asyncpg 기반 결과 저장 |
ddos-nginx | 정적 + reverse proxy + Basic Auth |
Cloudflare Tunnel로 https://xai.lab-it.net에 노출. 보안:
- nginx Basic Auth (
htpasswd,limit_except GET로 GET만 무인증 허용 시도하다 결국 풀 인증) - JWT 로그인 페이지 (대시보드)
- fail2ban으로 시도 차단
- UFW로 직접 포트 오픈 X (Tunnel만)
- Docker non-root (uid 1000)
SHAP → 대응 권고 매핑
이게 “XAI”의 실용적 의미다. 단순히 예측만이 아니라, 왜 그렇게 예측했는지로부터 행동을 결정한다.
def mitigation_from_shap(prediction, top_features):
if prediction == "BENIGN":
return "ALLOW"
# 강한 시그니처(SYN flood, NTP amp 등) → 즉시 차단
if prediction in {"Syn", "TFTP", "NTP", "DNS"} \
and top_features[0]["abs_shap"] > 0.5:
return "BLOCK"
# 약한 시그니처 → rate limit
return "RATE_LIMIT"
대시보드에서 흐름마다 권고가 뜬다. 실제 OpenFlow 컨트롤러(Ryu) 연동은 설계만 해두고 실배포 안 함.
디자인 — Toss 스타일
처음엔 다크 테마였다. 보안 대시보드 = 검정 배경 + 빨간 글자 컨벤션. 보다 보니 눈 아프고 정보가 안 들어왔다.
Toss 라이트 테마로 갈아엎음.
#F2F4F6회색 배경#3182F6파란 강조- Pretendard 폰트
- 카드 기반 레이아웃
- Recharts로 면적 차트, 빨강(악성)/파랑(정상) 분리
대시보드는 결국 읽혀야 하는 정보 매체다. 보안이라고 무조건 검정일 필요 없다.
만들면서 알게 된 것들
라이브러리 메이저/마이너 버전 차이의 비용은 시간으로 환산된다. SHAP 0.45.1의 변경 사항을 처음부터 알았다면 4시간을 안 썼다.
합성 데이터는 거짓말이다. 시연용으론 좋지만, 평가는 반드시 진짜 데이터셋으로 해야 한다. 99% → 62% 갭이 진짜 모델의 위치다.
62%가 무능을 뜻하진 않는다. 17-class multi-class에서 random baseline은 5.9%. 클래스별로 보면 핵심 공격은 거의 100%. “전체 정확도”라는 단일 숫자가 가린 게 더 많았다.
XAI는 “왜”의 답이 아니라 “어떻게 행동할지”의 입력이다. SHAP 점수 자체는 의미가 모호해도, BLOCK/RATE_LIMIT 결정의 근거로 쓰면 즉시 가치가 생긴다.
스택 정리
| 구성 | 기술 |
|---|---|
| 데이터 | CIC-DDoS2019 (Kaggle 미러), 1.6M 흐름 |
| 패킷→흐름 | CICFlowMeter 0.1.6 |
| 모델 | XGBoost 2.1.0 (multi-class, 300 trees, 30.5s CPU) |
| 설명 | SHAP 0.45.1 TreeExplainer |
| 백엔드 | FastAPI + asyncpg + WebSocket |
| 프론트 | React + Vite + Recharts (Toss 스타일) |
| 인프라 | vm2 (Ubuntu 24.04, 8GB), Docker Compose, Cloudflare Tunnel |
| 보안 | Basic Auth + JWT + fail2ban + UFW + non-root |
다음 차례:
- 클래스 불균형 처리 (SMOTE or class_weight)
- 17 → 7 클래스로 그룹핑
- Ryu 컨트롤러 실배포
- 모델 개선 후 정확도 재측정 및 정직한 비교 글로 다시 적기