개요 — 무엇을 하나
Security ▸ Detections(/security/detections)는 InfraGuard 의 탐지 플래그십 화면입니다. 세 가지를 한 화면에서 묶습니다.
- 탐지 알고리즘 서사 — 어떤 원리로 위협을 잡는지 교육용 popover 로 설명(Kitsune ML · srct 휴리스틱 · nginx
is_suspicious· JA3). - 탐지 후 서사 — 각 탐지기가 최근 창에서 잡은 이상 IP · 의심 웹 · run 이력을 실데이터로 표면화.
- ★ JA3 시퀀스 룰셋 — 한 출발지 IP 의 시간순 JA3 핑거프린트 시퀀스를 결정적 휴리스틱이 읽어 "연속적 이상행동"을 전문 용어 룰셋(
infr_sec_ruleset)으로 열거·저장합니다.
수집·자동차단은 이미 백엔드에서 돌지만(stats-design 시절 UI 는 방화벽·블랙리스트 표뿐이었습니다) 그 숨은 텔레메트리를 신뢰도 있는 서사로 표면화하는 것이 이 화면의 목적입니다.
이 화면은 읽기 위주 + 온디맨드 분석입니다. 탐지기 자체(kts_tx·srct_tx·eve_ingest)는 대상 호스트에서 pm2 로 도는 Node 수집기이며, 앱은 그들이 채운 테이블을 읽습니다. 앱이 직접 트래픽을 스니핑하지 않습니다.
AI 나레이션(LLM) 경로는 2026-09-04 제거되었습니다. 예전에는 시퀀스 요약을 AI 어댑터에 보내 자유서술 답변을 저장했습니다. 실측으로 확인된 세 가지가 이유입니다 — LLM 의 결론("JA3 로테이션")은 비율 계산 3줄이 이미 내던 것과 같은데 1회 약 9,100 토큰이 들었고, 자유서술을 그대로 저장해 쓴 사람의 언어가 굳어 영문 세션이 한글 카드를 읽었으며, 폴백 경로에서도
provider/model을 어댑터 값으로 찍어 저장된 13행 중 10행이model=kimi-k2.6라벨을 달고 실제로는 템플릿 출력이었습니다. 지금은 휴리스틱만 돌아 외부 호출·토큰 비용이 없고,provider/model은 실제로 무엇이 돌았는지만 기록합니다.
언제 쓰나
- 특정 IP 가 봇넷/자동화 스캐너인지 JA3 로테이션 패턴으로 판정하고 싶을 때.
- IDS·ML·nginx 각 탐지기가 지금 무엇을 잡고 있는지 한눈에 훑을 때(트리아지 진입점).
- 탐지 수집기를 새 호스트에 배치할 때(pm2 명령·소스코드 복사).
- 상위 JA3 오펜더를 분석해 차단/신고 권고 룰셋을 만들 때.
- Overview · Threats 에서 딥링크로 넘어와 특정 IP 를 즉시 분석할 때(
?ip=프리셀렉트).
화면 구성
KPI 스트립
| KPI | 의미 | 원천 |
|---|---|---|
| AI 이상 | 선택 창 내 Kitsune 이상탐지 건수 | kitsune_detail_log count |
| AI 차단 | 그중 is_blocked=1(임계+veto 충족) |
kitsune_detail_log where is_blocked |
| 의심 웹 | 창 내 nginx is_suspicious=1 요청 수 |
nginx_meta_ct count |
| 룰셋 | 현재 활성(status=active) 룰셋 수 |
infr_sec_ruleset |
앞의 두 KPI 가 "AI" 인 것은 Kitsune 이 실제로 머신러닝 오토인코더이기 때문입니다 — 이 라벨은 사실이라 그대로 둡니다.
탐지 알고리즘 패널 (edu popover)
각 탐지기 카드에 원리 · 무엇을 보나 · tip 을 여는 교육 popover. 텍스트는 security.det.edu.* 정적 lang 팩이 소유합니다.
- Kitsune — 앙상블 오토인코더 이상탐지
- srct — Suricata 휴리스틱 자동 차단기(5대 카테고리)
- nginx is_suspicious — TLS 종단 웹 공격 표식
- JA3 시퀀스 분석 — 이 화면의 주역
pm2 배치 안내
탐지 수집기 5종을 대상 호스트에 pm2 cron 으로 배치하는 명령·소스코드를 제공(§데이터 소스 · pm2 표).
탐지 로그 & run 이력
- 이상 IP(Kitsune top) —
anomaly_score내림차순 상위 50: IP·점수·세션수·차단여부·run_ts. - 의심 웹(nginx top) —
is_suspicious히트 상위 12: src_ip·hits·최근 ts. - run 이력 —
kitsune_process_log·srct_process_log최근 30건(스캔/차단 수·메시지).
JA3 오펜더 & 룰셋
- 오펜더 목록 — 고정 6시간 창 기준 상위 12 JA3 오펜더:
ja3_variety·tls_conns·sni_variety·last_ts·국가(geo). 자기망(등록 인스턴스 IP /24)은 제외됩니다. - 룰셋 테이블 — 저장된 룰셋 최신 60건: 엔티티·행동·분류·심각도·권고·신뢰도·출처. 행마다 활성화 / 보류 / 삭제.
사용법
1) 탐지 현황 훑기
상단 창 토글(1h / 6h / 24h) 로 관측 창을 바꿉니다(기본 24h). KPI·탐지 로그가 그 창으로 재계산되고, 화면은 30초 폴링·서비스는 30초 캐시입니다.
오펜더 목록만은 창과 무관하게 항상 최근 6시간입니다. JA3 로테이션은 수 시간에 걸쳐 누적되므로 짧은 창에서는 패턴이 드러나지 않습니다(자동 차단의 360분 JA3 창과 정합).
2) JA3 시퀀스 분석 실행 (★)
- 오펜더 목록에서 IP 를 고르거나(Overview·Threats 딥링크) [분석] 을 누릅니다.
- 서버가
suricata_tls_ct에서 그 IP 의 TLS JA3 시퀀스를 시간순으로 인출합니다(창 = 운영정책sec_ai_window_min, 기본 60분 · 최대 200행). - 시퀀스 길이가 최소치(
sec_ai_min_seq, 기본 5) 미만이면insufficient_sequence로 건너뜁니다. - 휴리스틱이 시퀀스를 findings 로 분류합니다(아래 규칙 표).
- 결과가 룰셋으로 저장되고 목록과 Overview 퍼널이 즉시 갱신됩니다.
응답 예시:
{
"ok": true,
"provider": "heuristic",
"model": null,
"saved": 1,
"seq_len": 37,
"findings": [
{ "key": "ja3_rotation",
"params": { "conns": 37, "variety": 34 },
"category": "tls_anomaly",
"severity": "crit", "action": "block", "confidence": 91 }
]
}
분석은 결정적이고 무료입니다 — 같은 시퀀스는 항상 같은 룰셋을 내고, 외부 서비스를 호출하지 않으며, 네트워크가 없어도 동작합니다. 실행 비용이 없으니 오펜더 수치가 바뀔 때마다 부담 없이 재분석하세요.
휴리스틱 규칙
findings 는 시퀀스 통계로 만듭니다(rotation = ja3_variety / seq_len).
| 조건 | 룰셋 | 분류 | 심각도 | 권고 | 신뢰도 |
|---|---|---|---|---|---|
| variety ≥ 3 그리고 rotation ≥ 0.5 | JA3 로테이션(자동화 스캐너/봇넷) | tls_anomaly | rotation≥0.9 → crit, 그 외 warn | block | 50 + round(rotation×45), 최대 95 |
| 민감 URL 관측(.env·.git·wp-config…) | 자격/설정 파일 조회 시도 | credential_access | crit | block | 88 |
| variety = 1 그리고 conns ≥ 10 그리고 sni_variety ≥ 4 | 단일 JA3 다중 표적 스캔 | scan | warn | observe | 72 |
| 위 어느 것도 아님 | TLS 기준 활동 | other | info | observe | 40 |
민감 URL 은 정규식 .env .git wp-config .aws /actuator phpunit /.. shell.php wp-login xmlrpc 로 매칭합니다(nginx is_suspicious + suricata_http 컨텍스트).
분류·심각도·권고는 삽입 전에 컬럼 enum 으로 검증됩니다(recon · credential_access · exfil · bruteforce · c2 · tls_anomaly · scan · other / info · warn · crit / observe · block · isolate · report).
룰셋 문구와 언어
finding 은 문장이 아니라 정체성으로 저장됩니다. heuristicFindings() 가 {key, params} 를 내고, 행은 rule_key + params(마이그레이션 000783)를 보관하며, 문구는 행을 읽는 시점에 읽는 사람의 언어로 입혀집니다.
- 문구는
resources/lang/<locale>/security.php의det.rule.<key>.{name,behavior}가 소유하며 콘솔 8개 언어 전부 번역돼 있습니다. name/behavior컬럼에는 여전히 영문 렌더가 폴백으로 들어갑니다 — 내보내기·감사, 그리고 000783 이전에 쓰인 행이 읽히지 않게 되면 안 되기 때문입니다.rule_key가 없거나 해당 로케일에 키가 없으면 저장된 문자열을 쓰며, 원시 키는 절대 노출되지 않습니다.- Detections 캐시는 로케일별로 분리됩니다(
infr:sec:det:<locale>:<range>). 이게 없으면 한국어 세션이 데운 30초 캐시를 다음 영문 독자가 그대로 받는데, 실제로 그런 상태였습니다.
000783 이전에 만들어진 행은 원래 텍스트를 유지합니다. 마이그레이션은 각 행의 evidence 로 구 템플릿을 재구성해 정확히 일치할 때만
rule_key를 복원합니다(추측하지 않습니다). 구 LLM 경로가 쓴 자유서술 행은 해당 IP 를 다시 분석해 교체되기 전까지 그대로 남습니다.
3) 룰셋 관리
| 액션 | 효과 |
|---|---|
| 활성화 | status=active(Overview 퍼널·차단 서사에 반영) |
| 보류 | status=dismissed |
| 삭제 | 행 제거 |
재분석은 그 IP 의 기존 기계 유래 룰셋만 교체합니다(entity_type=ip, entity_key=ip, source=ai 삭제 후 재생성) — 수동 작성 룰셋은 보존됩니다.
룰셋의
action필드는 권고이지 명령이 아닙니다. 이 값으로 동작하는 코드는 없습니다 — 자동 차단은suricata_tls_ct에서 JA3 로테이션을 직접 계산하며infr_sec_ruleset을 읽지 않습니다.action=block인 룰셋이 있어도 아무것도 차단되지 않습니다. 차단은 Blocklist 에서 하거나 자동 차단 스케줄러가 수행합니다.
4) 스케줄 축적(옵트인)
infr:sec-analyze 커맨드가 상위 JA3 오펜더를 순회하며 룰셋을 축적합니다. 단발 실행 후 종료하는 커맨드라 스케줄에 안전하게 걸 수 있습니다(데몬 아님).
# 게이트 무시하고 1회 실행(디버그)
php artisan infr:sec-analyze --force
# 단일 IP 분석(게이트 무관)
php artisan infr:sec-analyze --ip=203.0.113.10
- 게이트: 운영정책
sec_ai_schedule_on(기본0=꺼짐). 꺼져 있으면 no-op — 운영 정책 보안 탭에서 켭니다. - 스케줄 실행은 상위
sec_ai_top_n(기본 10) 오펜더를 분석합니다. - 스케줄은 30분마다 돌며 분석이 휴리스틱이라 토큰 비용도 외부 호출도 없습니다. 그럼에도 기본이 꺼짐인 것은 룰셋이 정보 제공용이기 때문입니다.
5) pm2 배치 명령 복사
pm2 카드는 [명령 복사](pm2 실행 줄), [소스코드 복사](스니펫 body), [인스턴스 값으로 채워 복사](HOSTNAME/IP 토큰 치환)를 제공합니다. 소스는 시드된 Inventory ▸ Tools 스니펫(infr_snippet)에서 옵니다.
데이터 소스
읽는 레거시 테이블(수정 금지)
| 테이블 | 용도 | 공급 수집기 |
|---|---|---|
kitsune_detail_log |
이상 IP·anomaly_score·is_blocked | kts_tx |
kitsune_process_log |
Kitsune run 이력 | kts_tx |
srct_process_log |
srct run 이력 | srct_tx |
nginx_meta_ct |
의심 웹 요청(is_suspicious) |
nginx_shipper/aggregator |
suricata_tls_ct |
JA3 시퀀스(ja3_hash·sni·version) | eve_ingest |
suricata_http_ct |
분석용 HTTP 컨텍스트 | eve_ingest |
blacklist_info |
자동 차단 결과(참조) | kts_tx · srct_tx |
ip_info_loop |
geo/ASN 인리치 | — |
앱 소유 신규 테이블
infr_sec_ruleset(마이그레이션 000730 · 000783) — 룰셋. 컬럼:entity_type·entity_key·name·behavior·rule_key·params(json)·category·severity·action·confidence·ja3·sequence(json)·evidence(json)·source(ai=기계 유래 | manual)·provider·model·status(draft|active|dismissed).SecurityAiService::persist()가 채웁니다.infr_snippet— pm2 소스코드 body.infr_audit_log—sec.ai_analyze·sec.ruleset_save·sec.ruleset_delete감사. 첫 액션 문자열은 그대로 두었습니다 — 바꾸면 기존 감사 이력이 둘로 갈라지기 때문입니다.
pm2 배치 수집기
| key | 공급 | 배치 명령(cron-restart) |
|---|---|---|
| eve_ingest | suricata_*_ct |
pm2 start infr_eve_ingest.cjs --name infr-eve --cron-restart="0 0 * * 6" |
| kts_tx | kitsune_*_log · blacklist_info |
pm2 start infr_kts_tx.cjs --name infr-kitsune --cron-restart="0 0 * * 2" |
| srct_tx | srct_process_log · blacklist_info |
pm2 start infr_srct_tx.cjs --name infr-srct --cron-restart="0 0 * * 4" |
| sync_black | ipset · iptables |
pm2 start infr_sync_black.mjs --name infr-syncblack --cron-restart="*/5 * * * *" |
| sync_firewall | firewall_status |
pm2 start infr_sync_firewall.cjs --name infr-firewall --cron-restart="0 0 * * 0" |
서비스 & 라우트
- DetectionService — KPI·탐지 로그·오펜더·룰셋 빌드(로케일별 30초 캐시,
SecurityRepository읽기 전용).phrase()가 읽는 시점에 룰셋 문구를 입힙니다. - SecurityAiService — JA3 시퀀스 → 휴리스틱 findings → 룰셋 upsert. LLM 호출이 없습니다.
- SecurityScope::excludePrefixes() — 자기망 제외(등록 인스턴스 IP /24 + 운영정책 · 하드코딩 금지).
| 메서드 | 라우트 |
|---|---|
| GET | /security/detections(index) |
| GET | /security/detections/live(폴링) |
| POST | /security/detections/analyze(분석) |
| POST | /security/detections/ruleset/save |
| POST | /security/detections/ruleset/delete |
접근 권한
| 작업 | 게이트 |
|---|---|
| 화면 조회 | Security 역할영역 접근(min_req 2) — demo(1)는 보안 영역 자체가 보이지 않고, staff(5)·admin(10)은 조회 가능 |
| 분석 / 룰셋 저장·삭제 | user_level ≥ 2(canWrite) + infr_audit_log 감사 |
- 쓰기 게이트는 컨트롤러
canWrite()(세션infr_user_level ≥ 2)가 강제합니다 — demo 레벨은 분석·룰셋 편집을 할 수 없습니다. - IP 입력은
filter_var(FILTER_VALIDATE_IP)로 검증합니다(잘못된 IP → 422). - 모든 파괴적 작업은 감사 로그에 기록됩니다 → 감사.
문제 해결 & FAQ
분석이
insufficient_sequence로 건너뜀 — 그 IP 의suricata_tls_ct시퀀스가sec_ai_min_seq(기본 5) 미만입니다. 창(sec_ai_window_min, 기본 60분)을 넓히거나 오펜더 목록(6시간 창)에서 더 활발한 IP 를 고르세요.
- 오래된 룰셋이 엉뚱한 언어로 보임 — 000783 이전에 쓰여 텍스트가 굳은 행입니다. 그 IP 를 재분석하면 새 행이
rule_key를 저장해 읽는 사람의 언어를 따릅니다. 더 이상 나타나지 않는 IP 의 행은 테이블에서 삭제하면 됩니다. - KPI·룰셋이 안 바뀜 — 30초 캐시 + 폴링 주기 때문입니다. 분석·룰셋 저장은 캐시를 즉시 무효화하므로(
flush· 전 로케일 변형) 새로고침하면 반영됩니다. - 오펜더 목록이 빔 — 최근 6시간
suricata_tls_ct에 JA3 활동이 없거나 전부 자기망으로 제외됐습니다.eve_ingest수집기가 도는지 확인하세요. - 스케줄이 아무것도 안 함 —
sec_ai_schedule_on=0(기본)입니다. 운영 정책 보안 탭에서 켜거나--force로 1회 실행하세요. - 탐지 로그가 전부 0 — 해당 pm2 프로세스가 죽었을 수 있습니다. pm2 카드의 배치 명령으로 재시작하고 run 이력을 확인하세요.
관련 문서
