8 min read

홈서버를 운영하다보면 반드시 마주치는 것

80, 443 포트 개방 후 무차별로 달려드는 봇들

1.webp

이것저것 내부적으로 쓰는 것들이야 이중 인증이나 내부 IP로만 사용하도록
굳이 DNS 로 연결하지 않았습니다만 블로그도 운영하고
일부 셀프호스트는 지인들에게까지도 개방하고 있기 때문에...
홈서버에 리버스 프록시를 붙이고 도메인을 연결한 순간, 로그에 이상한 요청이 쌓이기 시작했습니다.

이럴 줄 몰랐냐고요? 알았는데 너무 많이들어와요.

GET /.env HTTP/1.1
GET /.env.backup HTTP/1.1
GET /var/www/.env HTTP/1.1
GET /.env.preprod HTTP/1.1
GET /blog/.git/config HTTP/1.1
GET /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1
GET /config/database.yml HTTP/1.1

아무도 모르는 도메인인데, 공개한 지 몇 시간 만에 이런 요청이 들어옵니다. 봇이 IP 대역을 무차별로 스캔하면서 열린 포트를 찾고, 알려진 취약점 경로를 전부 때려보는 거예요.

내 서버는 작으니까 괜찮겠지는 착각이었습니다. 크기와 상관없이, 인터넷에 열려 있으면 타겟이 됩니다.


1주일 로그를 까봤다

Traefik access log 기준, 1주일 동안:

항목 수치
총 요청 수 ~65,000건
의심 요청 (버킷 전달) ~40,000건
로컬 탐지 → 자동 ban 50건
커뮤니티 블랙리스트 차단 30,540 IP

(AI로 로그를 분류해보니)
전체 요청의 60% 이상이 의심스러운 트래픽이었습니다. 정상 사용자보다 봇이 더 많이 방문하는 셈이에요.

어떤 공격이 오는가

유형 건수 뭘 노리는지
HTTP probing 236 존재하는 경로 탐색
민감 파일 접근 129 .env, .git/config, DB 설정
기술 스택 탐지 120 어떤 프레임워크 쓰는지 파악
Admin 패널 탐색 95 /admin, /wp-admin, /phpmyadmin
WordPress 스캔 94 WP 플러그인 취약점
알려진 CVE 공격 46 ThinkPHP, PHPUnit, Jira 등
백도어 시도 20 웹쉘 업로드 시도

재밌는 건 — 저는 PHP도 WordPress도 안 쓰는데, 봇은 그런 거 신경 안 씁니다. 가능한 모든 취약점을 전부 시도해봐요.

어디서 오는가

캐나다: 21건 (대부분 Microsoft Azure)
미국:   16건
독일:    6건 (VPS 호스팅 업체)
일본:    3건 (Tencent Cloud)
싱가포르: 3건

클라우드 VPS에서 오는 게 대부분입니다. 공격자가 직접 자기 IP를 쓰는 게 아니라, 클라우드 인스턴스를 빌려서 스캔을 돌리는 거죠.


CrowdSec: 커뮤니티 기반 방어

이걸 수동으로 막으려면 끝이 없습니다. IP 하나 차단해봤자 다른 IP로 또 옵니다.

회사에서 AWS CloudFront + WAF를 운영하면서 느낀 게 있었어요. WAF의 진짜 가치는 개별 룰이 아니라 Managed Rule Group — 즉 AWS나 서드파티가 관리하는 위협 인텔리전스 기반 IP 차단 목록이었습니다. 직접 룰을 하나하나 만드는 건 한계가 있고, 결국 "이미 알려진 악성 IP를 얼마나 빨리 공유받느냐"가 핵심이더라고요.

홈서버에 AWS WAF를 붙일 수는 없지만, 같은 원리로 동작하는 셀프호스팅 솔루션이 CrowdSec이었습니다. 커뮤니티가 곧 Managed Rule Group 역할을 하는 거죠.

CrowdSec의 핵심 아이디어는 단순합니다:

  1. 로컬 탐지: 내 서버 로그를 분석해서 악성 패턴을 감지하면 ban
  2. 커뮤니티 공유: 전 세계 CrowdSec 사용자가 탐지한 악성 IP를 공유
  3. 선제 차단: 다른 서버에서 이미 ban된 IP는 내 서버에 도달하기 전에 차단

약 3만개 IP가 커뮤니티 블랙리스트로 선제 차단되고 있었습니다. 이 IP들은 내 서버를 공격한 적 없지만, 다른 서버를 공격한 이력이 있어서 미리 막혀 있는 거예요.


Traefik + CrowdSec Bouncer 연동

구조는 이렇습니다:

요청 → Traefik → CrowdSec Bouncer 플러그인 → 서비스
                        │
                        ├─ IP가 블랙리스트에 있음 → 403 차단
                        └─ 정상 → 통과

Traefik 플러그인으로 동작하니까, 별도 프록시 레이어 없이 기존 구조에 끼워넣을 수 있습니다.

설치

# docker-compose.yml (예시)
services:
  crowdsec:
    image: crowdsecurity/crowdsec:v1.7
    environment:
      - COLLECTIONS=crowdsecurity/linux crowdsecurity/traefik
      - BOUNCER_KEY_firewall=${CROWDSEC_BOUNCER_KEY}
    volumes:
      - crowdsec-db:/var/lib/crowdsec/data/
      - crowdsec-config:/etc/crowdsec/
      - ./traefik/logs:/var/log/traefik:ro  # Traefik 로그를 읽어야 함

Traefik access log를 CrowdSec에 마운트하는 것. CrowdSec이 이 로그를 실시간으로 파싱하면서 악성 패턴을 감지합니다.

Traefik 플러그인 설정

# traefik 설정
experimental:
  plugins:
    crowdsec-bouncer:
      moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
      version: "v1.4.1"
# 동적 미들웨어 설정
http:
  middlewares:
    crowdsec:
      plugin:
        crowdsec-bouncer:
          crowdsecLapiScheme: http
          crowdsecLapiHost: crowdsec:8080
          crowdsecLapiKey: ${BOUNCER_API_KEY}

이 미들웨어를 원하는 라우터에 붙이면 끝입니다. 저는 외부 노출되는 서비스 전부에 걸어뒀어요.


로컬 탐지: 시나리오 기반

CrowdSec은 시나리오라는 단위로 공격을 탐지합니다. 커뮤니티가 만든 시나리오를 설치하면 됩니다.

# 설치된 시나리오 확인
cscli scenarios list

# 주요 시나리오들
crowdsecurity/http-probing          # 경로 무차별 탐색
crowdsecurity/http-sensitive-files  # .env, .git 등 접근
crowdsecurity/http-admin-interface-probing  # 관리자 페이지 탐색
crowdsecurity/http-wordpress-scan   # WP 취약점 스캔
crowdsecurity/thinkphp-cve-2018-20062  # ThinkPHP RCE

시나리오가 트리거되면 해당 IP를 자동으로 ban합니다. 기본 4시간, 반복 위반 시 더 길어져요.


실제로 잡힌 것들

현재 활성 ban 목록 일부:

IP: 144.31.xxx.xxx (US) — ThinkPHP CVE + HTTP probing + PHPUnit CVE
  → 하나의 IP가 3가지 공격을 동시에 시도. 전형적인 자동화 스캔.

IP: 45.135.xxx.xxx (DE, VPS 호스팅) — HTTP probing + 민감 파일 접근
  → 11건의 요청을 빠르게 보냄. .env 파일 변형을 전부 시도.

IP: 20.151.xxx.xxx (CA, Azure) — Admin 인터페이스 탐색
  → /admin, /administrator, /wp-admin 등 4개 경로 시도.

패턴이 보이죠? 한 IP가 여러 공격을 동시에 시도합니다. 하나라도 걸리면 즉시 ban되니까, 나머지 시도는 전부 차단됩니다.


fail2ban과 뭐가 다른가

fail2ban도 로그 기반 차단을 하는데, CrowdSec을 선택한 이유:

fail2ban CrowdSec
탐지 범위 내 서버 로그만 내 서버 + 커뮤니티 공유
선제 차단 ✅ (30,000+ IP 사전 차단)
시나리오 정규식 기반 구조화된 시나리오 (leaky bucket)
Traefik 연동 별도 iptables 조작 필요 플러그인으로 깔끔하게
관리 텍스트 설정 파일 CLI + 웹 대시보드

커뮤니티 블랙리스트가 결정적이었습니다. 내 서버를 공격하기 전에 이미 차단되어 있으니까, 로그에 남지도 않아요. fail2ban은 맞고 나서 막는 방식이고, CrowdSec은 맞기 전에 막는 방식입니다.


한계와 주의점

만능은 아닙니다.

오탐 가능성 — 커뮤니티 블랙리스트에 정상 IP가 포함될 수 있어요. 특히 공유 VPN이나 CGNAT 환경에서. 중요한 서비스라면 화이트리스트를 관리해야 합니다.

우회 가능 — IP 기반 차단이라 IP를 바꾸면 끝입니다. 정교한 공격자는 매번 새 IP를 쓰니까요. 이건 CrowdSec만의 한계가 아니라 IP 기반 방어의 근본적 한계입니다.

로그 의존 — Traefik이 로그를 안 남기면 탐지를 못 합니다. access log 설정이 필수예요.

그래도 자동화된 대량 스캔의 90% 이상은 걸러줍니다. 나머지 10%는 SSO나 애플리케이션 레벨 인증으로 막으면 됩니다.


돌이켜보면

좋았던 점: 설치 후 신경 쓸 게 거의 없습니다. 커뮤니티 블랙리스트가 자동 업데이트되고, 로컬 탐지도 알아서 돌아가니까요. 가끔 cscli decisions list로 뭐가 잡혔나 구경하는 정도.

아쉬웠던 점: 대시보드가 없으면 지금 뭘 막고 있는지 가시성이 떨어집니다. CrowdSec Console(클라우드)을 연결하면 되긴 하는데, 셀프호스팅 취지와 좀 맞지 않아서 CLI로 버티고 있어요.

다시 한다면: 처음부터 넣었을 겁니다. 나중에 보안 신경 쓰지 뭐 하고 미루다가 로그를 보고 소름 돋았거든요. 포트를 여는 순간 방어도 같이 시작해야 합니다.


참고 자료