こんな方向け: Cloudflare の無料プランに WordPress サイトを載せたものの、WAF は「とりあえず有効」のままの Web 担当者・制作会社の方。
この記事で解決すること: 無料プランの カスタムルール 5 本(+レート制限 1 本)を、WordPress でよく狙われる入口に割り振る式(式エディタにそのまま貼れる形)と手順がわかります。
はじめに:無料プランで使える WAF の範囲
WAF(Web Application Firewall)= Web サイトへのリクエストを中身で判定して、怪しいものを手前で止める仕組みです。Cloudflare では、プロキシ(オレンジ雲)を有効にしたホスト名への通信がエッジ(Cloudflare 側)で判定されます。
無料(Free)プランの主な制約は次のとおりです(Cloudflare 公式ドキュメントより)。
| 項目 | Free プランの範囲 |
|---|---|
| カスタムルール | 5 本 |
正規表現(matches 演算子) |
使えない(Business 以上) |
| アクション | Log 以外(Block / Managed Challenge / Skip など) |
| カスタムリスト(IP リスト) | 1 個・合計 1 万件まで |
| レート制限ルール | 1 本・10 秒単位で数えて 10 秒遮断・IP 単位・条件に使えるのは Path と Verified Bot のみ |
| マネージドルール | Cloudflare Free Managed Ruleset(影響が大きく悪用が広がった脆弱性向け) |
正規表現が使えないので、式は eq / in / wildcard(* のパターン一致)で書きます。wildcard は大文字小文字を区別しない(公式)ため、/WP-LOGIN.PHP のような表記ゆれも拾えます。
先に確認したい 2 つの前提
- WAF は Cloudflare を経由した通信にしか効きません。 オリジンの IP へ直接アクセスされると全ルールが素通りです。オリジン側で Cloudflare 以外からの 80/443 を受けない設定まで済ませてください(関連記事参照)。
- ルールは上から順に評価され、Block に当たるとそこで止まります。 あとから Skip ルールを足すときは並び順に注意してください。
準備:管理者 IP のリストを作る
ルール 1 で使う「管理画面に入ってよい IP」を、カスタムリストに登録しておきます。
- アカウントの Manage Account → Configurations → Lists(UI 更新で場所が変わることがあります)から IP リストを作成
- 名前は
admin_ipsとします(英小文字・数字・アンダースコア) - 事務所の固定 IP、VPN の出口 IP、制作会社の固定 IP などを登録
式からは $admin_ips で参照します。
ip.src in $admin_ips
無料プランではリストは 1 個までです。そのため、用途を「管理者の IP」に絞り、ほかのルールではリストを使わない設計にしています。
ルール 1:ログイン画面・管理画面を管理者 IP だけに絞る
総当たり攻撃(パスワードを大量に試す攻撃)を入口で止めます。admin-ajax.php と admin-post.php は 公開ページ側のフォームやプラグインも呼び出す ので除外しないと、サイトの一部機能が壊れます。
(
http.request.uri.path wildcard "/wp-login.php"
or http.request.uri.path wildcard "/wp-admin"
or http.request.uri.path wildcard "/wp-admin/*"
)
and not http.request.uri.path wildcard "/wp-admin/admin-ajax.php"
and not http.request.uri.path wildcard "/wp-admin/admin-post.php"
and not ip.src in $admin_ips
- アクション: Block
- WordPress をサブディレクトリ(例
/blog/)に置いている場合は、パスの先頭を/blog/wp-login.phpのように書き換えます - 会員登録・パスワードリセットで一般利用者も
wp-login.phpを使うサイトでは、このルールは使えません。次の代替案を使ってください
固定 IP が無い場合の代替案
在宅勤務などで管理者の IP が固定できないときは、国で絞ったうえで Block ではなく Managed Challenge(Cloudflare が人間かどうかを自動判定し、必要なときだけ確認画面を出す)にします。
(
http.request.uri.path wildcard "/wp-login.php"
or http.request.uri.path wildcard "/wp-admin"
or http.request.uri.path wildcard "/wp-admin/*"
)
and not http.request.uri.path wildcard "/wp-admin/admin-ajax.php"
and not http.request.uri.path wildcard "/wp-admin/admin-post.php"
and not ip.src.country in {"JP"}
ip.src.country は ISO 3166-1 の 2 文字コードです(古い ip.geoip.country は非推奨)。海外出張・海外 VPN 経由では確認画面が出ます。
ルール 2:xmlrpc.php を止める
xmlrpc.php は外部アプリから WordPress を操作するための古い入口です。使っていないのに開いたままだと、パスワード総当たりの的になり続けます。
http.request.uri.path wildcard "/xmlrpc.php"
- アクション: Block
- Jetpack・WordPress モバイルアプリなど XML-RPC を使う連携があるなら止めないでください。 不明なら Security Events(後述)で数日確認してから有効にします
ルール 3:uploads 内の PHP と機密ファイルへのアクセスを止める
画像置き場の wp-content/uploads/ に PHP が置かれて実行される被害は、改ざんの典型例です。あわせて、外から見えてはいけないファイルへのアクセスも止めます。
http.request.uri.path wildcard "/wp-content/uploads/*.php"
or http.request.uri.path wildcard "/wp-config.php*"
or http.request.uri.path wildcard "/.env*"
or http.request.uri.path wildcard "/.git/*"
or http.request.uri.path wildcard "/wp-content/debug.log"
or http.request.uri.path wildcard "*.sql"
or http.request.uri.path wildcard "*.bak"
- アクション: Block
- 証明書の自動更新で使う
/.well-known/はこの式に当たらないので、Let's Encrypt の更新は妨げません - これは 追加の層 です。Cloudflare を外した瞬間に無防備にならないよう、サーバー側でも uploads の PHP 実行禁止と機密ファイルの配置見直しを行ってください
ルール 4:ユーザー名の列挙を止める
WordPress では、ログイン ID(ユーザー名)が外から推測される経路がいくつかあります。代表的な 3 つの経路を、ログインしていない訪問者に限って 止めます。
(
http.request.uri.path wildcard "/wp-json/wp/v2/users*"
or http.request.uri.query wildcard "*rest_route=/wp/v2/users*"
or http.request.uri.query wildcard "*rest_route=%2fwp%2fv2%2fusers*"
or http.request.uri.query wildcard "author=*"
or http.request.uri.query wildcard "*&author=*"
)
and not http.cookie contains "wordpress_logged_in_"
- アクション: Block
- ブロックエディタはログイン中に
/wp-json/wp/v2/usersを呼ぶため、ログイン Cookie がある通信は除外しています - Cookie は利用者側で偽装できます。 このルールは「大量の機械的な列挙を手前で減らす」ためのものです。WordPress 側の対策(REST API のユーザー一覧の非公開化など)を別途行ってください
ルール 5:緊急用に 1 枠空けておく
5 本すべてを常設で埋めず、プラグインの脆弱性が公表されてすぐ更新できないときに 更新までのつなぎとして該当の入口だけを塞ぐ 枠を残しておきます。
http.request.uri.path wildcard "/wp-content/plugins/example-plugin/*"
- アクション: Block(
example-pluginは実際のプラグインのディレクトリ名に置き換えます) - ディレクトリごと止めると公開ページの CSS・JS も止まるので、可能なら公式アドバイザリが示すパスに絞ります
- 更新が済んだら必ず無効化・削除します。 つなぎのルールは更新の代わりになりません
おまけ:レート制限 1 本は wp-login.php に
カスタムルールとは別枠で、レート制限ルールが 1 本使えます。無料プランでは条件に Path しか使えないので、ログイン画面に充てるのが素直です。
http.request.uri.path eq "/wp-login.php"
- 数える単位: IP(無料プランはこれのみ)
- しきい値の例: 10 秒あたり 5 リクエストを超えたら Block(遮断時間は 10 秒固定)
- 遮断は 10 秒で解けるので 速度を落とす補助 です。サーバー側のレート制限・ログイン試行制限と組み合わせてください
効いているかを確認する
1. 自分のサイトに curl で確認する
必ず自社で管理しているサイトだけで 確認してください。管理者 IP 以外の回線(スマホのテザリングなど)から実行します。
SITE="https://example.com" # 自社サイトに置き換える
for p in /xmlrpc.php /wp-login.php /wp-content/uploads/test.php /.env "/?author=1" /wp-json/wp/v2/users; do
printf '%-32s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "$SITE$p")"
done
Block に当たったものは 403 が返ります。トップページや画像など通常のページが 200 のままであることも、あわせて確認してください。
2. Security Events で誤検知を見る
Security → Events(Security Analytics)でルールごとに止めた通信を見られます。有効化直後の数日は 正規の利用者や連携が止まっていないか を確認します。
3. ルールを API で棚卸しする
経緯不明のルールが残りがちなので、定期的に一覧を書き出します。WAF の読み取り権限だけの API トークンを使います。
# CF_API_TOKEN と ZONE_ID は環境変数で渡す(スクリプトに直書きしない)
curl -s "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets/phases/http_request_firewall_custom/entrypoint" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
| jq -r '.result.rules[] | [.enabled, .action, .description] | @tsv'
無効のまま放置されたルールや緊急用枠の消し忘れを確認します。
まとめ
| 枠 | 内容 | アクション |
|---|---|---|
| ルール 1 | wp-login.php・wp-admin を管理者 IP に限定(代替: 国外は Managed Challenge) | Block |
| ルール 2 | xmlrpc.php | Block |
| ルール 3 | uploads 内 PHP・機密ファイル | Block |
| ルール 4 | 未ログイン時のユーザー名列挙 | Block |
| ルール 5 | 緊急用の空き枠(更新までのつなぎ) | Block |
| レート制限 | wp-login.php を IP 単位で 10 秒 5 回まで | Block |
- オリジンへの直接アクセスを塞ぐところまでが 1 セットです
- エッジのルールはサーバー側の対策に 重ねる層 で、WordPress・プラグインの更新の代わりにはなりません
関連記事
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp