하루 11,000건, 봇 트래픽의 30%가 404: nginx에서 잡아낸 이야기


서버 로그를 열어봤더니
모바일에서 글 저장이 안 되는 증상을 조사하려고 서버 로그를 열었다. 저장 오류 원인은 금방 찾았는데, 로그를 훑다 보니 이상한 게 눈에 띄었다.
85.208.96.206 "GET /news/3062" 200 "SemrushBot/7~bl" 85.208.96.204 "GET /news/2018" 200 "SemrushBot/7~bl" 85.208.96.199 "GET /news/4010" 200 "SemrushBot/7~bl" ... 168.119.104.73 "GET /wp-login.php" 404 168.119.104.73 "GET /.env" 404 168.119.104.73 "GET /admin/config.php" 404
SemrushBot이 뉴스 페이지를 쉬지 않고 긁고 있고, 누군가는 WordPress 취약점을 찔러보고 있다. 이 사이트는 Next.js + Spring Boot인데.
대체 얼마나 오는 건지 궁금해서 24시간치 로그를 뽑아봤다.
숫자로 보는 봇 트래픽
24시간 동안 총 11,231건의 요청이 들어왔다.
요청 분포 ┌───────────────────────────────────────────────────┐ │ 전체 요청 11,231건 │ ├───────────────────────────────────────────────────┤ │ 정상 응답(200) ~6,500건 ████████████████ 58% │ │ 404 응답 3,397건 ██████████ 30% │ │ 기타(301,401 등) ~1,300건 ███ 12% │ └───────────────────────────────────────────────────┘
응답의 30%가 404다. 없는 페이지를 두드리는 요청이 전체의 거의 3분의 1.
누가 이러고 있을까?
봇별 요청 수 (24시간) ┌──────────────┬────────┬────────┐ │ 봇 │ 요청 수 │ 비율 │ ├──────────────┼────────┼────────┤ │ SemrushBot │ 3,059 │ 27.2% │ │ Googlebot │ 262 │ 2.3% │ │ Yeti(네이버) │ 180 │ 1.6% │ │ bingbot │ 146 │ 1.3% │ │ AhrefsBot │ 141 │ 1.3% │ │ MJ12bot │ 90 │ 0.8% │ │ GPTBot │ 2 │ 0.0% │ └──────────────┴────────┴────────┘
SemrushBot 혼자서 전체 트래픽의 27%를 차지하고 있었다. 하루 3,000건 넘게. Semrush라는 SEO 분석 서비스가 경쟁사 분석용으로 돌리는 봇인데, 내 사이트에는 아무 도움이 안 된다.
그 밑으로 AhrefsBot, MJ12bot 같은 SEO 봇들이 줄줄이 달려 있고, 취약점 스캐너들이 .php, .env, wp-login.php 같은 경로를 두드리고 있었다.
취약점 스캔은 이런 식이다
404 응답 3,397건 중 상당수가 이런 패턴이다.
GET /.env → 환경변수 파일 탈취 시도 GET /wp-login.php → WordPress 로그인 페이지 GET /admin/config.php → 관리자 설정 접근 GET /phpMyAdmin/ → DB 관리 도구 GET /xmlrpc.php → WordPress XML-RPC 공격 GET /backup.sql → DB 백업 파일 GET /.git/config → Git 설정 노출
자동화된 스캐너가 "혹시 WordPress 아니야? .env 파일 열려 있지 않아?" 하고 무작위로 찔러보는 거다. Next.js 앱이니 당연히 전부 404가 돌아가는데, 문제는 이 404도 공짜가 아니라는 점이다. Next.js가 SSR로 404 페이지를 렌더링해서 돌려주고 있으니까.
전략: 허용 목록 방식
봇 차단은 두 가지 방법이 있다.
방법 A: 차단 목록 (Blocklist) → 나쁜 봇을 하나씩 등록 → 새로운 봇이 나오면 계속 추가해야 함 방법 B: 허용 목록 (Allowlist) ← 이걸 골랐다 → 유용한 봇만 허용, 나머지는 전부 차단 → 새로운 봇이 나와도 자동 차단
허용 목록 방식을 골랐다. 검색 엔진에 노출시킬 봇은 어차피 정해져 있으니까.
허용 봇 목록 ┌──────────────────┬───────────────────────────┐ │ 봇 │ 용도 │ ├──────────────────┼───────────────────────────┤ │ Googlebot │ 구글 검색 색인 │ │ Yeti │ 네이버 검색 색인 │ │ bingbot │ 빙 검색 색인 │ │ Kakaobot │ 카카오톡 링크 미리보기 │ │ FacebookBot │ 페이스북 공유 미리보기 │ │ LinkedInBot │ 링크드인 공유 미리보기 │ │ Twitterbot │ 트위터 카드 미리보기 │ └──────────────────┴───────────────────────────┘
이 목록에 없으면 전부 차단한다. SEO 분석 봇이든, AI 크롤러든, 취약점 스캐너든.
nginx map으로 구현
앱 코드는 건드리지 않았다. nginx 설정만으로 처리했다.
요청 흐름 ┌─────────┐ ┌───────────────────┐ ┌──────────┐ │ 클라이언트 │───▶│ nginx (봇 판별) │───▶│ 앱 서버 │ └─────────┘ │ │ └──────────┘ │ bad_bot? → 403 │ │ vuln_scan? → 444 │ │ 그 외 → 통과 │ └───────────────────┘
봇 판별: User-Agent 기반
nginx의 map은 변수 값을 보고 다른 변수를 설정하는 기능이다. $http_user_agent를 보고 $is_bad_bot을 0 또는 1로 정한다.
map $http_user_agent $is_bad_bot { default 0; # SEO 분석 봇 ~*SemrushBot 1; ~*AhrefsBot 1; ~*MJ12bot 1; ~*DotBot 1; # AI 크롤러 ~*GPTBot 1; ~*CCBot 1; ~*ClaudeBot 1; ~*Bytespider 1; # 스크래핑 도구 ~*python-requests 1; ~*Scrapy 1; ~*HeadlessChrome 1; # 빈 User-Agent "" 1; }
~*는 대소문자 무시 정규식 매칭이다. SemrushBot이든 semrushbot이든 잡힌다.
취약점 스캔: URI 패턴 기반
map $request_uri $is_vuln_scan { default 0; ~*\.php 1; ~*\.env 1; ~*\.git 1; ~*wp-login 1; ~*wp-admin 1; ~*phpmyadmin 1; ~*/\.well-known/ 0; # ACME 인증은 열어둠 }
.well-known 경로는 Let's Encrypt SSL 인증서 갱신에 필요할 수 있어서 따로 열어뒀다.
server 블록에 적용
server { listen 443 ssl; server_name www.fullstackfamily.com; # 봇 차단 (server 블록 상단에) if ($is_bad_bot) { return 403; } if ($is_vuln_scan) { return 444; } # ... 나머지 설정 }
여기서 403과 444의 차이가 있다.
403 Forbidden → "너 차단됐어" 응답을 보냄 → 정상적인 HTTP 응답이라 봇이 인지 가능 444 (nginx 전용) → 응답 없이 연결을 끊어버림 → 봇 입장에서는 타임아웃처럼 보임 → "여기 서버가 있다"는 힌트조차 안 줌
일반 봇에게는 403으로 차단 사실을 알려주고, 취약점 스캐너에게는 444로 아무 정보도 안 준다.
적용 결과
nginx -t로 설정 검증하고 nginx -s reload로 반영했다. 바로 효과가 보였다.
적용 후 1시간 트래픽 ┌───────────────────────────────────────────┐ │ 전체 요청 715건 │ ├───────────────────────────────────────────┤ │ 정상 200 응답 414건 ████████████ 58% │ │ 403 차단(봇) 28건 █ 4% │ │ 444 차단(스캔) 18건 █ 3% │ │ 기타 255건 ████ 35% │ └───────────────────────────────────────────┘
차단된 요청을 뜯어보면:
403 차단 (봇) → SemrushBot이 여전히 오지만 전부 403으로 돌아감 → 응답 시간: 0.000초 (앱 서버까지 안 감) 444 차단 (취약점 스캔) → /.env, /web/.env, /laravel/.env 등 → /production/.env, /storage/.env ... → 하나의 스캐너가 경로를 바꿔가며 .env를 찾는 패턴
.env 파일 하나 찾겠다고 경로를 바꿔가며 수십 번 두드리는 게 보인다. /laravel/.env, /production/.env, /v1/.env... Laravel 앱도 아닌데. 이제 nginx에서 바로 끊긴다.
진짜 의미 있는 변화는 응답 시간이다. 차단 요청의 응답 시간이 0.000초. 이전에는 이 요청들이 전부 Next.js 앱 서버까지 가서 404 페이지를 SSR로 렌더링하고 돌아왔다.
이전: 봇 요청 → nginx → Next.js(SSR) → 404 렌더링 → 응답 이후: 봇 요청 → nginx → 403/444 (0ms)
앱 서버가 하지 않아도 될 일을 안 하게 된 거다.
주의할 점
robots.txt와의 관계
"robots.txt에 Disallow 넣으면 되는 거 아닌가요?"
정상적인 봇은 robots.txt를 존중한다. 그런데 SemrushBot이나 취약점 스캐너는 무시하는 경우가 많다. nginx 차단은 robots.txt와는 별개의 방어선이다.
방어선 1: robots.txt → 착한 봇에게 "여기 오지 마" 안내 방어선 2: nginx 차단 → 안 듣는 봇을 실제로 막음
둘 다 설정해두는 게 맞다.
과잉 차단 주의
빈 User-Agent("")를 차단하면 일부 API 호출이 막힐 수 있다. 브라우저에서 오는 요청은 반드시 User-Agent가 있으니 웹사이트 접근에는 영향 없다. 다만 내부 시스템 간 API 호출이 있다면 User-Agent를 붙여야 한다.
AI 크롤러도 차단했다
GPTBot, ClaudeBot, CCBot 같은 AI 크롤러도 막았다. LLM 학습 데이터 수집용으로 사이트를 긁는 봇들인데, 서버 자원만 쓸 뿐 내 사이트에 돌아오는 건 없다.
마무리
로그 한 번 열어봤다가 트래픽의 30%가 봇인 걸 알게 됐다. 소규모 사이트라도 봇은 생각보다 많이 온다.
nginx map 설정 파일 두 개 추가한 게 전부다. 앱 코드 수정 없음.
작업량: 설정 파일 2개 (~30분) 효과: 불필요한 봇 트래픽 30% 차단, 앱 서버 SSR 부하 감소 유지보수: 새 봇 발견 시 한 줄 추가
서버 로그는 가끔 들여다볼 만하다. 내 사이트에서 무슨 일이 벌어지는지 보면 매번 좀 놀란다.






댓글
댓글을 작성하려면 이 필요합니다.