WordPress のログイン画面や問い合わせフォームに、同じ IP から機械的なアクセスが大量に届く──。放置するとログの汚染、サーバ負荷、そして総当たりによるアカウント突破のリスクにつながります。この記事は、Web サーバ側(nginx / Apache)で「同じ相手からの過剰なリクエスト」を機械的に絞る設定を、コピペで導入できる形でまとめたものです。対象は WordPress サイトの運用担当者・サーバを管理する制作会社の方(中級者)です。
レート制限は「アプリを触らず、入口で過剰アクセスだけを間引ける」のが利点です。プラグインやアプリのコードを書き換えずに、まず管理画面とフォームという狙われやすい入口から守れます。
⚠️ 先に結論:レート制限は補助的な防御です。パスワードの強化・多要素認証・ソフトウェアの更新の代わりにはなりません。過剰に絞ると正規の利用者(共有回線の複数ユーザーや正当なクローラ)まで弾いてしまうため、強く絞るのは狙われやすい入口だけに留めるのが安全側の設計です。
なぜレート制限が要るのか(概念だけ)
Web サーバに届くアクセスには、正規の閲覧に混じって次のような機械的アクセスが含まれます。
- 総当たり(ブルートフォース): ログイン画面へ ID/パスワードを高速に試し続ける
- スクレイピング: サイト全体を短時間で大量取得し、サーバ負荷やコンテンツ複製につながる
- 軽量な DoS: 1 台〜少数から重いページへ集中アクセスし、レスポンスを遅延させる
いずれも「人間の操作としてはあり得ない頻度」であることが共通点です。レート制限はこの頻度に上限を設け、上限を超えたぶんを待たせるか 429 Too Many Requests で返します。攻撃を「検知して止める」というより、成立しにくくして割に合わなくする発想です。
nginx:limit_req でリクエスト頻度を絞る
nginx には標準モジュール ngx_http_limit_req_module が同梱されており、追加インストール不要で使えます。設計は 2 段階です。
-
http {}ブロックで**ゾーン(カウンタ置き場)**を定義する -
server {}/location {}でそのゾーンを適用する
1. ゾーンの定義(http ブロック)
/etc/nginx/nginx.conf の http {} 直下に置きます。
# キー = クライアントIP(binary_remote_addr は省メモリ)
# サイト全体向けのゆるいゾーン(1秒あたり10リクエスト)
limit_req_zone $binary_remote_addr zone=perip_general:10m rate=10r/s;
# ログイン・フォーム向けの厳しめゾーン(1秒あたり1リクエスト)
limit_req_zone $binary_remote_addr zone=perip_login:10m rate=1r/s;
# 同時接続数のゾーン(スロー系DoS・大量並列取得の抑制)
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
# 制限超過時は 429 を返す(既定は 503。429 のほうが意図が明確)
limit_req_status 429;
limit_conn_status 429;
10m は約 16 万 IP 分のカウンタを保持できるメモリサイズです。rate=10r/s は「1 秒あたり 10 リクエスト」=平均 100ms に 1 回の意味で、これを超えたぶんが待機・拒否の対象になります。
2. 適用(server / location ブロック)
burst は「一時的な集中をどれだけ許すか」のバケツ、nodelay は「バケツ内は待たせず即通す(超過ぶんだけ弾く)」指定です。
server {
# サイト全体:平常のブラウズは通し、機械的な連打だけ間引く
limit_req zone=perip_general burst=20 nodelay;
# 1IPあたりの同時接続を制限(重いページへの並列集中を抑える)
limit_conn perip_conn 10;
# WordPress のログイン画面は最優先で厳しく
location = /wp-login.php {
limit_req zone=perip_login burst=5 nodelay;
try_files $uri =404;
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
# XML-RPC は総当たりの踏み台になりやすい。使っていなければ遮断
location = /xmlrpc.php {
deny all;
}
# 問い合わせフォームの送信先(例)も厳しめに
location = /contact/submit {
limit_req zone=perip_login burst=3 nodelay;
try_files $uri /index.php?$args;
}
}
設定を書いたら、必ず構文チェックしてから反映します。
sudo nginx -t # 構文チェック(syntax is ok / test is successful を確認)
sudo systemctl reload nginx # 無停止でリロード
burst を小さくしすぎると、画像やCSSを並列取得する通常のブラウザまで弾かれることがあります。まずは全体 burst=20 nodelay、ログインだけ burst=5 程度から始め、アクセスログを見て調整してください。
社内IP・監視・決済コールバックは除外する
固定 IP からの正規アクセス(社内・監視サービス・決済のWebhook等)まで絞ると業務が止まります。geo で許可 IP を判定し、その場合はゾーンのキーを空にして制限をスキップできます。
# 空文字キーの行は limit_req の対象外になる
geo $limit_key {
default $binary_remote_addr;
203.0.113.10 ""; # 社内固定IP(例)
198.51.100.0/24 ""; # 監視サービスの帯域(例)
}
limit_req_zone $limit_key zone=perip_general:10m rate=10r/s;
CDN・リバースプロキシ配下では「本当のIP」で数える
Cloudflare などの CDN やロードバランサの背後に nginx がある場合、$binary_remote_addr はすべて CDN の IPになり、全利用者が同一 IP として合算されてしまいます。これでは正規利用者をまとめて弾く事故になります。ngx_http_realip_module で本来のクライアント IP を復元してから数えます。
# 信頼できる前段(CDN/LB)の帯域を列挙し、そのヘッダのIPを採用する
set_real_ip_from 173.245.48.0/20; # 例:CDN事業者が公開する帯域に置き換える
set_real_ip_from 103.21.244.0/22; # 同上(公式ドキュメントの最新帯域を使う)
real_ip_header CF-Connecting-IP; # 利用CDNが付与するヘッダ名に合わせる
real_ip_recursive on;
set_real_ip_from は信頼できる前段だけを列挙します。ここに広い範囲や不特定の IP を書くと、ヘッダを詐称した接続元の IP を信じてしまい、制限を回避されます。必ず利用中の CDN/LB が公開する正規の帯域に限定してください。
Apache の場合
Apache には nginx の limit_req に相当する「リクエスト回数ベース」の標準機能がありません。用途を分けて 2 つの標準・準標準モジュールを組み合わせます。
mod_reqtimeout:低速なリクエストによる占有を防ぐ(標準同梱)
ヘッダやボディをだらだらと少しずつ送って接続を長時間占有する低速 DoS は、mod_reqtimeout で緩和できます。多くのディストリで既定有効です。apache2.conf / httpd.conf に置きます。
<IfModule reqtimeout_module>
# ヘッダは20〜40秒以内かつ最低500バイト/秒、ボディも同様に要求
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500
</IfModule>
mod_evasive:短時間の集中アクセスを一時ブロック(要インストール)
回数ベースの抑制は mod_evasive(サードパーティ)で行います。同一 IP から同一ページ/サイトへ短時間に閾値を超えたアクセスがあった場合、一定時間 429(または 403)を返します。
# Debian/Ubuntu
sudo apt install libapache2-mod-evasive
sudo a2enmod evasive
sudo systemctl reload apache2
<IfModule mod_evasive20.c>
DOSHashTableSize 3097
DOSPageCount 5 # 同一ページへ1秒に5回超で一時ブロック
DOSSiteCount 50 # 同一サイトへ1秒に50回超で一時ブロック
DOSPageInterval 1
DOSSiteInterval 1
DOSBlockingPeriod 60 # ブロック継続(秒)
DOSReturnHTTPStatus 429
# 監視・社内など正規の固定IPは除外する
DOSWhitelist 203.0.113.10
DOSWhitelist 198.51.100.*
</IfModule>
Apache でも、Cloudflare 等の背後では mod_remoteip で本来の IP を復元してから数えるようにします(そうしないと mod_evasive が CDN の IP を基準に誤判定します)。
<IfModule mod_remoteip.c>
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20
</IfModule>
反映前に必ず構文確認します。
sudo apachectl configtest # Syntax OK を確認
sudo systemctl reload apache2
効き目の確認(自分のサーバに対してだけ)
設定が効いているかは、自分が管理するサーバに対して軽い連続アクセスを行い、429 が返り始めるかで確認します。ab(Apache Bench)で少数リクエストを送るのが手軽です。
# 自サーバのログインURLへ、同時2並列で20リクエスト(あくまで自サイトの動作確認)
ab -n 20 -c 2 https://example.com/wp-login.php
# レスポンス内訳に「Non-2xx responses」(=429で弾かれた数)が出れば制限が効いている
他人のサイトへ負荷をかける行為は不正アクセスにあたります。確認は必ず自分の管理下のサーバに限定してください。日常的には、アクセスログで 429 の発生状況を眺めるだけでも十分に傾向がつかめます。
# nginx: 直近ログから 429 の多い送信元IP上位を集計
awk '$9==429 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
正規の利用者が 429 を踏んでいないか(共有回線・社内・正当なクローラ)を確認し、踏んでいれば burst や rate を緩める、あるいは除外リストに追加します。弾きすぎは機会損失なので、ログを見ながら数週間かけて落ち着かせるのが実務的です。
まとめ
- レート制限はアプリを触らずに入口で過剰アクセスを間引く補助防御。まず狙われやすい入口(ログイン・フォーム・XML-RPC)から
- nginx は標準の
limit_req_zone+limit_req(burst/nodelay)とlimit_conn。超過は429で返す - Apache は
mod_reqtimeout(低速DoS)+mod_evasive(回数ベース)を組み合わせる - CDN/プロキシ配下では
real_ip/mod_remoteipで本当のIPに直してから数える(でないと全員を巻き込む) - 社内・監視・決済コールバックは除外。反映前に
nginx -t/apachectl configtest。効き目は自サーバへのabとログで確認 - あくまで補助。更新・強いパスワード・多要素認証が本丸であることは変わらない
関連記事
- WordPressログイン総当たり対策5選(wp-login.php保護)
- 問い合わせフォームのスパム対策5選(honeypot/Turnstile/レート制限)
- WordPressのユーザー名がバレる3つの経路と塞ぎ方
本記事のレート制限やヘッダ設定が正しく効いているかは、Webサイトを9つの守りでまるごと守る「サイトドック」の月次の定期健診でまとめて確認できます → https://sitedock.jp