본문 바로가기

IT/Cloud

Nginx Rate Limiting으로 DDoS 공격 막기

문제 상황

서비스를 운영하다 보면 예상치 못한 트래픽 급증이나 악의적인 공격에 직면할 수 있다.
실제로 우리 서비스도 특정 API 엔드포인트에 대량의 요청이 몰리면서 서버가 불안정해지는 경험을 했다.

사진을 보면 같은 API에 대해 초당 수십 건의 요청이 들어오고 있었다. 정상적인 사용 패턴이 아닌 건 명백했다.

이런 상황에서 가장 먼저 떠올릴 수 있는 해결책은 Cloudflare 같은 CDN/WAF 서비스를 도입하는 것이다. 하지만 지금 당장 문제를 해결

해야 하는 상황이라면? Nginx의 Rate Limiting 기능을 활용하면 빠르게 대응할 수 있다.


Rate Limiting이란?

Rate Limiting은 특정 시간 동안 허용되는 요청 수를 제한하는 기법이다. 예를 들어:

  • IP당 초당 10개 요청만 허용
  • 동시 접속은 IP당 최대 10개까지
  • 초과하는 요청은 429 (Too Many Requests) 에러 반환

이를 통해 서버 리소스를 보호하고, 악의적인 트래픽을 차단할 수 있다.


Nginx Rate Limiting 적용하기

우리 서비스는 Nginx를 리버스 프록시로 사용하고 있어서, 애플리케이션 레벨 수정 없이 설정만으로 Rate Limiting을 적용할 수 있었다.

1. Rate Limiting Zone 정의

먼저 Nginx 설정 파일 상단에 Rate Limiting을 위한 존(zone)을 정의한다.

# API 전체에 대한 Rate Limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=50r/s;

# 동시 접속 수 제한
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

 

각 설정의 의미:

  • $binary_remote_addr: 클라이언트 IP 주소 (바이너리 형식으로 메모리 절약)
  • zone=api_limit:10m: "api_limit"이라는 이름의 존을 생성하고, 10MB 메모리 할당 (약 160,000개의 IP 추적 가능)
  • rate=50r/s: 초당 50개 요청으로 제한

2. DDoS 방어 기본 설정

서버 블록에 타임아웃 관련 설정을 추가해서 느린 공격(Slow HTTP Attack)도 방어한다.

server {
    listen 443 ssl;
    server_name example.com

    # DDoS 방어 기본 설정
    client_body_timeout 10s;      # 요청 본문 전송 타임아웃
    client_header_timeout 10s;    # 헤더 전송 타임아웃
    keepalive_timeout 5s 5s;      # Keep-Alive 연결 타임아웃
    send_timeout 10s;             # 응답 전송 타임아웃

    # ... SSL 설정 등
}

3. Location별 Rate Limiting 적용

이제 실제로 각 엔드포인트에 Rate Limiting을 적용한다.

# 일반 API 엔드포인트
location /api/ {
    # Rate Limiting 적용
    limit_req zone=api_limit burst=100 nodelay;
    limit_conn conn_limit 10;

    # 429 에러 커스텀 응답
    limit_req_status 429;

    proxy_pass http://example;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # 타임아웃 설정
    proxy_connect_timeout 5s;
    proxy_send_timeout 10s;
    proxy_read_timeout 10s;
}

 

주요 설정 설명:

  • limit_req zone=api_limit burst=100 nodelay
    • api_limit 존 사용
    • burst=100: 순간적으로 최대 100개까지 버스트 허용
    • nodelay: 초과 요청을 큐에 넣지 않고 즉시 처리 또는 거부
  • limit_conn conn_limit 10
    • IP당 동시 접속 10개로 제한
  • limit_req_status 429
    • 제한 초과 시 429 상태 코드 반환

4. 커스텀 에러 페이지

Rate Limit에 걸린 사용자에게 친절한 안내 메시지를 제공한다.

# 429 에러 페이지
error_page 429 /429.json;

location = /429.json {
    internal;
    default_type application/json;
    return 429 '{"error":"Too many requests","message":"Please try again later"}';
}

전체 설정 예시

최종적으로 우리 서비스에 적용한 전체 설정은 다음과 같다.

# Rate Limiting Zone 정의
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

upstream example {
    server 127.0.0.1:????;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    client_max_body_size 500M;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    resolver 8.8.8.8 1.1.1.1 valid=30s ipv6=off;

    # DDoS 방어 기본 설정
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 5s 5s;
    send_timeout 10s;

    # ... FrontEnd 설정 등

    # 백엔드 API
    location /api/ {
        # Rate Limiting 적용
        limit_req zone=api_limit burst=20 nodelay;
        limit_req_status 429;

        proxy_pass http://example;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 타임아웃 설정
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 10s;
    }

    # 429 에러 페이지
    error_page 429 /429.json;
    location = /429.json {
        internal;
        default_type application/json;
        return 429 '{"error":"Too many requests","message":"Please try again later"}';
    }
}

테스트

Apache Bench(ab)를 사용해서 실제로 Rate Limiting이 작동하는지 테스트했다.

# 45개 요청을 15개 동시 접속으로 테스트
ab -n 45 -c 15 https://example.com/api/@@@@@@

 

테스트 결과:

Server Software:        nginx/1.24.0
Server Hostname:        example.com
Server Port:            443

Document Path:          /api/@@@@@@@@
Document Length:        3660 bytes

Concurrency Level:      15
Time taken for tests:   0.429 seconds
Complete requests:      45
Failed requests:        33        ← Rate Limit으로 차단된 요청
   (Connect: 0, Receive: 0, Length: 33, Exceptions: 0)
Non-2xx responses:      33        ← 429 에러 응답
Total transferred:      58533 bytes

Requests per second:    104.88 [#/sec] (mean)
Time per request:       143.021 [ms] (mean)

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       23   46  16.9     39      76
Processing:     5   40  51.0     10     143
Waiting:        5   40  51.0     10     142
Total:         29   86  59.5     58     211

 

결과 분석:

  • 전체 45개 요청 중 12개만 성공 (200 OK)
  • 나머지 33개는 429 에러 (Too Many Requests)
  • Rate Limiting이 정상적으로 작동

이는 우리가 테스트 설정 rate=5r/s, burst=10이 짧은 시간(0.429초)에 몰린 45개의 동시 요청을 적절히 제어했다는 의미다.


효과 측정

Rate Limiting 적용 전후를 비교해보면 확실한 개선을 확인할 수 있다.

Before (적용 전)

동시 대량 요청 시:
- 모든 요청이 서버까지 도달
- CPU 사용률 급증 (70~95%)
- 응답 시간 지연 (평균 300ms → 최대 2초)
- 정상 사용자도 영향 받음

After (적용 후)

동시 대량 요청 시:
- 초과 요청은 Nginx에서 즉시 차단 (429 반환)
- 서버는 허용된 요청만 처리
- CPU 사용률 안정적 유지 (30~50%)
- 정상 사용자는 영향 없음

실제 테스트 결과:

45개 동시 요청 발생 시:
- ✅ 12개 정상 처리 (200 OK)
- ❌ 33개 즉시 차단 (429 Too Many Requests)
- 처리 시간: 평균 86ms (매우 빠름)

서버 리소스 보호:

  • Nginx가 앞단에서 차단하므로 백엔드는 과부하 방지
  • 데이터베이스 커넥션 낭비 방지
  • 메모리 사용량 안정적 유지

악의적인 트래픽이 차단되면서 서버 리소스가 정상 사용자를 위해 사용되었고, 전체적인 안정성이 크게 개선되었다.


주의사항 및 팁

1. 너무 엄격한 제한은 피하자

Rate Limiting을 너무 빡빡하게 설정하면 정상 사용자도 불편을 겪을 수 있다. 우리 서비스의 경우:

  • 일반 API: 초당 50개 (burst 100)

이 정도면 정상적인 사용에는 문제가 없으면서도 공격은 효과적으로 막을 수 있었다.

2. 부하 테스트로 적정값 찾기

설정 적용 전에 부하 테스트로 적절한 값을 찾는 것이 중요하다.

# Apache Bench로 테스트
ab -n 45 -c 15 https://example.com/api/@@@@@@

# 결과 확인:
# - 정상 사용자 패턴에서 429 에러가 거의 없어야 함
# - 공격 패턴에서는 대부분 429 에러가 발생해야 함

 

실제 테스트 예시:

45개 요청, 15개 동시 접속:
- 성공: 12개 (27%)
- 차단: 33개 (73%)
→ Rate Limiting이 효과적으로 작동 ✅

3. 화이트리스트 적용

특정 IP (예: 사내망, 모니터링 도구)는 Rate Limiting에서 제외할 수 있다.

geo $limit {
    default 1;
    # 화이트리스트 IP
    10.0.0.0/8 0;
    192.168.0.0/16 0;
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=api_general:10m rate=50r/s;

하지만 아직 적용은 안 했다

4. 엔드포인트별 차등 적용

모든 API에 동일한 제한을 적용할 필요는 없다. 중요도와 트래픽 패턴에 따라 차등 적용하자.

# 읽기 API - 관대하게
location /api/read {
    limit_req zone=api_general burst=100 nodelay;
    # ...
}

# 쓰기 API - 엄격하게
location ~ ^/api/write$ {
    if ($request_method = POST) {
        limit_req zone=api_heavy burst=20 nodelay;
    }
    # ...
}

다음 단계: Cloudflare 도입

Nginx Rate Limiting은 즉각적이고 효과적인 해결책이지만, 더 강력한 보호를 위해서는 Cloudflare 같은 전문 서비스 도입을 고려해야 한다.

 

Nginx vs Cloudflare

항목 Nginx Cloudflare
적용 속도 5분 5분
비용 무료 무료~유료
DDoS 방어 규모 서버 수준 Tbps 급
관리 편의성 수동 설정 자동화된 룰
글로벌 CDN

현재는 Nginx로 충분하지만, 트래픽이 더 증가하거나 대규모 DDoS 공격에 대비하려면 Cloudflare 도입을 검토할 예정이다.


마무리

Rate Limiting은 작은 설정으로 큰 효과를 볼 수 있는 기법이다.

특히 Nginx를 이미 사용하고 있다면, 설정 파일 몇 줄만 추가해도 서버를 보호할 수 있다.

물론 완벽한 해결책은 아니다. 고도화된 DDoS 공격이나 분산 공격에는 Cloudflare, AWS Shield 같은 전문 서비스가 필요하다.

하지만 지금 당장 문제를 해결해야 한다면, Nginx Rate Limiting은 가장 빠르고 효과적인 선택지다.

우리 서비스도 이 설정만으로 즉각적인 효과를 봤고, 서버 안정성이 크게 개선되었다.

여러분도 비슷한 상황이라면 한번 시도해보길 추천한다!