WordPress の .htaccess で、開いてよい .php だけを通し、探りを fail2ban で遮断する(Web サイト担当者向け)

WordPress の .htaccess で、開いてよい .php だけを通し、探りを fail2ban で遮断する(Web サイト担当者向け)

アフィリエイト広告を利用しています

このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
挨拶や報告は無しで大丈夫です

さくらのVPSのCentOS Stream 9で、WordPress のサイトに届く探りを、.htaccess で拒んで fail2ban で遮断した覚書です。

その前の記事と前の記事で、サーバー管理者が、サーバー全体とサイトごとの設定をしました。今回は、Web サイトの担当者の立場で、サイトの .htaccess に、開いてよい .php だけを通して目印を付ける設定と、許可する拡張子を外す設定を書きます。
記事の中の IP アドレス・ドメインなどは、実際のものから書き換えています。

なお本番環境で利用される場合はここにある内容だけを鵜呑みにせずセキュリティ専門家に相談されることをお勧めします。

環境

実施日2026-11-09
サーバさくらのVPS 2G
OSCentOS Stream 9
cat /etc/redhat-release
CentOS Stream release 9

経緯と解決策

経緯について

WordPress のサイトには、WordPress には無い .php を次々に試すアクセスが届きます。プラグインの穴を狙った URL や、置かれているかもしれない管理用のスクリプトなどです。
.env などはサーバー全体の設定で、wp-config.php や xmlrpc.php などはサイトごとの設定(サーバー管理者が書く VirtualHost)で拒めます。ただ、どの .php を外から開くかはサイトによって違うので、それらの設定では拒めません。

何もしなければ、無い .php へのアクセスも WordPress まで届き、404 を返すためだけに PHP と WordPress が動きます。

解決案を考える

WordPress で、外から .php の URL として開くものは限られます。そこで、開いてよい .php をホワイトリストにして、それ以外を .htaccess で拒みます。拒むのは Apache の段なので、PHP も WordPress も動きません。

拒むときに、fail2ban 向けの目印(中:f2b_mid)を付けます。目印の付いたアクセスは、サーバー管理者が用意した fail2ban 向けのファイルに書かれ、fail2ban が数えて遮断します。
サイトの担当者がするのは、目印を付けるところまでです。何回で遮断するかは、サーバー管理者が決めています。

あわせて、サーバー全体で許可している拡張子のうち、このサイトでは出さないものがあれば、.htaccess で拒みます。
ホワイトリストに残る wp-login.php と管理画面(/wp-admin/)は、Basic 認証や IP アドレスの制限で、別に守っておきます。

事前確認

設定を書く前に、サーバー管理者の設定が入っているかと、サブディレクトリの .htaccess を確かめます。
例のサイトは example.com で、ファイルは /home/example_com/public_html/ にあるとします。

サーバー管理者の設定が入っているか

サーバー全体の設定が入っていれば /.git/config が、サイトごとの設定が入っていれば wp-config.php が、403 で拒まれます。
サーバーに SSH でログインして、サーバーから打ちます。サーバー自身からのアクセスは、fail2ban に数えられません。自分の PC から打つと、/.git/config は強に当たり、1 回で 1 日遮断されます。
歓迎しない bot を拒んでいるサーバーでは、curl の名乗りも拒まれて 403 になることがあるので、-A でブラウザに似た名乗りを付けます。

UA='Mozilla/5.0 (probe test)'
curl -s -A "$UA" -o /dev/null -w '%{http_code}\n' https://example.com/.git/config
curl -s -A "$UA" -o /dev/null -w '%{http_code}\n' https://example.com/wp-config.php

どちらも 403 と出れば、済んでいます。目印の名前(強:f2b_strong、中:f2b_mid、弱:f2b_weak)は、サーバー管理者に確かめておきます。
403 にならないときは、サーバー管理者の設定が入っていません。サーバー管理者に、前の 2 本の記事の設定を入れてもらいます。入れられないサーバーで、この記事の .htaccess だけを入れると、拒むだけになり、fail2ban には数えられません。

ほかの .htaccess

サブディレクトリに、RewriteEngine On を書いた .htaccess があると、そのディレクトリの下には、ドキュメントルートの .htaccess の書き換えが効きません。どこにあるかを見ておきます。

find /home/example_com/public_html -name .htaccess -not -path '*/wp-content/*' | xargs grep -il 'RewriteEngine on'

ドキュメントルートのほかに出てきたディレクトリ(サブディレクトリに置いた別の WordPress など)は、この記事の設定の外になります(補足説明の「サブディレクトリの .htaccess」)。

今回行うこと

はじめに、このサイトで外から開いてよい .php を決めます。
そのあと、.htaccess に、それ以外を拒んで目印を付ける設定と、許可する拡張子を外す設定を書きます。
最後に、試しのアクセスで、拒まれることと、サイトと管理画面が今までどおり動くことを確かめます。

ホワイトリストを決める

WordPress の本体で、外から開く .php は次のとおりです。これを基本にします。

URL何か
/index.php入口。固定リンクの URL も中でここに回る
/wp-login.phpログイン
/wp-cron.php予約投稿などの定期処理
/wp-comments-post.phpコメントの投稿
/wp-admin/ の下管理画面と admin-ajax.php
/wp-includes/js/tinymce/wp-tinymce.php旧エディター
/wp-trackback.php・/wp-signup.php・/wp-activate.php・/wp-links-opml.php・/wp-mail.phpトラックバック・会員登録・メールでの投稿など。使っていなくても、WordPress が自分で返すので通しておく
/xmlrpc.php使うサイトだけ(Jetpack など)。サーバー管理者に、サイトごとの設定で通してもらったうえで足す

WordPress の外に置いた PHP(問い合わせのフォームなど)や、プラグインが外から開かせる .php があれば、このサイト独自のものとして足します(探し方は、補足説明の「開いてよい .php の探し方」)。
ホワイトリストに無い .php は、中で拒みます(強・中・弱の決め方は、サーバー全体の設定の記事の「強・中・弱を決める」)。

.htaccess に書く

控えを取ってから、ドキュメントルートの .htaccess を開きます。

cp -p /home/example_com/public_html/.htaccess /home/example_com/public_html/.htaccess.20261109
vi /home/example_com/public_html/.htaccess

# BEGIN WordPress の行より前に、次を足します。

# BEGIN fail2ban
# 拒むときに、fail2ban 向けの目印を付ける(中:f2b_mid)
<IfModule mod_rewrite.c>
RewriteEngine On

# 中:ホワイトリストに無い .php
RewriteCond %{REQUEST_URI} \.php$
RewriteCond %{REQUEST_URI} !^/(?:index|wp-login|wp-cron|wp-comments-post|wp-trackback|wp-signup|wp-activate|wp-links-opml|wp-mail)\.php$
RewriteCond %{REQUEST_URI} !^/wp-admin/
RewriteCond %{REQUEST_URI} !^/wp-includes/js/tinymce/wp-tinymce\.php$
# サイト独自のもの
RewriteCond %{REQUEST_URI} !^/tools/form\.php$
# xmlrpc.php を使うサイトだけ(サイトごとの設定で通してもらったうえで、行頭の # を消す)
#RewriteCond %{REQUEST_URI} !^/xmlrpc\.php$
RewriteRule ^ - [E=f2b_mid:1,F]

# サーバー全体で許可している拡張子のうち、このサイトでは出さないもの(目印は付けない)
#RewriteRule "\.(?:mp3|mp4|webm)$" - [NC,F]
</IfModule>
# END fail2ban
  • [E=f2b_mid:1,F]:目印 f2b_mid を付けて、403 で拒みます。目印が付いたアクセスは、サーバー管理者が用意した fail2ban 向けのファイルに書かれます
  • RewriteCond の ! の行:ホワイトリストです。サイト独自のものは、/tools/form.php と同じ形で 1 行ずつ足します
  • [NC,F]:許可する拡張子を外すときは、目印を付けずに 403 で拒みます(数えません)。外したい拡張子を | で区切って並べ、行頭の # を消します。NC で、大文字と小文字を区別しません
  • 置く場所:# BEGIN WordPress より前に置きます。WordPress の書き換えより先に判断するためです。# BEGIN WordPress から # END WordPress の間は、WordPress が書き換えることがあるので、その中には書きません。すでに自分で書いた書き換えのうち、古い .php の URL を転送するものがあれば、そのあとに置きます(先に置くと、転送する前に拒まれます)。.php の URL にかからない書き換えとは、どちらが先でもかまいません
  • 書かないもの:.env などはサーバー全体の設定で、wp-config.php・xmlrpc.php・アップロードの置き場の .php などはサイトごとの設定で拒んでいるので、ここには書きません。xmlrpc.php を使うサイトでは、サーバー管理者にサイトごとの設定で通してもらい、ホワイトリストにも足します。許可する拡張子を足すことも、.htaccess ではできません(サーバー管理者に頼みます)

.htaccess は保存した時点で効きます。Apache の再起動は要りません。書き損じがあるとサイト全体が 500 になるので、保存したらすぐにトップページを開いて確かめます。500 になったら、控えに戻します。

動きを確かめる

サーバーから、試しのアクセスを送ります。

UA='Mozilla/5.0 (probe test)'
for p in /f2b-test.php /wp-login.php /wp-cron.php /; do
  printf '%-16s ' "$p"; curl -s -A "$UA" -o /dev/null -w '%{http_code}\n' "https://example.com$p"
done
/f2b-test.php    403
/wp-login.php    401
/wp-cron.php     200
/                200

ホワイトリストに無い .php は 403 で拒まれ、WordPress の本体の URL は、これまでどおり返ります。/wp-login.php の 401 は、ログインに Basic 認証をかけている場合です(かけていなければ 200)。

fail2ban 向けのファイルに書かれたかは、サーバー管理者に見てもらいます(root で打ちます)。

tail -n 1 /var/log/httpd/fail2ban-mid_log
192.0.2.10 [09/Nov/2026:01:05:12 +0900] example.com "GET /f2b-test.php HTTP/1.1" 403

/f2b-test.php が、中のファイルに書かれています。192.0.2.10 はサーバー自身なので、fail2ban は数えず、遮断もしません。

管理画面への道が塞がっていないかも見ます。

UA='Mozilla/5.0 (probe test)'
curl -s -A "$UA" -o /dev/null -w '%{http_code} %{redirect_url}\n' https://example.com/wp-admin/
curl -s -A "$UA" -o /dev/null -w '%{http_code}\n' https://example.com/wp-admin/admin-ajax.php
302 https://example.com/wp-login.php?redirect_to=https%3A%2F%2Fexample.com%2Fwp-admin%2F&reauth=1
400

/wp-admin/ はログイン画面に回され、admin-ajax.php は WordPress が 400(何をするかの指定が無い)を返しています。どちらも WordPress まで届いているので、塞がっていません。
あわせて、ブラウザで、トップページ、記事のページ、管理画面へのログインとダッシュボードが、今までどおり開くかを見ます。開かないものがあれば、その URL をホワイトリストに足します。

遮断を解く

遮断を解くのは、サーバー管理者です。解き方は、サーバー全体の設定の記事の「遮断を解く」です。
サイトの利用者が遮断されたと分かったら、その人が開いた URL が、ホワイトリストから漏れていないかを確かめます。

元に戻す

.htaccess から、# BEGIN fail2ban から # END fail2ban までを消します。控えに戻してもかまいませんが、控えを取ったあとに WordPress やプラグインが .htaccess を書き換えていれば、その分も戻ってしまうので気をつけます。

cp -p /home/example_com/public_html/.htaccess.20261109 /home/example_com/public_html/.htaccess

補足説明

開いてよい .php の探し方

サイト独自の .php が分からないときは、アクセスログで、200 を返した .php の URL を数えると見つかります(ログの置き場は、サーバー管理者に確かめます)。

awk '$9 == 200 { p = $7; sub(/\?.*/, "", p); if (p ~ /\.php$/) print p }' /home/example_com/logs/*access_log | sort | uniq -c | sort -rn | head -n 30
   2016 /wp-cron.php
    742 /wp-admin/admin-ajax.php
     38 /wp-admin/post.php
     21 /wp-admin/load-styles.php
     15 /wp-admin/admin.php
     12 /wp-admin/load-scripts.php
      9 /wp-login.php
      7 /blog/wp-cron.php
      6 /wp-admin/options-general.php
      3 /blog/wp-admin/admin-ajax.php
      2 /tools/form.php
      1 /wp-includes/js/tinymce/wp-tinymce.php

この例では、WordPress の本体のもののほかに、/blog/ の下(サブディレクトリに置いた別の WordPress)と、/tools/form.php(WordPress の外に置いた PHP)があります。/tools/form.php をホワイトリストに足し、/blog/ は自分の .htaccess を持っていれば、そちらに書きます。
しばらく使われていない .php は、ログに出てきません。月に 1 回しか動かない処理などは、漏れていないかを別に確かめます。

xmlrpc.php を WordPress の側でも止める

xmlrpc.php は、外から投稿・操作するための古い仕組みです。今は REST API とアプリケーションパスワードに移っていて、使わないサイトがほとんどです。
Apache で拒んでいても、WordPress の側でも止めておくと、Apache で拒めないサーバー(レンタルサーバーなど)に移したときの守りになります。

<?php
// xmlrpc.php の、認証が要る操作(投稿など)を止める
add_filter( 'xmlrpc_enabled', '__return_false' );
?>
  • 止まるもの:投稿など、ユーザー名とパスワードが要る操作です。xmlrpc.php を使ったパスワードの総当たりも効かなくなります
  • 止まらないもの:ピンバックのような、認証の要らない操作です。名前に反して、xmlrpc.php そのものを止めるものではありません(WordPress のソースのコメントにも、そう書かれています)。細かく止めるときは、xmlrpc_methods のフィルターを使います
  • 置き場:テーマの functions.php に書きます。配られているテーマは、更新すると functions.php が上書きされるので、子テーマか、自作の小さなプラグインに書きます
  • Jetpack など、xmlrpc.php を使うサイト:このフィルターは入れません。認証が要る操作が止まり、動かなくなります

先に拒まれているものには、目印が付かない

.htaccess の書き換えは、Apache の中では遅い順番で動きます。サーバー管理者の設定(サーバー全体の設定や、サイトごとの VirtualHost)や、Require で先に拒まれたアクセスは、.htaccess まで来ません。

  • サーバー管理者の設定で拒んだもの:数えるかどうかは、そちらの設定で決まっています(許可する拡張子で拒んだものなどは数えません)。.htaccess に同じ規則があっても、届かないだけで害はありません
  • Require で拒んだもの(<Files> や <FilesMatch> の Require all denied など):拒めてはいますが、目印が付かないので、fail2ban には数えられません。数えたいときは、その設定を書いた人(サーバー管理者やプラグイン)と、書き換えに任せてよいかを相談します

サブディレクトリの .htaccess

mod_rewrite の書き換えは、サブディレクトリの .htaccess に RewriteEngine On があると、そのディレクトリの下では、親の .htaccess の書き換えを引き継ぎません。サブディレクトリに別の WordPress を置いているときなどは、そのディレクトリの .htaccess にも、同じまとまりを書きます。ホワイトリストの URL は、そのディレクトリから始まる形(例:!^/blog/(?:index|wp-login|…)\.php$)に直します。

拡張子の無い URL は扱わない

/login や /admin のような拡張子の無い URL は、WordPress の固定リンクと見分けが付かないので、この記事では扱いません。無いページなら、WordPress が 404 を返します。

WordPress のプラグインとの違い

WordPress のセキュリティのプラグインにも、404 が続く接続元を止める機能があるものがあります。プラグインは WordPress が動いてから判断するので、止めるまでのアクセスにも PHP と WordPress が動きます。この記事のやり方は、Apache の段で拒み、遮断したあとは firewalld で止めるので、PHP も WordPress も動きません。

PHP 以外のサイト

Ruby や Python などのアプリを、Apache からプロキシで渡しているサイトでは、.htaccess が効きません。考え方は同じで、そのサイトで開いてよい URL を決め、それ以外に目印を付けて拒みます。書く場所は、サーバー管理者と相談して、そのサイトの VirtualHost にします(サイトごとの設定の記事)。

まとめ

  • WordPress で外から開く .php は限られるので、ホワイトリストにして、それ以外を .htaccess で拒む
  • 拒むときに、fail2ban 向けの目印(中)を付ける。何回で遮断するかは、サーバー管理者が決める
  • ホワイトリストは WordPress の本体のものを基本にし、サイト独自のものを足す
  • 許可する拡張子は、.htaccess で外せる(目印は付けない)。足すのはサーバー管理者に頼む
  • Require で先に拒んでいるものや、自分の .htaccess を持つサブディレクトリには、この設定が効かない

サーバー管理者が行う設定は、次の記事で扱っています。