Nginxのngx_http_limit_conn_moduleとngx_http_limit_req_moduleで大量リクエスト制限をする

Nginxのngx_http_limit_conn_modulengx_http_limit_req_moduleを使用すると、Dos攻撃等による大量リクエストがあった際に制限をかけられるとのことで、試してみたいと思います。

ngx_http_limit_conn_moduleでは同時接続数の制限、ngx_http_limit_req_moduleでは時間当たりのリクエスト制限のようです。

今回もdocker-composeを使用して検証してみます。

環境

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_zonelimit_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