nginxのrate limitingでアクセス制限・bot対策をする

※本ページには広告(アフィリエイトプログラム等)が含まれます。

アクセスログを眺めていると、同じIPから1秒間に何十回もリクエストが飛んできていることがある。行儀の悪いクローラや、脆弱性スキャンをかけてくるボットは想像以上に多い。正規のユーザーを締め出さずにこうしたアクセスだけ絞りたい場合、nginxのlimit_reqlimit_connで十分実用的な対策になる。ここではさくらのVPS(Debian 12・nginx)を前提に、設定と考え方をまとめる。

なぜrate limitingが必要か

検索エンジンの公式クローラはrobots.txtのクロール遅延をある程度尊重するが、SEO系の解析ボットや自作スクレイパーの中には秒間数十リクエストを平気で送ってくるものがある。PHP-FPMのworkerが埋まってしまえば正規ユーザーのレスポンスも遅くなるし、DBへの負荷も上がる。WAFやCDNを導入するのも手だが、まずはnginx単体でできる制限をかけておくのがコスト0で効果が高い。

limit_reqでリクエスト数を制限する

まずhttp{}ブロックでゾーンを定義する。ゾーンはIPごとの状態を保持する共有メモリ領域だと思えばいい。

http {
    # $binary_remote_addr をキーに、10MBのゾーン"api"で秒間10リクエストまで
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    ...
}

10MBあれば概算でおよそ16万IP分の状態を保持できるので、個人サイト〜中規模サイトなら十分な容量だ。実際に制限をかけたいlocationで適用する。

location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://127.0.0.1:9000;
}

burstとnodelayの意味

rate=10r/sは平均レートの上限であって、瞬間的なバーストを全く許さないわけではない。nginxの実装はトークンバケット(leaky bucket)方式で、burst=20は「一時的に20リクエスト分まで貯めておける」という意味になる。

  • burstを指定しない場合、rateを一瞬でも超えたリクエストは即座に弾かれる。実際のブラウザ挙動(画像やCSSの同時取得など)ではこれだと誤検知しやすい。
  • burst=20だけでnodelayを付けない場合、バケットに溜まった超過分のリクエストは即座に処理されず、rateに合わせて遅延キューイングされる。体感で「なぜか遅い」状態になる。
  • nodelayを付けると、バケットの範囲内であれば遅延させずに即時処理し、バケットを使い切った分だけ弾く。ユーザー体験を損なわずに過剰アクセスだけ弾きたいなら基本的にnodelayとセットで使うのが実務的だ。

limit_connで同時接続数を制限する

limit_reqがリクエストの「頻度」を制限するのに対し、limit_connは同時に張っていられる「接続数」を制限する。大きなファイルのダウンロードを1つのIPから何本も同時に張られる、といったケースに効く。

http {
    limit_conn_zone $binary_remote_addr zone=conn:10m;
    ...
}

server {
    location /downloads/ {
        limit_conn conn 10;
    }
}

これで1IPあたり同時10接続までに制限される。動画配信や大容量ファイルを扱うサイトでは特に効果が分かりやすい。

制限超過時のステータスコードを変える

デフォルトでは制限に引っかかると503(Service Unavailable)が返る。だが503はサーバー障害を意味するステータスなので、クライアント側の過剰アクセスを表すなら429(Too Many Requests)にしておいたほうが意味的に正しいし、監視ツールが503を障害と誤検知するのも防げる。

limit_req_status 429;
limit_conn_status 429;

特定IP・UAを除外する

自社の監視サーバーや管理画面からのアクセスまで制限に巻き込みたくない場合は、geomapを組み合わせてキーを空にする方法が定番だ。キーが空文字になったリクエストはlimit_req_zoneの対象から外れる。

geo $limit_exclude {
    default     1;
    203.0.113.0/24 0;   # 自社の監視サーバーなど除外したいレンジ
}

map $limit_exclude $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=api:10m rate=10r/s;

User-Agentで悪質ボットを弾きたい場合はmapで判定してifで即403を返すパターンがシンプル。

map $http_user_agent $bad_bot {
    default 0;
    ~*(SemrushBot|AhrefsBot|MJ12bot|DotBot|PetalBot) 1;
}

server {
    if ($bad_bot) {
        return 403;
    }
    ...
}

ただしUser-Agentは詐称が簡単なので過信は禁物。あくまで「行儀のいい自称ボット」に対する一次フィルタと考えたほうがいい。

効果の確認とやりすぎ注意

設定を反映したら、アクセスログで429が出ているか確認する。

tail -f /var/log/nginx/access.log | grep ' 429 '

$limit_req_statusや制限のログをerror_logにも出しておくと、どのIPがどれだけ弾かれているか把握しやすい。ここで注意したいのは、rateやburstを絞りすぎると正規ユーザーまで巻き込むこと。特に同一オフィスやテザリング環境ではNAT配下の複数ユーザーが同じグローバルIPになるため、$binary_remote_addr単位の制限は厳しすぎると誤爆する。まずは緩めの値(rate=10r/s、burst=20程度)から始めて、ログを見ながら調整するのが安全だ。

まとめ

limit_reqでリクエスト頻度、limit_connで同時接続数を制限し、429を返すよう明示しておく。burstとnodelayの組み合わせを理解しておけば、正規ユーザーの体感を損なわずに悪質なアクセスだけ絞れる。除外リストとUA判定を組み合わせれば、自社アクセスを守りつつボット対策もできる。まずは緩めに設定してログで様子を見る、というのが結局一番安全なやり方だと思う。

読んで頂いて有り難うございます!