Quản trị VPS

Hardening Nginx: Cấu hình Rate Limiting chống DoS và bot crawl cạn kiệt tài nguyên

Hướng dẫn cấu hình Rate Limiting trên Nginx: nguyên lý rate, burst và nodelay, cách bảo vệ endpoint tìm kiếm, trả mã lỗi 429 và thử tải bằng ApacheBench.

Cấu hình Rate Limiting trên Nginx chống DoS và bot crawl cạn RAM
MỤC LỤC

Một trang web tĩnh phục vụ file HTML hoặc ảnh chỉ tiêu tốn khoảng vài mili-giây xử lý của CPU. Ngược lại, một yêu cầu gửi vào ô tìm kiếm sản phẩm, lọc danh mục hay gửi mã xác thực SMS đòi hỏi ứng dụng phải chạy qua nhiều tầng xử lý: kết nối cơ sở dữ liệu, chạy các câu truy vấn phức tạp và phân bổ bộ nhớ cho worker. Chỉ cần một con bot cào dữ liệu quét qua vài chục trang tìm kiếm trong một giây, lượng worker của PHP-FPM hay Node.js có thể bị vét cạn, đẩy CPU máy chủ lên 100% và khiến những người dùng khác nhận lỗi 504 Gateway Timeout do hàng đợi backend bị nghẽn (xem thêm cách chẩn đoán tại bài Website chậm nhưng CPU thấp: kiểm tra bottleneck ở đâu).

Nginx cung cấp module có sẵn ngx_http_limit_req_module để kiểm soát tần suất gửi yêu cầu của từng địa chỉ IP. Để tính năng này bảo vệ được máy chủ mà không gây gián đoạn trải nghiệm của khách hàng thật, người quản trị cần nắm rõ cách phối hợp giữa ba tham số: rate, burst và nodelay.

Cơ chế điều tiết lưu lượng với Nginx Rate Limiting

Luồng xử lý yêu cầu qua bộ kiểm tra rate, bộ đệm burst nodelay và phản hồi kết quả.

Cơ chế tính toán thời gian của Nginx Rate Limiting

Khi khai báo tham số rate=5r/s, Nginx không đếm dồn số lượng theo từng giây tròn. Hệ thống chia đều thời gian xử lý: với mức 5r/s, cứ mỗi 200 mili-giây (1000ms chia 5) Nginx mới cho phép một request từ địa chỉ IP đó đi qua.

Khi một người dùng mở trang web, trình duyệt sẽ gửi dồn dập từ 5 đến 10 request cùng lúc để tải đồng thời file HTML, ảnh và các tệp mã nguồn. Do các kết nối này đến gần như cùng một thời điểm trong vài mili-giây đầu tiên, chỉ có request đầu tiên được tiếp nhận. Những request tiếp theo sẽ bị Nginx từ chối lập tức nếu không có bộ đệm.

Một cấu hình Rate Limiting hoạt động chính xác luôn cần sự kết hợp của ba thành phần:

  1. rate (Hạn mức cơ sở): Xác định tần suất trung bình cho phép trong điều kiện duyệt web thông thường, ví dụ rate=5r/s (5 request/giây) cho toàn trang hoặc rate=10r/m (10 request/phút) cho các trang gửi mã OTP hay đăng nhập.
  2. burst (Bộ đệm vượt ngưỡng): Cho phép một IP gửi dồn dập một lượng request nhất định vượt quá hạn mức rate trong khoảng thời gian ngắn mà không bị báo lỗi. Ví dụ cấu hình burst=10, Nginx sẽ tiếp nhận nhanh 10 request tải trang ban đầu của trình duyệt.
  3. nodelay (Xử lý ngay bộ đệm): Nếu chỉ có burst mà thiếu nodelay, Nginx sẽ ép các request trong bộ đệm phải chờ lần lượt theo đúng nhịp 200 mili-giây. Điều này khiến trình duyệt người dùng bị trễ nhân tạo và có cảm giác website tải rất chậm. Chỉ thị nodelay yêu cầu Nginx xử lý ngay lập tức toàn bộ các request hợp lệ nằm trong khoảng burst, đồng thời vẫn trừ dần hạn mức để chặn đứng các request tiếp theo nếu tiếp tục gửi dồn dập.

Khai báo vùng nhớ limit_req_zone

Toàn bộ thông tin theo dõi trạng thái của các địa chỉ IP được lưu trữ trong một vùng nhớ chia sẻ (shared memory zone). Vùng nhớ này phải được khai báo trong khối http của file cấu hình Nginx (thường tại /etc/nginx/nginx.conf hoặc /etc/nginx/conf.d/rate-limit.conf):

nginx
http {
    # 1. Giới hạn toàn trang: 10 request/giây, vùng nhớ 10MB
    limit_req_zone $binary_remote_addr zone=general_limit:10m rate=10r/s;

    # 2. Giới hạn chặt chẽ cho endpoint nặng (tìm kiếm, đăng nhập): 2 request/giây
    limit_req_zone $binary_remote_addr zone=search_limit:10m rate=2r/s;

    # Đổi mã lỗi trả về sang 429 Too Many Requests thay vì mặc định 503
    limit_req_status 429;

    # Mức độ ghi log khi có IP vi phạm
    limit_req_log_level warn;
}

Trong cấu hình trên, biến $binary_remote_addr được sử dụng thay cho biến $remote_addr vì lý do tối ưu bộ nhớ:

  • $remote_addr lưu địa chỉ IP dưới dạng chuỗi văn bản, tiêu tốn từ 7 đến 15 byte cho IPv4 và tới 39 byte cho IPv6.
  • $binary_remote_addr lưu IP dưới dạng nhị phân, chỉ tốn cố định 4 byte cho IPv4 và 16 byte cho IPv6.
  • Với kích thước vùng nhớ 10m (10 Megabyte), hệ thống có thể lưu trữ trạng thái của khoảng 160.000 địa chỉ IP đồng thời, hoàn toàn đủ cho các website có lưu lượng truy cập lớn.

Chỉ thị limit_req_status 429; là một tinh chỉnh quan trọng. Mặc định, Nginx trả về mã lỗi 503 Service Temporarily Unavailable khi một kết nối bị giới hạn. Mã 503 dễ gây hiểu lầm rằng ứng dụng backend (như PHP-FPM hay cơ sở dữ liệu) đang bị sập. Đổi sang mã chuẩn 429 Too Many Requests giúp các công cụ cào dữ liệu hợp chuẩn (như Googlebot) tự động hiểu rằng chúng cần giảm tần suất gửi yêu cầu, đồng thời giúp đội ngũ vận hành phân biệt chính xác sự cố hạ tầng với việc kích hoạt luật giới hạn.

Cấu hình phân tầng trong Virtual Host

Sau khi khai báo vùng nhớ trong khối http, bạn áp dụng các hạn mức này vào từng khối server và location cụ thể bên trong file cấu hình website (ví dụ /etc/nginx/sites-available/tenmien.com):

nginx
server {
    listen 443 ssl http2;
    server_name example.com;

    # Áp dụng giới hạn chung cho toàn bộ website
    # Cho phép vượt ngưỡng tối đa 20 request tải trang ban đầu
    limit_req zone=general_limit burst=20 nodelay;

    # Bảo vệ riêng endpoint tìm kiếm và lọc dữ liệu phức tạp
    location /search {
        limit_req zone=search_limit burst=5 nodelay;
        try_files $uri /index.php?$args;
    }

    # Bảo vệ trang đăng nhập chống tấn công dò mật khẩu
    location = /wp-login.php {
        limit_req zone=search_limit burst=3 nodelay;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Các tệp tĩnh không cần áp dụng rate limit để tối đa tốc độ tải trang
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

Với cấu hình phân tầng này:

  • Toàn bộ website được bảo vệ ở mức vừa phải (10r/s kèm burst=20 nodelay), đủ cho người dùng lướt web bình thường mà không bao giờ gặp lỗi.
  • Riêng đường dẫn /search và /wp-login.php được siết chặt ở mức 2r/s kèm burst=5. Nếu một bot tự động gửi liên tục 10 truy vấn tìm kiếm trong 1 giây, chỉ 5 truy vấn đầu tiên được xử lý, 5 truy vấn còn lại sẽ nhận ngay mã phản hồi 429 mà không thể làm nghẽn CPU của VPS.
  • Các tệp tĩnh (ảnh, CSS, font) nằm ở location riêng không bị tính vào hạn mức, giúp trang hiển thị trọn vẹn.

Lưu ý bắt buộc khi máy chủ đứng sau Reverse Proxy

Nếu website của bạn đang sử dụng Cloudflare Proxy hoặc một hệ thống cân bằng tải phía trước (như đã thiết lập trong bài về bảo vệ VPS bằng Cloudflare Authenticated Origin Pulls), biến $binary_remote_addr mặc định sẽ lấy địa chỉ IP của máy chủ proxy chứ không phải IP của khách truy cập.

Hậu quả là hàng nghìn người dùng truy cập qua cùng một IP của Cloudflare sẽ bị Nginx tính chung vào một hạn mức rate limit duy nhất, dẫn đến việc toàn bộ website bị khóa hàng loạt.

Để khắc phục, bạn bắt buộc phải cấu hình module real_ip trước khối rate limit để Nginx phân giải đúng địa chỉ IP thực tế của client:

nginx
# Đặt trong khối http hoặc server
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# (Khai báo đầy đủ các dải IP của proxy)

real_ip_header CF-Connecting-IP;

Khi Nginx đã phục hồi đúng IP gốc, biến $binary_remote_addr sẽ lưu trữ địa chỉ IP thực của người dùng, đảm bảo việc giới hạn diễn ra chính xác cho từng cá nhân riêng biệt.

Kiểm tra cấu hình và thử tải thực tế

Sau khi chỉnh sửa cấu hình, luôn kiểm tra tính hợp lệ của cú pháp trước khi nạp lại dịch vụ:

bash
sudo nginx -t

Nếu kết quả thông báo thành công, tiến hành nạp lại Nginx mà không làm gián đoạn các kết nối đang chạy:

bash
sudo systemctl reload nginx

Để kiểm tra xem cấu hình giới hạn có hoạt động đúng như mong đợi hay không, bạn sử dụng công cụ kiểm thử tải dòng lệnh ab (ApacheBench) từ một máy tính khác:

bash
# Gửi 20 request tới trang tìm kiếm, thực hiện đồng thời 5 kết nối
ab -n 20 -c 5 https://example.com/search?q=test

Đầu ra của lệnh ab sẽ cung cấp bảng phân tích mã phản hồi:

text
Complete requests:      20
Failed requests:        13
   (Connect: 0, Receive: 0, Length: 13, Exceptions: 0)
Non-2xx responses:      13

Trong 20 request gửi đi liên tiếp:

  • 7 request đầu tiên (gồm hạn mức cơ sở và bộ đệm burst) nhận về mã phản hồi 200 OK.
  • 13 request còn lại gửi dồn dập sau đó lập tức nhận về mã phản hồi 429 Too Many Requests.

Bạn cũng có thể theo dõi trực tiếp các kết nối bị chặn trong file error log của Nginx:

bash
sudo tail -f /var/log/nginx/error.log

Dòng log ghi nhận vi phạm sẽ có dạng:

text
2026/10/19 06:20:00 [warn] 1420#1420: *582 limiting requests, excess: 5.210 by zone "search_limit", client: 203.0.113.88, server: example.com, request: "GET /search?q=test HTTP/2.0"

Nếu phát hiện một địa chỉ IP cố tình spam request với cường độ cao liên tục sau khi đã nhận mã 429, thay vì để Nginx tiếp tục tốn chu kỳ xử lý phản hồi, bạn có thể kết hợp thêm cấu hình Fail2ban trên Linux với nftables để tự động cấm triệt để IP đó ở tầng mạng.

Checklist kiểm tra an toàn trước khi vận hành

  1. Vùng nhớ limit_req_zone sử dụng biến $binary_remote_addr để tiết kiệm bộ nhớ RAM.
  2. Mọi chỉ thị limit_req đều đi kèm tham số burst và nodelay để không chặn nhầm trình duyệt người dùng thật.
  3. Đã cấu hình limit_req_status 429; để trả về đúng chuẩn HTTP status code.
  4. Các endpoint nặng (tìm kiếm, đăng nhập, API xuất báo cáo) đã được phân tách hạn mức riêng so với toàn trang.
  5. Nếu website đứng sau CDN Proxy, đã cấu hình đầy đủ module real_ip trước khi áp dụng rate limit.
  6. Đã kiểm tra file error.log sau 24 giờ vận hành đầu tiên để đảm bảo không có cảnh báo hạn mức đối với các luồng thao tác nghiệp vụ thông thường.
Nội dung hướng dẫn này có hữu ích với hệ thống của bạn không?

Chia sẻ bài viết hữu ích

Gửi bài viết này cho đồng nghiệp

Đọc tiếp

Bài nên đọc tiếp

Xem tất cả bài về Quản trị VPS