見つけた。では、どう止めるか
姉妹サイトの記事 「攻撃者は、パターンで浮かぶ」で、 アクセスログから攻撃者を検出するところまで来ました。この記事は、その続きです。 検出した相手を、実際に止める。
大事なのは 検出 ≠ 遮断 だということ。検出は「誰を止めるか」を決めるだけ。
実際に門を閉じるのは Apache の仕事です。その第一の道具が .htaccess。
ただし「IPを弾く」という単純な操作ですら、IPv4 / IPv6 / UA で書き方がまるで違います。
203.0.113.x(IPv4)と 2001:db8::/32(IPv6)はドキュメント用の予約レンジで、実在のアクセス元ではありません。
どこで、何を止めるか
.htaccess による遮断には、大きく3つの軸があります。IPアドレス(v4 / v6)と User-Agent。それぞれ別の構文が要ります。共通なのは「アプリに届く前、Apache 層で弾く」こと。 軽くて速い、第一の関門です。
ここで扱うのは .htaccess による静的な遮断——「リストに載っている相手」を弾くもの。頻度に応じて動的に弾く(APCu レート制限)のは、次回2本目の担当です。
IPv4 を弾く — RewriteCond
Apache 2.4 では、古い Deny from ではなく mod_rewrite や Require が主流です。
IPv4 はパターンで書きやすいので、RewriteCond で REMOTE_ADDR を見て弾きます。
RewriteEngine On # 単一IP/サブネット前方一致で弾く RewriteCond %{REMOTE_ADDR} ^203\.0\.113\.66$ [OR] RewriteCond %{REMOTE_ADDR} ^198\.51\.100\. RewriteRule .* - [F,L] # 403 Forbidden で即終了
[F] で 403 を返し、[L] でルール評価を打ち切ります。
RewriteRule 方式なら、[OR] で複数条件をまとめたり、前方一致でサブネットを丸ごと弾いたりと、柔軟に組めます。
IPv6 を弾く — RequireAll
IPv6 は勝手が違います。: を含む表記や省略記法があり、RewriteCond の正規表現では綺麗に書けません。
ここは mod_authz の Require not ip を使い、CIDR で範囲指定するのが素直です。
<RequireAll> Require all granted Require not ip 2001:db8::/32 # この範囲を拒否 Require not ip 2001:db8:dead::/48 </RequireAll>
RewriteCond、IPv6 は CIDR が素直なので Require。
無理に1つの書き方へ統一せず、それぞれが得意な構文を使う——これが実運用で落ち着いた形です。
User-Agent を弾く — 名乗りで止める
検出の記事で「UAは偽装できる」と書きました。だから UA ブロックは万能ではありません。 でも、正直に名乗る迷惑ボット——スクレイパやスキャナには有効です。既知の名前を軽く弾く用途に向きます。
RewriteCond %{HTTP_USER_AGENT} (EvilScanner|badbot|scanner) [NC] RewriteRule .* - [F,L] # [NC]=大文字小文字を無視
UA遮断は「既知の迷惑ボットを軽く弾く」用途。名乗りを偽装する本命の攻撃者は、頻度やパターン——次回2本目の APCu レート制限で止めます。静的リストと動的判定の二枚看板です。
AH01071 との戦い
これは PHP-FPM(FastCGI)が、存在しないスクリプトへのリクエストを受け取ってしまったときに出ます。
スキャナは /wp-login.php のような、このサイトに実在しない .phpを次々に叩きます。
.php を無条件で PHP-FPM に渡す設定だと、その存在しないパスがそのまま FPM に届き、AH01071 を量産します。
回避 — 存在するスクリプトだけ FPM へ渡す
handoff の前に「ファイルが実在するか」を確かめます。実在しない .php は FPM に渡さず、Apache 側で 404 にする。
これで AH01071 は止まります。
<FilesMatch \.php$> # 実在するスクリプトだけ PHP-FPM へ <If "-f %{REQUEST_FILENAME}"> SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost" </If> </FilesMatch>
順序 — 遮断は FPM 到達より前に効かせる
もう1つの勘所は順序です。遮断ルール([F,L])を、FPM への handoff より前に置く。
[L] で確実に打ち切れば、弾いた相手はそもそも FPM に届きません。「止める」を先、「渡す」を後——この並びが崩れると、
せっかく弾いたつもりのリクエストが FPM を叩いて AH01071 になります。
リクエストは、どこで弾かれるか
送信元をプリセットから選んで、リクエストを流します。IPv4 / IPv6 / UA の各関門を通過していき、
該当する関門で 403 [F] として弾かれます。正常なリクエストだけが PHP-FPM に到達します。
正常ユーザーは全関門を素通りして 200。ブロック対象は、それぞれ対応する関門で 403 として止まります。 ここで見えてくるのは静的遮断の限界です——「リストに載っている相手」しか止められない。 まだ弾くと決めていない相手、頻度で初めて怪しくなる相手は、この仕組みでは通ってしまいます。
静的遮断の、その先へ
.htaccess による静的遮断は、IPv4(RewriteCond)/ IPv6(RequireAll)/ UA(RewriteCond)を使い分けるのが要点でした。
そして AH01071 は遮断と FastCGI の噛み合わせ——実在チェックとルール順序で回避します。
この記事のまとめ
- IPv4 はパターンが効く
RewriteCond %{REMOTE_ADDR}+[F,L]。 - IPv6 は CIDR が素直な
<RequireAll>+Require not ip。 - UA ブロックは正直に名乗る迷惑ボット用。偽装する本命は頻度で止める。
- AH01071 は「存在しない .php を FPM に渡す」のが原因。実在チェックとルール順序で回避。
- 静的遮断は「リストに載った相手」限定。未知・高頻度は次の層へ。
静的リストで止まらない相手を、動的に止めます。APCu による 30req/60sec のレート制限、セッション uqid の失効、 サブネット単位のブロック(str_starts_with)——「リストに載る前」に、振る舞いで弾く層です。