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は入口の負荷を下げる仕組みで、強いパスワードや二段階認証、プラグインの更新の代わりにはなりません。