2026年9月15日

2026年9月15日

WordPressのNginxでlimit_reqを設定してリクエストを制限する方法

Nginxの limit_req を使うと、IPアドレスごとのリクエスト数に上限を設け、wp-login.php への総当たり攻撃や xmlrpc.php への大量アクセスがPHPに届く前に止められます。この記事では、WordPress向けに制限をかける場所の選び方、具体的な設定、Cloudflareなどのプロキシを挟む場合の注意、ドライランでの事前確認とログの読み方までを説明します。読み終えると、管理画面を使う自分を締め出さずに、負荷の高いアクセスだけを抑えられるようになります。

limit_reqの仕組み

limit_req は、Nginxに標準で組み込まれている ngx_http_limit_req_module の機能です。設定は二段構えになっています。

  • **limit_req_zone**(httpブロック):何を単位に数えるか(キー)、記録用の共有メモリの名前と大きさ、許可する速度を決めます。
  • **limit_req**(server・locationブロック):どのURLにそのゾーンを適用するか、一時的な超過をどこまで許すか(burst)を決めます。

速度は「1秒あたり(r/s)」か「1分あたり(r/m)」で指定します。rate=6r/m なら10秒に1回のペースで、それを超えたリクエストは拒否されます。ただし人間の操作は短時間にまとまることが多いため、burst で一時的な超過を待ち行列として受け入れ、nodelay で待たせずに即時処理する、という組み合わせがよく使われます。

拒否したときの応答は、初期値ではステータス503です。WordPressのメンテナンスやサーバー障害と区別しやすいよう、limit_req_status 429;(Too Many Requests)に変えておくと、ログやモニタリングで見分けられます。

制限をかける場所を選ぶ

WordPressでは、どのURLにも同じ制限をかけるのは危険です。管理画面やブロックエディターは、1回の操作で多くのリクエストを送ります。

  • **wp-login.php(POSTのみ)**:ログインの総当たり対策の中心です。ログイン画面の表示(GET)まで絞る必要はありません。
  • **xmlrpc.php**:使っていなければ deny all; で閉じるのがいちばん単純な対策です。Jetpackや外部のブログエディターなどで使っている場合だけ、制限付きで開けます。
  • PHP全体(ゆるい上限):キャッシュを通らないPHPへのリクエストに、人間の閲覧では届かない程度の上限をかけ、クローラーや攻撃ツールの暴走を抑えます。
  • 制限しない、またはゆるくする場所:/wp-admin/admin-ajax.php と /wp-json/ は、管理画面のハートビートやブロックエディターの保存が頻繁に使います。ここをきつく絞ると、編集中に保存できなくなります。画像やCSSなどの静的ファイルも対象外にします。

設定例

作業前に現在の設定ファイルをバックアップし、ステージング環境で試してから本番に反映してください。以下はUbuntuなどで /etc/nginx/nginx.conf の http ブロックに読み込まれる conf.d に置く例です。

# /etc/nginx/conf.d/ratelimit.conf (httpコンテキストで読み込まれる)

# 管理者の固定IPは制限の対象外にする(例の値は書き換える)
geo $limit_exempt {
    default        0;
    203.0.113.10   1;
}

# ログイン:POSTのときだけIPごとに数える。空文字のキーは数えられない
map "$limit_exempt:$request_method" $login_limit_key {
    default    "";
    "0:POST"   $binary_remote_addr;
}

# PHP全体:除外IP以外をIPごとに数える
map $limit_exempt $php_limit_key {
    0  $binary_remote_addr;
    1  "";
}

limit_req_zone $login_limit_key zone=wp_login:10m rate=6r/m;
limit_req_zone $php_limit_key   zone=wp_php:10m   rate=10r/s;

limit_req_status 429;
limit_req_log_level warn;

$binary_remote_addr はIPv4なら4バイト、IPv6なら16バイトの固定長で、文字列の $remote_addr よりメモリ効率が良いキーです。公式ドキュメントでは、1MBのゾーンでおよそ1万6千件の状態を保持できるとされているため、10MBあれば一般的なサイトには十分です。

次に、サイトのserverブロックで適用します。

server {
    # ...(listen, server_name, root などは既存のまま)

    location = /wp-login.php {
        limit_req zone=wp_login burst=4 nodelay;

        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }

    location = /xmlrpc.php {
        deny all;
    }

    location ~ \.php$ {
        limit_req zone=wp_php burst=20 nodelay;

        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}

ここでつまずきやすいのが、location = /wp-login.php の中にもPHPの処理(fastcgi_pass など)を書く点です。= で指定した厳密一致の location が選ばれると、正規表現の location ~ \.php$ には進みません。PHPの処理を書き忘れると、ログイン画面がPHPとして実行されずにダウンロードされたり、404になったりします。fastcgi_pass のソケットのパスは、環境のPHP-FPMの設定に合わせてください。

wp_login の設定は「10秒に1回のペースを基本に、連続4回までの超過は即時に通す」という意味です。パスワードを数回打ち間違えても締め出されず、ツールによる連続試行は数回目から429で止まります。

Cloudflareやロードバランサーの後ろにある場合

CDNやロードバランサーを経由していると、Nginxから見た接続元はすべてプロキシのIPになります。この状態で $binary_remote_addr を数えると、全訪問者が同じIPとみなされ、少しのアクセスでサイト全体が429を返します。

ngx_http_realip_module を使って、信頼できるプロキシから届いたヘッダーで接続元IPを置き換えてから制限します。

# httpコンテキスト。set_real_ip_from にはプロキシのIP範囲を列挙する
set_real_ip_from 192.0.2.0/24;      # 例:自前のロードバランサー
real_ip_header   X-Forwarded-For;
real_ip_recursive on;
# Cloudflareの場合は CF-Connecting-IP を使い、
# CloudflareのIP範囲(公式に公開されている一覧)を set_real_ip_from に並べる

set_real_ip_from を広く書きすぎると、攻撃者が偽のヘッダーでIPを詐称できてしまいます。Cloudflareの範囲は変わることがあるため、公開一覧から定期的に更新する仕組みを用意してください。Cloudflare側にもレート制限の機能がありますが、提供範囲はプランによって異なります(2026年10月現在)。

反映前にドライランで確かめる

いきなり拒否を始めると、想定外の利用者を巻き込むことがあります。Nginx 1.17.1以降では limit_req_dry_run on; を limit_req と同じ場所に書くと、実際には拒否せず、拒否したはずのリクエストをログに残すだけにできます。

location ~ \.php$ {
    limit_req zone=wp_php burst=20 nodelay;
    limit_req_dry_run on;
    # ...
}

構文を確認してから再読み込みします。

sudo nginx -t && sudo systemctl reload nginx

数日分のエラーログを見て、どのIPやURLがどの程度引っかかるかを確認します。

sudo grep "limiting requests" /var/log/nginx/error.log | tail -n 20

ドライラン中は limiting requests, dry run, excess: ... のように表示され、本番の拒否では dry run の文字が付きません。行には by zone "wp_php" とゾーン名、client: に接続元IP、request: に対象のリクエストが出ます。自分の管理作業や検索エンジンのクローラーが引っかかっていれば、burst を増やすか、対象から外します。問題がなければ limit_req_dry_run の行を消して有効にします。

アクセスログにも結果を残したい場合は、Nginx 1.17.6以降で使える $limit_req_status 変数(PASSED、DELAYED、REJECTED など)を log_format に加えると、集計しやすくなります。

動作を確認する

自分の環境から、ログインのPOSTを連続で送り、途中から429が返ることを確認します。除外IPに入っている端末からでは確認できないため、別の回線から試してください。

for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://example.com/wp-login.php
done

最初の数回は200、その後は429が並べば想定どおりです。確認後は、管理画面で記事を編集・保存し、プラグインの更新もできることを確かめます。

まとめ

  • limit_req_zone で数える単位と速度を、limit_req で適用先と burst を決めます。拒否時のステータスは limit_req_status 429; にすると切り分けやすくなります。
  • WordPressでは wp-login.php のPOSTをきつめに、PHP全体はゆるめに制限し、admin-ajax.php や /wp-json/、静的ファイルは巻き込まないようにします。
  • location = で厳密一致させた場所にはPHPの処理も書きます。プロキシの後ろでは realip で本当の接続元IPに置き換えてから数えます。
  • 本番では limit_req_dry_run とエラーログで影響を確かめてから有効にし、管理者のIPは geo と map で除外しておきます。
  • limit_req は入口の負荷を下げる仕組みで、強いパスワードや二段階認証、プラグインの更新の代わりにはなりません。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲