Nginx 서버에 DDoS 공격이 들어오면 limit_req만으로는 서버가 버티지 못합니다. limit_req는 IP당 초당 요청 수를 제한하는 Nginx 기본 모듈인데, 요청 수신 자체를 막지 않아 대규모 공격 앞에서는 한계가 있습니다.
Ubuntu 22.04 기준, iptables 커넥션 차단 → Fail2Ban 자동 ban → Nginx rate limiting을 3단계로 쌓으면 Nginx 앞단에서 대부분의 DDoS 공격 트래픽을 잘라낼 수 있습니다.
필자의 경우, 이 글을 쓰기 전에 실제 웹 서버에서 DDoS 공격을 받은 뒤 Nginx DDoS 방어 설정, iptables, Fail2Ban을 이용한 방화벽 구축을 적용했으며, 직접 검증한 구성을 공유합니다.
커스텀 로그 포맷 환경에서 발생하는 Fail2Ban failregex 파싱 오류 문제와 v0.9.3 iptables 액션 버그 해결 방법도 함께 정리했으니 서버 상황에 맞게 응용해 보시길 바랍니다.
설정 환경
| 항목 | 버전 / 내용 |
|---|---|
| OS | Ubuntu 22.04 LTS (기준) · 16.04/24.04 별도 명시 |
| Web Server | Nginx |
| 방화벽 | iptables + iptables-persistent |
| 침입 차단 | Fail2Ban 1.0.2 (22.04 기본) · 24.04는 1.1.0 이상 필요 |
| 운영 형태 | 멀티 도메인 단일 서버 |
| Nginx 로그 경로 | /var/www/log/도메인/https-도메인-access.log (커스텀) |
| Nginx 로그 포맷 | 커스텀 포맷 ([날짜] 상태코드 IP - METHOD URL) |
NGINX의 limit_req 설정만으로 DDoS를 막을 수 없는 이유
limit_req는 처리 속도를 제한할 뿐, 요청 수신 자체를 막지 않습니다. 초당 1,000개 요청이 들어와도 Nginx는 TCP 연결을 전부 accept한 뒤 429(Too Many Requests)를 반환합니다.
커넥션 수락과 응답 생성에 CPU와 메모리가 소모되고, 거부된 요청도 access 로그에 전부 기록되므로 디스크 I/O 부하까지 겹칩니다. 충분한 규모의 공격 앞에서는 Nginx DDoS 방어 설정이 켜진 채로 서버가 다운됩니다.
해결은 Nginx 앞단에 방화벽 레이어를 추가하는 것입니다. iptables는 리눅스 커널에 내장된 패킷 필터링 방화벽으로, TCP 연결 자체를 커널 레벨에서 차단하므로 Nginx 프로세스에 부하가 가지 않습니다.
트래픽 처리 순서는 아래와 같습니다.

요청
↓
iptables connlimit (IP당 동시 연결 30개 초과) → REJECT ← 커널 레벨
↓
iptables f2b-nginx-ddos (Fail2Ban IP 차단 목록) → REJECT
↓
nginx limit_req (초당 4req 초과) → status code 429
↓
Fail2Ban (5초 내 15회 429/401/444) → iptables에서 IP 차단 24시간
1. Nginx rate limiting 설정
Nginx 방화벽 역할로 IP 차단을 구성하려면, /etc/nginx/nginx.conf의 http 블록에 limit_req_zone으로 zone을 선언합니다.
limit_req_zone은 어떤 기준으로 요청을 추적할지(key)와 메모리 크기, 허용 속도를 정의하는 디렉티브입니다. $binary_remote_addr은 클라이언트 IP를 바이너리 형식으로 저장해 문자열 방식보다 메모리를 절약합니다.
# http 블록
limit_req_zone $binary_remote_addr zone=ddos_req:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=ddos_conn:10m;
각 도메인의 server 블록에 적용합니다.
# server 블록
limit_conn ddos_conn 20;
limit_req zone=ddos_req burst=10 nodelay;
# WordPress 관리자·REST API 경로 제외
location /wp-admin/ {
limit_req off;
}
location /wp-json/ {
limit_req off;
}
rate=5r/s는 일반 사용자의 평균 초당 요청이 1~2개 수준임을 감안해 여유를 둔 값입니다. 상용 웹사이트에 적용해 테스트한 결과, 정상적인 페이지 접근에서 차단이 발생하지 않았습니다.
순간적으로 IP 접속이 증가하는 상황은 burst=10이 커버합니다. burst=10은 순간 최대 10개 요청을 대기열 없이 즉시 처리하는 버퍼이고, nodelay는 버퍼 초과 시 대기 없이 즉시 429를 반환하는 옵션입니다.
nodelay를 제거하면 초과 요청이 대기열에 쌓이며 처리 지연이 발생합니다. WordPress를 운영하는 경우 /wp-admin과 /wp-json 경로는 반드시 제외해야 합니다. 이유는 아래 섹션에서 추가 설명합니다.
설정 적용은 아래 명령어로 합니다.
service nginx reload
rate와 burst의 차이
Nginx rate limiting에서 rate는 지속 허용량, burst는 순간 허용 버퍼입니다. 두 값의 조합이 실제 DDoS 방어 성능을 결정하며, burst를 활용하면 IP 접속이 순간적으로 증가하는 정상 트래픽을 차단하지 않고 동적 IP 차단 대상만 선별합니다.
rate=5r/s, burst=10 nodelay로 설정하면 평상시 초당 5개 요청을 처리하고, 순간적으로 최대 15개(5 + burst 10)까지 허용합니다. 15개를 초과하면 즉시 429를 반환합니다. 정상 사용자도 페이지 하나를 로드할 때 동시 요청이 몰릴 수 있으므로, burst가 없으면 정상 트래픽도 차단됩니다.
rate=20r/s만 설정하고 burst를 두지 않으면 공격자가 초당 19개로 꾸준히 요청해도 차단되지 않습니다. rate가 높아질수록 지속 공격에 취약해집니다.
| 설정 | 허용 패턴 | 취약점 |
|---|---|---|
rate=5r/s (burst 없음) | 초당 5개 엄격 제한 | 정상 사용자 차단 가능성, WordPress 오류 |
rate=5r/s burst=10 nodelay | 순간 15개, 지속 5r/s | 없음 (권장 구성) |
rate=20r/s (burst 없음) | 초당 20개 | 19r/s 지속 공격 통과 |
burst 값은 정상 사용자의 최대 동시 요청 수 기준으로 설정합니다. 일반 블로그는 burst=10, WordPress 멀티 블록 에디터(Gutenberg) 환경은 burst=15 이상이 안전합니다.
WordPress 적용 시 Gutenberg 저장 오류 문제
limit_req를 전역 적용하면 WordPress Gutenberg 에디터에서 게시물 저장 시 연결 오류가 발생합니다.
Gutenberg는 저장 버튼 클릭 한 번에 REST API(/wp-json/)로 여러 요청을 동시에 보내는 구조이기 때문에, burst 없이 rate=5r/s만 설정한 상태라면 이 동시 호출이 즉시 429로 막힙니다.
해결은 두 가지입니다. /wp-admin과 /wp-json 경로를 limit_req off로 완전히 제외하거나, 해당 경로에만 더 큰 burst를 별도 적용합니다.
# 방법 1: 경로 제외 (권장)
location /wp-admin/ {
limit_req off;
}
location /wp-json/ {
limit_req off;
}
# 방법 2: 경로별 burst 확장 (제외 대신 완화)
location /wp-json/ {
limit_req zone=ddos_req burst=15 nodelay;
}
/wp-admin은 로그인 후 관리자만 접근하므로 rate limit 제외로 인한 공격 노출 위험이 낮습니다.
/wp-json은 공개 엔드포인트가 포함되어 있어 공격 표면이 우려된다면, 방법 2로 burst만 늘리는 쪽이 더 안전합니다.
크롤러 및 봇 스캔 차단
공격 규모가 크지 않은 크롤러나 자동화 봇은 Nginx location 블록에서 직접 차단합니다. Nginx 방화벽 역할로 특정 확장자나 엔드포인트를 아예 막아두면 Fail2Ban이 감지하기 전에 선제적으로 Nginx IP 차단이 이루어집니다.
# server 블록
location ~* \.(env|php|asp|aspx|jsp)$ {
deny all; # 접근 차단
}
# WordPress REST API 봇 차단 예시
location = /wp-json/oembed/1.0/embed {
limit_req zone=ddos_req burst=2 nodelay;
}
AWS, GCP, Azure 같은 클라우드 서비스 IP 대역에서 공격이 들어오는 경우, IP 자체를 수동으로 차단하는 것이 빠릅니다. 클라우드 사업자는 공식적으로 IP 대역 목록을 공개하므로 해당 대역을 iptables로 직접 차단할 수 있습니다.
# 특정 IP 대역 차단 예시
iptables -I INPUT -s 1.2.3.0/24 -j DROP
iptables-save > /etc/iptables/rules.v4
2. iptables connlimit으로 동시 연결 제한
iptables의 connlimit 모듈은 IP당 동시 TCP 연결 수를 커널 레벨에서 제한하는 iptables IP 차단 방식입니다.
Nginx가 TCP 핸드셰이크조차 처리하지 않으므로 서버 부하가 최소화됩니다. 브라우저가 동일 도메인에 여는 최대 연결 수는 6~8개 수준이므로, 50개 초과는 자동화된 공격 트래픽으로 판단해 즉시 REJECT합니다.
iptables -I INPUT -p tcp --dport 80 -m connlimit --connlimit-above 50 -j REJECT
iptables -I INPUT -p tcp --dport 443 -m connlimit --connlimit-above 50 -j REJECT
동시 연결 제한이 적용되어 있는지 직접 확인하려면 iptables -L INPUT -n 명령어를 사용합니다. 재부팅 후에도 방화벽 규칙이 유지되도록 iptables-persistent를 설치하고 저장합니다.
iptables-persistent는 현재 iptables 규칙을 /etc/iptables/rules.v4에 저장하고 부팅 시 자동으로 복원하는 패키지입니다.
apt install iptables-persistent
iptables-save > /etc/iptables/rules.v4
참고 : iptables-persistent 설치 중 현재 규칙 저장 여부를 묻는 프롬프트가 나타납니다. 위 명령어로 먼저 저장한 뒤 설치하거나, 설치 중 “Yes”를 선택하면 됩니다.
만일, 값을 수정하고 싶다면 기존에 등록된 규칙을 삭제 후 다시 등록하는 방식으로 처리합니다.
# 기존 규칙 삭제
iptables -D INPUT -p tcp --dport 80 -m connlimit --connlimit-above 50 -j REJECT
iptables -D INPUT -p tcp --dport 443 -m connlimit --connlimit-above 50 -j REJECT
# 100으로 다시 추가
iptables -I INPUT -p tcp --dport 80 -m connlimit --connlimit-above 100 -j REJECT
iptables -I INPUT -p tcp --dport 443 -m connlimit --connlimit-above 100 -j REJECT
# 설정 저장
iptables-save > /etc/iptables/rules.v4
3. Fail2Ban으로 동적 IP 차단 자동화
Fail2Ban은 로그 파일을 실시간으로 감시해 공격 패턴이 감지된 IP를 iptables에 자동으로 추가하는 침입 차단 소프트웨어입니다.
Nginx access 로그에서 429(Too Many Requests)·401(Unauthorized)·444(Nginx 강제 종료) 응답이 5초 내 15회 이상 발생한 IP를 탐지해 24시간 차단합니다.
이처럼 Fail2Ban IP 차단은 수동 개입 없이 반복 공격 IP를 지속적으로 동적으로 차단하며, Nginx IP 차단을 자동화하는 핵심 구성 요소입니다.
Fail2Ban은 HTTP 인증 실패(fail2ban nginx http auth)나 과도한 요청(nginx-ddos) 등 jail 단위로 감시 대상을 분리할 수 있어, 공격 유형별로 다른 차단 조건을 적용하는 것도 가능합니다.
Fail2Ban 설치
apt install fail2ban # 설치
systemctl enable fail2ban # 서비스 등록, 부팅 후 자동 실행
service fail2ban start
Ubuntu 24.04에서는 기본 저장소의 fail2ban 1.0.2-3이 Python 3.12와 충돌해 서비스가 시작되지 않습니다. 이 경우 GitHub 릴리즈에서 1.1.0 이상을 직접 설치해야 합니다.
wget -O /tmp/fail2ban.deb \ https://github.com/fail2ban/fail2ban/releases/download/1.1.0/fail2ban_1.1.0-1.upstream1_all.deb
dpkg -i /tmp/fail2ban.deb
Ubuntu 22.04는 기본 저장소 버전(1.0.2)이 정상 동작합니다.
filter 설정
filter는 Fail2Ban이 로그에서 공격 IP를 추출할 때 사용하는 정규표현식 규칙입니다. /etc/fail2ban/filter.d/nginx-ddos.conf를 생성합니다.
failregex에 정의한 패턴과 일치하는 로그 라인에서 <HOST> 자리의 IP를 추출해 차단 대상으로 등록합니다.
[Definition]
failregex = (401|429|444) <HOST> -
ignoreregex =
커스텀 로그 포맷 주의 : Nginx 로그가 표준 combined 포맷이 아닌 경우 failregex에 ^ 앵커를 사용하면 0 matched가 됩니다. Fail2Ban은 날짜 파싱 후 남은 문자열에 regex를 적용하는 방식이므로, 앵커 없이 IP 뒤 - 패턴으로 파싱 오류를 방지합니다.
표준 combined 포맷 환경에서는 아래처럼 앵커를 포함한 더 엄격한 패턴을 사용할 수 있습니다.
failregex = ^<HOST> .+ (401|429|444) \d+
특정 User-Agent 제외 : ignoreregex는 failregex에 매칭된 로그 라인 중 차단에서 제외할 패턴을 정의합니다. 모니터링 도구, 정상 크롤러, 특정 장치의 User-Agent를 여기에 등록하면 해당 요청은 차단 카운트에서 제외됩니다.
[Definition]
failregex = (401|429|444) <HOST> -
ignoreregex = Googlebot|bingbot|Yeti # 주요 검색 크롤러
UptimeRobot|Pingdom|StatusCake # 모니터링 서비스
iPhone|Android|iPad # 모바일 장치
Mozilla/5\.0 \(compatible # 호환 모드 브라우저
주의할 점은 ignoreregex가 UA 문자열 전체가 아닌 로그 라인 전체에 매칭된다는 것입니다. UA가 로그에 기록되지 않는 포맷이라면 효과가 없으므로, fail2ban-regex 명령어로 실제 로그와 대조해 검증한 뒤 적용합니다.
fail2ban-regex /var/log/nginx/access.log \
/etc/fail2ban/filter.d/nginx-ddos.conf
jail 설정
jail은 Fail2Ban에서 어떤 로그를 감시하고, 어떤 조건일 때, 어떤 액션을 실행할지를 정의하는 단위입니다.
/etc/fail2ban/jail.local에 아래 내용을 추가합니다. jail.local이 없으면 신규 생성합니다.
jail.conf를 직접 수정하면 패키지 업데이트 시 덮어씌워지므로 반드시 .local 파일을 사용합니다.
[DEFAULT]
bantime = 86400 # ban 유지 시간(초), 86400 = 24시간
findtime = 5 # 이 시간(초) 안에 maxretry 횟수를 초과하면 ban
maxretry = 15 # findtime 내 허용 실패 횟수
[nginx-ddos]
enabled = true
filter = nginx-ddos
action = iptables-multiport[name=nginx-ddos, port="80,443", protocol=tcp] logpath = /var/www/log/*/https-*-access.log
logpath는 실제 환경에 맞게 수정합니다. 표준 Nginx 로그 경로는 /var/log/nginx/*access*.log입니다.
Fail2Ban을 재시작해 설정을 적용합니다.
service fail2ban restart
action 선택 : iptables-multiport를 사용합니다. fail2ban v0.9.x에서 iptables 단독 액션은 INPUT chain 연결 버그가 있어 IP 차단이 실제로 적용되지 않습니다. 현재 버전(1.0.x 이상)에서는 두 액션 모두 정상 동작하지만, iptables-multiport가 80/443 포트를 단일 규칙으로 처리해 더 효율적입니다.
Fail2Ban 영구 차단 설정
bantime = -1로 설정하면 Fail2Ban IP 차단이 영구 적용됩니다. jail 전체에 적용하려면 [DEFAULT]에, 특정 jail에만 적용하려면 해당 섹션에 개별 지정합니다.
# 전체 jail 영구 차단
[DEFAULT]
bantime = -1
# nginx-ddos jail에만 영구 차단 적용
[nginx-ddos]
bantime = -1
영구 차단은 오탐 시 수동 해제 전까지 복구되지 않으므로 주의가 필요합니다. 특히 회사, 학교처럼 여러 사용자가 공유 IP를 쓰는 NAT 환경에서는 정상 사용자 전체가 차단될 수 있습니다.
또한 차단 IP는 /var/lib/fail2ban/fail2ban.sqlite3에 누적 저장되므로, 장기 운영 시 DB 크기를 주기적으로 확인하는 것이 좋습니다.
영구 차단 대신 bantime.increment를 사용하면 동일 IP가 반복 차단될수록 차단 시간이 배수로 늘어나는 점진적 차단이 적용됩니다. 오탐 시 자동 해제되면서 실질적으로 영구 차단에 가까운 효과를 냅니다.
[DEFAULT]
bantime = 86400
bantime.increment = true
bantime.factor = 2
위 설정은 1차 차단 24시간, 2차 48시간, 3차 96시간으로 배수 증가합니다.
차단 IP 확인 및 주요 명령어
Nginx 방화벽, iptables, Fail2Ban 각각 차단 IP를 확인하는 방법이 다릅니다.
Nginx 차단 로그 확인
Nginx는 별도의 차단 IP 목록을 관리하지 않고, 429 응답을 access 로그에 기록합니다. 아래 명령어로 차단된 요청을 실시간으로 확인합니다.
# 429 응답만 필터링해 실시간 확인
tail -f /var/log/nginx/access.log | grep " 429 "
# 차단 횟수 상위 IP 집계
awk '$2 == 429 {print $3}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
iptables 차단 IP 확인
iptables는 connlimit 초과 IP를 규칙 단위로 처리하므로 차단 IP 목록 대신 현재 활성 규칙과 Fail2Ban이 추가한 차단 목록을 확인합니다.

# connlimit 규칙 포함 전체 INPUT 체인 확인
iptables -L INPUT -n -v
# Fail2Ban이 추가한 nginx-ddos 차단 IP 목록
iptables -L f2b-nginx-ddos -n
# 수동으로 차단한 IP 대역 확인
iptables -L INPUT -n | grep DROP
Fail2Ban 차단 IP 확인 및 관리
Fail2Ban은 차단 중인 IP 목록을 jail 단위로 관리합니다. 동적 IP 차단 현황은 아래 명령어로 확인합니다.

# jail 전체 현황 (차단 중인 IP 수, 총 차단 횟수)
fail2ban-client status nginx-ddos
# 차단 중인 IP 목록만 출력
fail2ban-client status nginx-ddos | grep "Banned IP"
# 실시간 차단 로그
tail -f /var/log/fail2ban.log
# 수동 차단 / 해제
fail2ban-client set nginx-ddos banip 1.2.3.4
fail2ban-client set nginx-ddos unbanip 1.2.3.4
# filter regex 테스트 (차단 패턴 검증)
fail2ban-regex /var/log/nginx/access.log \
/etc/fail2ban/filter.d/nginx-ddos.conf
# iptables 규칙 백업 / 복원
iptables-save > /tmp/iptables-backup.txt
iptables-restore < /tmp/iptables-backup.txt
트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
failregex 0 matched | 커스텀 로그 포맷, ^ 앵커 문제 | 앵커 제거, IP 뒤 - 패턴 사용 |
iptables action 0 references | fail2ban v0.9.x 버그 | iptables-multiport 액션으로 변경 |
WARNING Unable to find IP for Mozilla | regex가 UA 문자열을 IP로 파싱 | IP 뒤 - 추가로 패턴 엄격화 |
jail.conf만 존재, jail.local 없음 | 기본 설치 상태 | jail.local 신규 생성해 오버라이드 |
| Ubuntu 24.04에서 fail2ban 시작 실패 | Python 3.12 비호환(asynchat 제거) | fail2ban 1.1.0 이상으로 업그레이드 |
| Gutenberg 저장 시 연결 오류 | REST API 동시 호출이 burst 초과 | /wp-admin, /wp-json에 limit_req off 추가 |
마치며
limit_req 단독으로는 커넥션 수락 부하를 막지 못합니다. iptables IP 차단으로 커널 레벨 연결을 선제 차단하고, Fail2Ban IP 차단으로 반복 공격 IP를 24시간 동적으로 차단하면 Nginx가 실제로 처리하는 트래픽 양이 크게 줄어듭니다.
커스텀 로그 포맷 환경이라면 failregex 앵커 제거와 - 패턴 추가가 핵심입니다. jail.local 없이 jail.conf만 수정하면 패키지 업데이트 때마다 설정이 초기화되므로, 처음부터 .local 파일로 관리하는 습관이 필요합니다.
FAQ
bantime을 -1로 설정하면 어떻게 되나요?
영구 차단됩니다. 공격이 완전히 가라앉을 때까지 임시로 사용할 수는 있지만, 정상 사용자가 같은 IP를 재사용하는 경우 수동 차단이 필요합니다. 일반적으로 86400(24시간) 이상으로 설정하고 재발 시 수동 처리하는 방식이 관리하기 편합니다.
iptables-persistent를 설치해도 iptables 규칙이 재부팅 후 사라져요.
iptables-persistent는 설치 시점의 규칙을 /etc/iptables/rules.v4에 저장하고 이를 부팅 시 복원합니다. 규칙을 추가·변경한 뒤 iptables-save > /etc/iptables/rules.v4를 실행하지 않으면 변경 사항이 영구 저장되지 않습니다.
Fail2Ban이 차단한 IP가 VPN이나 프록시를 통해 우회하면 차단이 무효화되나요?
공격자가 IP를 교체하면 해당 차단 규칙은 무의미해집니다. 이 구성은 동일 IP의 지속 요청을 차단하는 데 최적화되어 있습니다. 대규모 분산 공격(진짜 DDoS)에는 Cloudflare WAF나 CDN 레벨 차단을 병행해야 합니다.



