Nginxのngx_http_limit_conn_moduleとngx_http_limit_req_moduleで大量リクエスト制限をする
Nginxのngx_http_limit_conn_module
とngx_http_limit_req_module
を使用すると、Dos攻撃等による大量リクエストがあった際に制限をかけられるとのことで、試してみたいと思います。
ngx_http_limit_conn_module
では同時接続数の制限、ngx_http_limit_req_module
では時間当たりのリクエスト制限のようです。
今回もdocker-composeを使用して検証してみます。
環境
- Apple M1 Pro
- macOS Monterey 12.3
- Docker Engine 20.10.13
- Docker Compose v2.2.3
- Ruby 3.1.1
- Ruby on Rails 7.0.2
- Nginx 1.21.6
ngx_http_limit_conn_module
ngx_http_limit_conn_module
では、主にlimit_conn_zone
ディレクティブとlimit_conn
ディレクティブを設定し、IPアドレス単位での制限を実施します。
以下設定ファイルです(本題でない箇所は割愛してます)。
nginx.conf
http { limit_conn_zone $binary_remote_addr zone=addr_limit:10m; server { listen 80; server_name localhost; root /app/public; location / { limit_conn addr_limit 10; limit_conn_status 503; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://puma; }
limit_conn_zone
ディレクティブでは、メモリに保存するためのキー、ゾーン、テーブルのサイズを指定します。キーに$binary_remote_addr
を指定することで、32ビット表現のIPv4アドレス単位で制限をかけられます(メモリ容量に優しい)。
limit_conn
ディレクティブでは、ゾーンを指定し、最大同時接続数をその後に続けます。
http、server、locationの各コンテキストに配置可能ですが、今回はアプリケーションルートへのアクセスで検証したいので、location /
に設定してみました。
この設定だと、同一IPアドレスからの最大接続数は10が上限となっています。
検証
abコマンドを使用してリクエストを投げてみます。
$ ab -n 10 -c 10 http://localhost/
オプションのnはリクエスト数、cは同時接続数を設定するものです。
結果
# ... Complete requests: 10 Failed requests: 0
Failed requests
が0となっており、全てのリクエストが成功した結果になりました。
では同時接続数を変更してみます。
$ ab -n 20 -c 20 http://localhost/
結果
# ... Complete requests: 20 Failed requests: 9
Complete requests
が20、Failed requests
が9となりました。
最大接続数が10で、20接続が1リクエストずつ送ったことを考えるとFailed requests
は10になってほしかった・・。
何となくupstream
側の影響もありそうな気がするのですが、一旦ここでは制限ができていることが確認できたので良しとします(「limit_conn not working」等で調べると色々出てきたりもしました)。
ngx_http_limit_req_module
ngx_http_limit_req_module
ではlimit_req_zone
とlimit_req
ディレクティブを使用し、IPアドレスにつき単位時間あたりのリクエスト数に制限をかけることができます。
nginx.conf
http { limit_req_zone $binary_remote_addr zone=req_limit:10m rate=1r/s; server { listen 80; server_name localhost; root /app/public; location / { limit_req zone=req_limit burst=5; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://puma; }
limit_req_zone
ディレクティブではlimit_conn_zone
ディレクティブと同様、メモリに保存するためのキー、ゾーン、テーブルのサイズを指定します。また、リクエスト回数の許容レート(頻度)を指定します。
limit_req
ディレクティブではゾーンを指定し、burst
というオプションを指定します。これはlimit_req_zone
ディレクティブで指定したレートを越えたリクエストについて、burst
に指定した数までをキューイングし、遅延して処理をしてくれるようになるオプションです。burst
をオーバーしたリクエストについてはデフォルトで503エラーを返却します。
上記設定だと、IPアドレスにつき秒間1リクエスト相当のリクエスト頻度を許可しており、超過した5リクエスト分までをキューに追加して遅延処理させています。
burst
オプションについては穴の空いたバケツを想像するとわかりやすいかもしれません。5個のリクエストが入るバケツがあり、穴は1秒につき1リクエストを通過させています。バケツから溢れたリクエストは503エラーを流します。
検証
$ ab -n 6 -c 1 http://localhost/
結果
# ... Concurrency Level: 1 Time taken for tests: 5.055 seconds Complete requests: 6 Failed requests: 0 Total transferred: 79545 bytes HTML transferred: 74544 bytes Requests per second: 1.19 [#/sec] (mean) Time per request: 842.499 [ms] (mean) Time per request: 842.499 [ms] (mean, across all concurrent requests) Transfer rate: 15.37 [Kbytes/sec] received
Time taken for tests
を見ると5秒かかっていることがわかります。実際コマンドを打って5秒後に結果が出てきています。Time per request
を見るとおよそ0.8秒で、1リクエストがざっくり1秒で処理された結果になりました。
nodelayオプション
nodelay
オプションというものも指定可能です。通常、burst
オプションで指定したリクエスト分は遅延させてリクエストを処理しますが、その遅延処理をなくし、即座にリクエストを処理するオプションがnodelay
です。
初めてnodelay
を見た際はアクセス制限として意味が無くなってしまうのでは…?とも思いましたが、例えば「秒間10リクエストまではサービス利用における想定の範囲内で問題無い、体験が悪くなってしまうので遅延させる必要も無い。しかしそれを超えるリクエストが一気に来てしまうのは想定していないし不審な動きと考えられるから制限したい」等、柔軟な制限設定ができるようになるので今は便利なのかもと思ってます。
検証
nginx.conf
http {
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=1r/s;
server {
listen 80;
server_name localhost;
root /app/public;
location / {
limit_req zone=req_limit burst=5 nodelay; # nodelay追加
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://puma;
}
$ ab -n 6 -c 1 http://localhost/
結果
Concurrency Level: 1 Time taken for tests: 0.118 seconds Complete requests: 6 Failed requests: 0 Total transferred: 79529 bytes HTML transferred: 74544 bytes Requests per second: 50.69 [#/sec] (mean) Time per request: 19.730 [ms] (mean) Time per request: 19.730 [ms] (mean, across all concurrent requests) Transfer rate: 656.08 [Kbytes/sec] received
Time taken for tests
を見ると、およそ0.1秒でテストが終わったことがわかります。
また、Time per request
からもわかるように、nodelay
を指定していない場合と比べてリクエストの処理が速攻で終わった結果です。コマンドを打って即結果が表示されます。
ちなみに当然ですが、バケツの容量自体は空いていない(リクエスト頻度が高い)ので、同じab -n 6 -c 1 http://localhost/
を間髪入れずに叩くとその分Failed requests
がカウントされます。
終わりに
今回はNginxで大量リクエスト制限を試してみました。
当然のことながらセキュリティやパフォーマンスの観点でも有用に思いました。セキュリティで言うと、より低レイヤな攻撃は防げないのでもう少し踏み込んでいきたいところ。
次はキャッシュとしてのNginxも試してみたいです。
参考記事等
書籍:nginx実践入門
Nginxでのアクセス流量制御を検証してみた - Qiita
過度のアクセスに備える(その2)!!Nginxのlimit_reqの設定と検証 | Code & Business
nginx でリクエストを制限できるモジュール「ngx_http_limit_req_module」 - kakakakakku blog
Re: nginx limit_req and limit_conn not working to prevent DoS attack