왜 rate limiting이 필요한가
특정 클라이언트가 초당 수백 건씩 요청을 쏟아내면 백엔드 커넥션 풀과 DB가 먼저 고갈된다. 애플리케이션 레벨에서 Redis 카운터로 막을 수도 있지만, 이 경우 요청이 이미 애플리케이션 스레드를 점유한 뒤에 거절된다. 로드밸런서 단에서 걸러내면 비정상 트래픽이 백엔드에 도달하기 전에 차단되므로, 실제로 보호가 필요한 자원을 지킬 수 있다. HAProxy의 스틱 테이블(stick table)은 별도 저장소 없이 이 기능을 제공한다.
스틱 테이블의 동작 원리
스틱 테이블은 키(주로 소스 IP)를 기준으로 카운터를 유지하는 인메모리 자료구조다. http_req_rate, conn_cur 같은 데이터 타입을 지정하면 HAProxy가 슬라이딩 윈도우 방식으로 요청 빈도를 자동 집계한다. 예를 들어 http_req_rate(10s)는 최근 10초 동안의 요청 수를 실시간으로 계산한다. 테이블은 정해진 크기와 만료 시간을 가지며, 만료된 엔트리는 자동으로 제거된다.
기본 IP 기반 rate limiting 설정
frontend에 테이블을 선언하고, track-sc0로 요청마다 소스 IP를 추적한 뒤 임계치를 넘으면 거절한다.
frontend web
bind *:80
# 100k 엔트리, 10초 윈도우, 30초 미사용 시 만료
stick-table type ip size 100k expire 30s store http_req_rate(10s)
# 모든 요청의 소스 IP를 추적
http-request track-sc0 src
# 10초당 100건 초과 시 429 반환
http-request deny deny_status 429 \
if { sc_http_req_rate(0) gt 100 }
default_backend app_servers
여기서 sc0는 sticky counter 슬롯 번호이며 sc0~sc2까지 동시에 쓸 수 있다. 서로 다른 기준(IP, URL, 쿠키)을 병렬로 추적할 때 유용하다.
어떤 데이터 타입을 쓸 것인가
공격 유형에 따라 추적할 지표가 다르다. 아래는 자주 쓰는 조합이다.
| 데이터 타입 | 측정 대상 | 주 용도 |
|---|---|---|
| http_req_rate(p) | 기간 내 HTTP 요청 수 | 일반적인 요청 폭주 차단 |
| http_err_rate(p) | 4xx 등 에러 응답 빈도 | 스캐닝·브루트포스 탐지 |
| conn_cur | 현재 동시 커넥션 수 | slowloris형 커넥션 고갈 방지 |
| bytes_out_rate(p) | 기간 내 전송 바이트 | 대용량 다운로드 남용 제어 |
차단 대신 지연시키기(tarpit)
즉시 거절하면 공격자가 재시도를 반복한다. tarpit은 커넥션을 붙잡아 두었다가 끊어 공격 도구의 효율을 떨어뜨린다. 또한 에러율 기반으로 로그인 브루트포스를 걸러낼 수 있다.
frontend web
stick-table type ip size 100k expire 60s \
store http_req_rate(10s),http_err_rate(60s)
http-request track-sc0 src
# 1분간 4xx가 20건 넘으면 tarpit으로 붙잡음
http-request tarpit \
if { sc_http_err_rate(0) gt 20 }
운영 중 상태 확인
Runtime API로 현재 테이블 내용을 조회하면 어떤 IP가 임계치에 근접했는지 실시간으로 볼 수 있다.
# 소켓 활성화가 전제 (global에 stats socket 설정)
echo "show table web" | socat stdio /var/run/haproxy.sock
# 특정 임계치 이상 IP만 필터
echo "show table web data.http_req_rate gt 80" \
| socat stdio /var/run/haproxy.sock
# 오탐 IP 수동 해제
echo "clear table web key 203.0.113.10" \
| socat stdio /var/run/haproxy.sock
실무에서 놓치기 쉬운 주의점
첫째, src는 TCP 소스 IP다. 앞단에 CDN이나 또 다른 프록시가 있으면 모든 요청이 소수의 프록시 IP로 보여 오탐이 난다. 이 경우 src 대신 req.hdr(X-Forwarded-For)를 키로 쓰되, 신뢰할 수 있는 프록시가 넣은 값인지 반드시 검증해야 한다. 둘째, 스틱 테이블은 프로세스 로컬 메모리다. HAProxy 인스턴스를 여러 대 두면 카운터가 분산되므로 peers 섹션으로 테이블을 동기화하거나, 실효 임계치를 인스턴스 수로 나눠 설정해야 한다. 셋째, size는 넉넉히 잡아야 한다. 테이블이 가득 차면 새 엔트리가 밀려나면서 추적이 무력화된다. IP당 약 50바이트를 기준으로 예상 동시 클라이언트 수의 몇 배로 잡는 것이 안전하다. 마지막으로, 임계치는 반드시 실제 정상 트래픽 분포를 로그로 측정한 뒤 정하라. 감으로 정한 값은 정상 사용자를 막거나 공격을 통과시킨다.