nginx로 로그인 무차별 대입 줄이기: limit_req 시험 기록
로그인과 가입 요청만 제한하는 설정과 실제로 429가 나오는지 시험한 결과를 정리했습니다.
점검 중에 로그인 시도 횟수 제한이 어디에도 없다는 것을 확인했습니다. 애플리케이션을 고치기 전에 nginx에서 먼저 막으면 앱까지 요청이 가지 않아 작은 서버에 특히 유리합니다.
GET은 두고 POST만 제한하기
가입 주소는 GET과 POST를 함께 씁니다. limit_req의 키가 빈 값이면 제한하지 않는 점을 이용하면 POST만 고를 수 있습니다.
map $request_method $signup_key {
POST $binary_remote_addr;
default "";
}
limit_req_zone $binary_remote_addr zone=login:10m rate=20r/m;
limit_req_zone $signup_key zone=signup:10m rate=5r/m;
limit_req_status 429;기본 상태 코드는 503인데, 서버 장애와 구분되도록 429로 바꿨습니다.
burst와 nodelay
location = /api/user {
limit_req zone=signup burst=5 nodelay;
proxy_pass http://127.0.0.1:8080;
}burst는 잠깐 몰리는 요청을 허용하는 여유분이고, nodelay는 그 요청을 대기시키지 않고 바로 처리합니다. 없으면 사용자가 느린 사이트로 느낍니다.
값을 어떻게 정했나
같은 공유기나 통신사 뒤의 사용자는 IP를 나눠 쓸 수 있습니다. 너무 낮추면 정상 사용자가 막히므로 로그인은 분당 20회에 여유 20회로 시작했습니다. 공격을 완전히 막는 값은 아니고 속도를 크게 늦추는 값입니다.
실제로 시험하기
설정 파일이 맞는지만 보면 부족합니다. 격리된 임시 nginx를 다른 포트로 띄우고 요청을 연속으로 보내 상태 코드를 셌습니다.
for i in $(seq 1 30); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://127.0.0.1:18999/login
done | sort | uniq -c로그인 30회 중 21회가 백엔드로 전달되고 9회가 429였습니다. 가입은 12회 중 6회가 429였고, 같은 주소의 GET은 12회 모두 제한되지 않아 의도대로 동작했습니다.
한계
- 여러 IP로 나눠 들어오는 공격에는 약합니다.
- 프록시나 CDN 뒤에 있으면 실제 사용자 IP를 복원해야 합니다.
- 계정별 실패 횟수 제한과 함께 쓰면 더 좋습니다.