집 서버에 WAF/IPS를 직접 짓다 — nginx + CrowdSec 4계층 방어 구축 리서치 — ZINO
2026-05-22·1분 읽기·
집 서버에 WAF/IPS를 직접 짓다 — nginx + CrowdSec 4계층 방어 구축 리서치
zino.kr은 집에 있는 미니 PC 한 대다. 8일치 nginx 로그를 까보니 16만 요청 중 2만 건 넘게 공격이었다. CrowdSec(IPS) + AppSec(WAF) + AbuseIPDB(평판)로 4계층 심층 방어를 직접 구축하고, 실제 공격 로그에서 커스텀 룰을 뽑고, 8일치 로그를 리플레이해 검증하고, 관제 대시보드까지 만든 전 과정 기록.
집에 있는 미니 PC 한 대. 그 위에서 nginx가 리버스 프록시로 돌고, 그 뒤에 Next.js 앱이 pm2로 떠 있다. 그게 전부다.
그런데 이 작은 서버도 인터넷에 노출된 이상 24시간 두들겨 맞는다.
막연히 알고는 있었다. 하지만 정확히 얼마나, 그리고 어떻게 맞는지는 제대로 본 적이 없었다. 그래서 먼저 8일치 nginx 액세스 로그를 통째로 분석했다.
결과는 명확했다.
zino.kr 가상호스트로 들어온 요청이 153,638건. 거기에 도메인도 없이 IP로 직접 찔러본 스캔이 10,101건. 그중 공격 시그니처에 걸리는 요청이 2만 건을 넘었다.
8일 중 단 하루도 공격이 0인 날이 없다. 5월 14일은 3,675건까지 치솟았다.
기존에 방어가 아예 없던 건 아니다.
AbuseIPDB IP 블로커 — 15분마다 cron이 nginx 로그를 분석해, 짧은 시간에 많이 때린 IP를 AbuseIPDB로 조회한다. 신뢰도 70점 이상이면 nginx geo 모듈로 403 처리한다. (예전에 만들어 둔 것이고, playground에 모니터링 페이지도 있다.)
fail2ban — SSH 무차별 대입만 막는다.
문제는 이 구성이 "이미 평판이 나쁜 IP" 와 "SSH 브루트포스" 만 막는다는 것이다.
정작 웹으로 들어오는 공격 — SQL 인젝션, 경로 순회, .env 수집, 알려진 CVE 정찰 — 은 그냥 통과했다. 요청 내용을 들여다보는 계층이 아예 없었기 때문이다.
다행히 Next.js 앱이라 PHP 익스플로잇이 먹히진 않았다. 하지만 "안 맞아서 다행"과 "막아서 안전함"은 다르다.
이 글은 그래서 WAF(웹 방화벽)와 IPS(침입 차단 시스템)를 집 서버에 직접 구축한 기록이다.
남이 만든 매니지드 서비스를 붙이는 게 아니다. 오픈소스로 self-hosted 구성을 짜고, 내 실제 공격 로그에서 룰을 뽑고, 그게 진짜 막는지 검증하고, 마지막으로 무엇이 잡혔는지 한눈에 보는 관제 대시보드까지 만들었다.
154.19.37.43 — WordPress 플러그인 대상 시간 기반 블라인드 SQLi. ?id=1+AND+(SELECT+1+FROM+(SELECT(SLEEP(6)))A) 를 알려진 취약 플러그인 엔드포인트에 정밀 타격했다.
이 분석에서 나온 핵심 통찰
가장 중요한 발견은 따로 있었다.
zino.kr은 Next.js 앱이다. PHP를 단 한 줄도 서빙하지 않는다. Java 백엔드도, 노출된 .git 디렉터리도 없다.
그렇다면 /admin.php, /.git/config, /actuator/heapdump, /boaform/... 같은 요청은 — 정상일 가능성이 0% 다.
일반적인 WAF 룰은 "SQLi처럼 보이는 패턴"을 휴리스틱으로 잡느라 오탐과 싸운다. 하지만 "이 사이트엔 절대 존재하지 않는 것"의 목록은 그 자체로 오탐 없는 완벽한 룰이 된다.
이 통찰이 6장 커스텀 룰의 토대가 된다.
한 가지 주의할 함정도 있었다. /wp-content/uploads/2022/09/*.png 같은 경로가 로그에 잔뜩 찍힌다. 이건 공격이 아니라 — 이 사이트가 예전에 WordPress였던 시절의 정상 이미지 URL이고, SEO 크롤러가 아직도 인덱스를 따라 들어오는 것이다. "wp-content가 보이면 차단" 같은 거친 룰을 짰다면 멀쩡한 크롤러를 막을 뻔했다. 룰은 .php 확장자를 노려야지, wp-content 경로를 노리면 안 된다.
3. 설계 — 4계층 심층 방어
공격 지형을 알았으니 방어를 설계한다.
핵심 원칙은 심층 방어(defense in depth) 다. 하나의 만능 방패가 아니라, 서로 다른 원리로 동작하는 여러 계층을 겹쳐서 한 계층이 놓쳐도 다음이 잡게 한다.
들어오는 요청은 nginx 안에서 4개의 관문을 통과한다. 오른쪽 CrowdSec 엔진이 '무엇을 막을지' 결정한다.
계층
기술
막는 것
원리
L1 엣지
nginx
비정상 메서드·과도한 요청
형식 검사
L2 IP 평판
AbuseIPDB
이미 평판이 나쁜 IP
평판 DB
L3 IPS
CrowdSec
행동으로 들킨 IP
행위 분석
L4 WAF
CrowdSec AppSec
요청 내용이 공격인 것
시그니처
네 계층은 던지는 질문이 다르다.
L1은 "형식이 맞나?", L2는 "이 IP, 전과 있나?", L3은 "이 IP, 지금 수상하게 구나?", L4는 "이 요청 내용 자체가 공격인가?"를 묻는다.
그래서 한 계층을 우회해도 다른 계층의 질문에는 걸린다.
왜 CrowdSec인가
L3와 L4를 무엇으로 구현할지가 핵심 결정이었다.
후보는 ModSecurity(전통적 WAF), SafeLine(독립형 WAF), CrowdSec였다. CrowdSec 단일 스택을 골랐다.
이미 있는 nginx 로그를 그대로 먹는다. 엣지 구조를 갈아엎을 필요가 없다. (SafeLine은 80/443을 가로채는 프록시라 프로덕션 엣지 수술이 필요했다.)
AppSec 컴포넌트로 IPS와 WAF를 한 시스템에서 처리한다.
커뮤니티 인텔리전스(CAPI) — 전 세계 CrowdSec 사용자가 공유하는 악성 IP 블록리스트를 받는다. 남이 당한 공격자를 내가 미리 막는 집단 면역이다.
cscli로 데이터가 깔끔하게 빠져서 관제 대시보드 만들기 좋다.
⚠️ 한 가지 미리 못 박아둘 것. 호스트 단 WAF/IPS는 볼류메트릭 DDoS는 못 막는다. 회선 자체가 포화되면 nginx 앞단에서 할 수 있는 게 없다. 그건 Cloudflare 같은 업스트림의 영역이고, 이 글의 범위 밖이다. 여기서 막는 건 "지능형 공격"이지 "물량 공세"가 아니다.
4. L3 구축 — CrowdSec IPS
설치: 버전 함정
Ubuntu 25.10 universe에도 crowdsec가 있다.
하지만 1.4.6 — AppSec(WAF) 기능이 들어오기 전 버전이다. WAF까지 쓰려면 1.5+ 가 필요하므로 CrowdSec 공식 저장소를 추가해 1.7.8을 받았다.
공격 15종을 모아 테스트 배터리를 돌렸다 — /admin.php, /.git/config, /boaform/..., /actuator/heapdump, PHPUnit RCE 등. 15종 전부 403 차단. AppSec 엔진 메트릭에 15 처리 / 15 차단으로 정확히 집계됐다.
이 리플레이는 두 가지를 동시에 증명했다.
하나, 이 시스템을 진작 켰다면 그 439건은 전부 잡혔다. 둘, 대시보드에 보여줄 실제 데이터가 생겼다.
9. 관제 대시보드 — 무엇이 잡혔는지 한눈에
방어를 구축했으면, 무엇을 막고 있는지 볼 수 있어야 한다.
안 보이는 방어는 운영되지 않는다. 그래서 playground에 통합 보안 관제 대시보드를 만들었다 — 기존 IP 블로커 모니터링과 통합해서.
데이터 파이프라인
cscli는 root 권한이 필요하고, 홈페이지 프로세스는 jinho 권한으로 돈다.
그래서 기존 AbuseIPDB 블로커와 똑같은 패턴을 썼다 — root cron이 데이터를 JSON 파일로 덤프하고, Next.js가 그 파일을 읽는다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…