タグ: apache Page 1 of 7

「Jailhouse Lock」の運用・トラブルシューティング(mod_rewriteで詰まったところ)

この記事の修正となります。

Webサーバーを公開していると、日常的に /wp-admin や /.git、生IPアドレスへのスキャン(Probe)が大量に飛んできます。

単に 404 や 403 を返すだけでも防御にはなりますが、今回は「悪意あるスキャナーやボットを検知し、専用のトラップ(檻)ページへ自動転送・隔離する」という多層防御システム(通称: Jailhouse Lock)を Apache + ModSecurity で構築しました。

構築の過程でハマった「設定の競合」と「ModSecurityのフェーズ問題」の解決策を記録として残します。

1. 目的

  1. 不正スキャンの隔離:
    隠しファイル(/.git)、WordPress探索(/wp-admin)、CGI探索(/cgi-bin)、生IP直撃などの不審なリクエストを検知し、アプリケーション(Rails/Node/PHP等)へ届く前に専用のトラップページ(檻)へ 302 Redirect する。
  2. 正常通信の保護:
    通常のブラウザからのアクセスや Let's Encrypt(ACME チャレンジ)などの正常な通信に一切影響を与えない。
  3. 無駄なログ・負荷の削減:
    ボットによる無駄なリクエスト処理コストを最小限に抑える。

2. 最初の設定と準備

(1) トラップページ(檻)ディレクトリとファイルの準備

トラップ先のコンテンツを配置するディレクトリとダミーファイルを作成します。

# 檻となるディレクトリの作成
sudo mkdir -p /var/www/jailhouse_trap

# トラップページの作成(例: Git用、WordPress用、CGI用、生IP/ボット用)
sudo touch /var/www/jailhouse_trap/git.html
sudo touch /var/www/jailhouse_trap/login.html
sudo touch /var/www/jailhouse_trap/cgi.html
sudo touch /var/www/jailhouse_trap/topgear.html

# 権限の調整
sudo chown -R www-data:www-data /var/www/jailhouse_trap

(2) Apache VirtualHost の基本設定(修正後)

諸々ハマったあとで最終的に決定した内容がこれです。

  • ドメイン名: example.com
  • ドキュメントルート: /var/www/my_app/public
  • トラップディレクトリ Alias: /__jailhouse_lock -> /var/www/jailhouse_trap
<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/my_app/public

    # ----- 檻(トラップベースディレクトリ)の定義 -----
    <IfModule mod_alias.c>
        Alias /__jailhouse_lock /var/www/jailhouse_trap

        <Directory /var/www/jailhouse_trap>
            Options -Indexes -ExecCGI
            AllowOverride None
            Require all granted
        </Directory>
    </IfModule>

    # ----- Jailhouse Lock (mod_rewrite トラップ群) -----
    <IfModule mod_rewrite.c>
        RewriteEngine On

        # ----- 0. ループ防止および例外通過(最優先判定) -----
        # 檻(トラップページ自身)と ACME チャレンジ(SSL更新)は即座に通過
        RewriteCond %{REQUEST_URI} ^/__jailhouse_lock [NC,OR]
        RewriteCond %{REQUEST_URI} ^/\.well-known/acme-challenge/ [NC]
        RewriteRule ^ - [L]

        # ----- 1. トラップ1: 隠しファイル / ドットディレクトリ探知 -----
        # /.git, /.env, /.htaccess 等
        RewriteRule ^/\. /__jailhouse_lock/git.html [R=302,L,E=dontlog:1]

        # ----- 2. トラップ1.5: WordPress探知 -----
        # /wp-admin, /wp-admin/*, /wordpress配下, /wp-login.php を隔離
        RewriteRule ^/(wp-admin|wordpress)(/.*)?$ /__jailhouse_lock/login.html [R=302,L,NC,E=dontlog:1]
        RewriteRule ^/wp-login\.php$ /__jailhouse_lock/login.html [R=302,L,NC,E=dontlog:1]

        # ----- 3. トラップ2: CGI探索者探知 -----
        # /cgi-bin配下、または .cgi, .pl, .py 拡張子を隔離
        RewriteRule ^/(cgi-bin|cgi-sys|cgi-mod)(/.*)?$ /__jailhouse_lock/cgi.html [R=302,L,NC,E=dontlog:1]
        RewriteRule \.(cgi|pl|py)$ /__jailhouse_lock/cgi.html [R=302,L,NC,E=dontlog:1]

        # ----- 4. トラップ3: 生IP直撃 / Hostヘッダー不正判定 -----
        # HostヘッダーがIPアドレス形式、または空の場合
        RewriteCond %{HTTP_HOST} ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(:[0-9]+)?$ [OR]
        RewriteCond %{HTTP_HOST} ^$
        RewriteRule ^.*$ /__jailhouse_lock/topgear.html [R=302,L,E=dontlog:1]

        # ----- 5. トラップ4: 悪質ボット隔離 -----
        RewriteCond %{ENV:bad_bot} ^1$
        RewriteRule ^.*$ /__jailhouse_lock/topgear.html [R=302,L,E=dontlog:1]

    </IfModule>
</VirtualHost>

3. 想定外の動きと原因(ハマりポイント)

構築中、/wp-login.php は正常に 302 で login.html に転送されるものの、/wp-admin や /wp-admin/admin-ajax.php などのディレクトリ配下へのアクセスが 404 Not Found になる現象が発生しました。

原因の調査で分かった「罠」は以下の2点です。

原因1:ModSecurity(Phase 1)による先行拒否

過去に ModSecurity 側に「WordPress 探索者を 404 で弾く」独自ルール(phase:1)を記述していました。

# [原因となった旧 ModSecurity ルール]
SecRule REQUEST_URI "@rx /(?:wordpress|wp-admin)" \
    "id:10002, phase:1, deny, status:404"

ModSecurity の phase:1(リクエストヘッダー受信直後)は、Apache の mod_rewrite 処理(Phase 2 相当)よりも前に実行されます。
そのため、mod_rewrite でリダイレクト(302)をかける前に ModSecurity が 404 を返してリクエストを終了させていました。

原因2:Alias と mod_rewrite のパス展開の競合

当初 /wp-admin を Alias /wp-admin /var/www/jailhouse_trap/login.html で処理しようとしたところ、/wp-admin/admin-ajax.php へのアクセスが /var/www/jailhouse_trap/login.html/admin-ajax.php という存在しないファイルパスへ展開され、Apache 内部で 404 が発生していました。

4. 修正内容

  1. ModSecurity 側の旧ルールの撤去:
    ModSecurity 側で 404 拒否していた旧ルールを削除し、URI 判定と転送ロジックをすべて mod_rewrite へ統一・一元化しました。
  2. Alias から mod_rewrite(正規表現捕獲)への切り替え:
    サブパスを持つ可能性のあるディレクトリトラップ(wp-admin や cgi-bin)は、Alias ではなく RewriteRule の正規表現 ^/(wp-admin|wordpress)(/.*)?$ を使用して配下ファイルごと一網打尽に捕捉するように変更しました。

5. 修正後の検証テスト

設定反映後、curl を使用して各エンドポイントの挙動をテストしました。

1 WordPress探索のテスト

# 末尾スラッシュなし
curl -i -A "Mozilla/5.0" "https://example.com/wp-admin"

# 配下ファイル
curl -i -A "Mozilla/5.0" "https://example.com/wp-admin/admin-ajax.php"

【結果】
いずれも HTTP/1.1 302 Found が返り、Location: [https://example.com/__jailhouse_lock/login.html](https://example.com/__jailhouse_lock/login.html) へ正しく誘導されました。

2 CGI探索のテスト

curl -i -A "Mozilla/5.0" "https://example.com/cgi-bin/test.cgi"

【結果】
HTTP/1.1 302 Found で Location: [https://example.com/__jailhouse_lock/cgi.html](https://example.com/__jailhouse_lock/cgi.html) へ誘導。

3 生IPアドレスアクセスのテスト

curl -i -k "https://192.0.2.1/"

【結果】
HTTP/1.1 302 Found で Location: [https://192.0.2.1/__jailhouse_lock/topgear.html](https://192.0.2.1/__jailhouse_lock/topgear.html) へ誘導後、ModSecurity との連携により無効化完了。

4 正常アクセス・ループ防止テスト

# 正常なブラウザ通信
curl -i -A "Mozilla/5.0" "https://example.com/"

# トラップページへの直接アクセス(ループ確認)
curl -i -A "Mozilla/5.0" "https://example.com/__jailhouse_lock/git.html"

【結果】
いずれもリダイレクトループを起こさず HTTP/1.1 200 OK が返り、正常なコンテンツが表示されました。

まとめ

今回のハマりポイントを通して、「WAF (ModSecurity) と Apache (mod_rewrite) の処理フェーズの違い」を理解することの重要性を痛感しました。

多くのクローラーは

  • IPアドレス直打ちである
  • ヘッダが欠損している
  • wp-adminやwp-loginを真っ先に狙ってくる
  • 隠しファイルを執拗に狙う

が私の経験則です。であれば、

  • IPアドレス直打ちやヘッダの欠損はまともにリクエストをさせない。(前段で弾く)
  • 存在しないパスや隠しファイルはダミーページに飛ばす

で、多くのノイズを減らすことができます。

攻撃者は何を探していたのか? ModSecurityのログから読み解く自動スキャンの狙い

Webサーバーを運用していると、ModSecurityやApacheのエラーログには毎日のように不審なアクセスが記録されます。

その多くは機械的なスキャンですが、中には「攻撃者が何を考えてこのURLを叩いているのか」がよく分かるものがあります。

今回は、筆者のサーバーで実際に検知したログをもとに、「攻撃者は何を探していたのか」「WAFはどこを危険と判断したのか」を読み解いてみます。

ログの内容抜粋

※ IPアドレスやドメイン名はダミーに置き換えています。

[Mon Sep 07 02:49:34.840627 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] ModSecurity: Warning. String match within "/content-encoding/ /proxy/ /lock-token/ /content-range/ /if/ /x-http-method-override/ /x-http-method/ /x-method-override/ /x-middleware-subrequest/ /expect/" at TX:header_name_920450_x-middleware-subrequest. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1228"] [id "920450"] [msg "HTTP header is restricted by policy (/x-middleware-subrequest/)"] [data "Restricted header detected: /x-middleware-subrequest/"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL-ENFORCEMENT"] [tag "capec/1000/210/272"] [hostname "example.com"] [uri "/_image"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.841346 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?i)(?:[/\\\\x5c]|%(?:2(?:f|5(?:2f|5c|c(?:1%259c|0%25af))|%46)|5c|c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|(?:bg%q|(?:e|f(?:8%8)?0%8)0%80%a)f|u(?:221[56]|EFC8|F025|002f)|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|1u)|0x(?:2f|5c))(?:\\\\.(?:%0[01]|\\\\?)?|\\\\?\\\\.?|%(?:2( ..." at REQUEST_URI_RAW. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "54"] [id "930100"] [msg "Path Traversal Attack (/../) or (/.../)"] [data "Matched Data: /../ found within REQUEST_URI_RAW: /_image?href=/../../../.env"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "example.com"] [uri "/_image"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.842572 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 35)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/_image"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.842998 2026] [security2:error] [pid 136747:tid 137621397292736] [client 192.0.2.100:55326] [client 192.0.2.100] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=35, detection=35, per_pl=35-0-0-0, threshold=5) - (Outbound Scores: blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) - (SQLI=0, XSS=0, RFI=0, LFI=30, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=35)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/var/www/app/public/404.html"] [unique_id "ap2nrj8r0OUiUBbK7B-mlAAAAMo"]
[Mon Sep 07 02:49:34.849032 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] ModSecurity: Warning. String match within "/content-encoding/ /proxy/ /lock-token/ /content-range/ /if/ /x-http-method-override/ /x-http-method/ /x-method-override/ /x-middleware-subrequest/ /expect/" at TX:header_name_920450_x-middleware-subrequest. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1228"] [id "920450"] [msg "HTTP header is restricted by policy (/x-middleware-subrequest/)"] [data "Restricted header detected: /x-middleware-subrequest/"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL-ENFORCEMENT"] [tag "capec/1000/210/272"] [hostname "example.com"] [uri "/google-credentials.json"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep 07 02:49:34.849243 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] ModSecurity: Warning. Matched phrase "credentials.json" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: credentials.json found within REQUEST_FILENAME: /google-credentials.json"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "example.com"] [uri "/google-credentials.json"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep 07 02:49:34.850394 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 10)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/google-credentials.json"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep07 02:49:34.850677 2026] [security2:error] [pid 136747:tid 137621380507328] [client 192.0.2.100:55444] [client 192.0.2.100] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=10, detection=10, per_pl=10-0-0-0, threshold=5) - (Outbound Scores: blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) - (SQLI=0, XSS=0, RFI=0, LFI=5, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=10)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/var/www/app/public/404.html"] [unique_id "ap2nrj8r0OUiUBbK7B-mlQAAAMw"]
[Mon Sep 07 02:49:34.990300 2026] [security2:error] [pid 136775:tid 137621634270912] [client 192.0.2.100:55394] [client 192.0.2.100] ModSecurity: Warning. String match within "/content-encoding/ /proxy/ /lock-token/ /content-range/ /if/ /x-http-method-override/ /x-http-method/ /x-method-override/ /x-middleware-subrequest/ /expect/" at TX:header_name_920450_x-middleware-subrequest. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1228"] [id "920450"] [msg "HTTP header is restricted by policy (/x-middleware-subrequest/)"] [data "Restricted header detected: /x-middleware-subrequest/"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/PROTOCOL-ENFORCEMENT"] [tag "capec/1000/210/272"] [hostname "example.com"] [uri "/keys/service-account.json"] [unique_id "ap2nrn53LMPC8yKIKKnxogAAAYU"]
[Mon Sep 07 02:49:34.991569 2026] [security2:error] [pid 136775:tid 137621634270912] [client 192.0.2.100:55394] [client 192.0.2.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/keys/service-account.json"] [unique_id "ap2nrn53LMPC8yKIKKnxogAAAYU"]

普通のブラウザは送らないHTTPヘッダ

最初にModSecurityが反応したのは次のルールでした。

HTTP header is restricted by policy
Restricted header detected: x-middleware-subrequest

この X-Middleware-Subrequest は、一般的なWebブラウザが送るヘッダではありません。これは Next.js が内部処理で利用する特殊なヘッダとして知られており、近年では

「このヘッダを偽装すれば認証を回避できないか」

という攻撃が数多く試されています。つまり攻撃者は、

「このサーバーは Next.js ではないか?」

という前提でアクセスを始めています。ModSecurityは「通常の利用者が送るはずのないヘッダ」と判断し、この時点で危険なアクセスとして記録しました。

本命は .env ファイル

続いて現れたのがこちらです。

/_image?href=/../../../.env

一見すると画像を要求しているように見えますが、本当の目的は画像ではありません注目すべきなのは

../../../.env

という部分です。

../../../ が意味するもの

../ は

「一つ上のディレクトリへ移動する」

という意味を持ちます。

つまり

../../../.env

とは、

Web公開ディレクトリ
        ↓
      ../
        ↓
      ../
        ↓
      ../
        ↓
       .env

というように、公開ディレクトリの外側にある設定ファイルを読もうとしていることになります。これは Path Traversal(ディレクトリトラバーサル) と呼ばれる典型的な攻撃手法です。

なぜ .env を狙うのか

最近のWebアプリケーションでは、

.env

に重要な設定が保存されています。

例えば

DB_PASSWORD=
API_KEY=
SECRET_KEY=
AWS_SECRET_ACCESS_KEY=
SMTP_PASSWORD=

などです。もし .env が取得できてしまえば、

  • データベースへ接続
  • クラウドサービスへの接続
  • メールサーバーの悪用
  • 他システムへの横展開

など、一気に被害が拡大する可能性があります。攻撃者にとって .env は「宝箱」のような存在なのです。

WAFは何を見ていたのか

今回のログでは

930100

と

930110

という二つのルールが繰り返し反応していました。

Rule 930100

こちらは

パストラバーサルらしい文字列

を検知するルールです。

単純な

../

だけではなく、

URLエンコードやUnicode表現など、回避を試みた表現まで幅広く検査します。

Rule 930110

こちらはもっと単純です。実際に

../

という並びを検出します。

つまり、

  • URI全体
  • パラメータ
  • エンコード表現

それぞれを別々に検査しているため、同じリクエストでも複数回ヒットしています。

次に探し始めたもの

攻撃者は .env の次に、

/google-credentials.json

へアクセスしました。さらに

/keys/service-account.json

というリクエストも続いています。これは Google Cloud や Firebase などで利用される サービスアカウント鍵 を探しています。これらのJSONファイルには

private_key
client_email
project_id

などが含まれており、環境によってはGoogle Cloud APIを操作できる権限が保存されています。そのためOWASP CRSには、

Restricted File Access Attempt

という専用ルールが用意されており、今回も即座にブロックされました。

WAFが35点という高スコアを付けた理由

最後には

Inbound Scores

blocking = 35
LFI = 30

という結果が記録されています。

OWASP CRSは、一つのルールだけでブロックするのではなく、「怪しい行動の積み重ね」を点数化します。

今回であれば、

  • 通常使われないHTTPヘッダ
  • パストラバーサル
  • .env を狙うアクセス
  • 危険なファイル名へのアクセス

といった複数の危険要素が重なった結果、35点 という非常に高い危険度になりました。

既定では5点程度でブロックされるため、このアクセスは「かなり悪質」と判断されたことになります。

このログから見える攻撃者の思考

このログを見ていると、攻撃者は無差別にURLを叩いているようでいて、実際には一定の手順に沿って行動していることが分かります。

  1. まず特殊なHTTPヘッダを送ってNext.js系のアプリケーションではないかを探る。
  2. 次に画像APIを悪用できないか試し、.env を読み出せるか確認する。
  3. 失敗すると今度はGoogle Cloudの認証情報やサービスアカウント鍵を探し始める。

つまり、この一連のアクセスは

「秘密情報(Secrets)がどこかに置きっぱなしになっていないか」

を高速で探し回る自動スキャンなのです。

サーバーそのものを壊すことが目的ではなく、認証情報を盗み出して、その先にあるデータベースやクラウドサービスへ侵入することが本当の狙いと言えるでしょう。

ログを一つひとつ眺めるだけでは「また攻撃が来た」で終わってしまいます。しかし、攻撃者の行動を時系列で追ってみると、「何を探していたのか」「WAFは何を危険だと判断したのか」が見えてきます。

WAFは単に「怪しい文字列」を検出しているのではありません。攻撃者の行動全体を点数化し、その積み重ねから危険度を評価しています。

Redmine 6.x移行記(後編)犯人はGooglebotだった。Apacheの410 GoneでPassengerを守るまで

前回の記事では、Redmine 6.1への移行後に Passenger の CPU 使用率が異常に高止まりし、production.log と netstat を追い始めたところまでを書きました。

調べていく中で、

  • 削除した Knowledgebase へのアクセス
  • sort= や set_filter= を含むチケット一覧
  • PDF や CSV、Atom のエクスポート要求

といった、一見すると関連性のないリクエストが大量に飛んできていることが分かってきます。そして最後に、

「もしかしてクローラーなのでは?」

という疑問が残りました。今回は、その続きを書いていこうと思います。

アクセス元を確認してみる

まずは production.log とアクセスログを突き合わせながら、アクセス元を確認してみました。

すると、見えてきたのは見慣れた IP アドレスです。

66.249.xx.xx

Googlebot でした。しかも一度や二度ではありません。HTTPS のセッションを何十本も張りながら、次々とリクエストを投げています。

しかもアクセスされていたのは、削除した Knowledgebase だけではありません。チケット一覧についても、

sort=
page=
set_filter=

といったパラメータ違いを延々と巡回しています。さらに、

issues.pdf
issues.csv
issues.atom

まで取得しようとしていました。普段ブラウザで Redmine を使っていると、これらは「便利な機能」という認識です。

ですが、サーバー側から見ると話は変わります。例えば PDF の生成であれば、データベースからチケットを取得し、テンプレートを組み立て、PDF をレンダリングし、レスポンスを返す。

HTML を返すだけとは比較にならないくらい重い処理になります。

つまり Googlebot は、リンクがあるから辿っているだけなのですが、Passenger からすると重い仕事ばかり持ち込まれている状態でした。

404でもPassengerは起きてしまう

Knowledgebase のアクセスについても同じです。

「存在しないページなんだから404で終わりでは?」

最初はそう考えていました。しかし実際には、Rails が起動し、ルーティングを行い、存在しないことを確認し、404 を返します。404 を返すこと自体は正しい動作です。ですが、その404を返すためにも Passenger は仕事をしています。

これでは、存在しないページへのアクセスであっても CPU を使い続けることになります。

ならばRailsまで届かせなければいい

ここで考え方を変えました。Rails が重いのではありません。Rails に仕事を渡してしまっていることが問題です。

であれば、Rails が起きる前にApacheで止めればいい。Apache を使っている以上、一番軽いのは Apache の段階で処理してしまうことです。

そこで mod_rewrite を使うことにしました。バーチャルホストの.confを以下のように変えていきます。

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

まずは削除済みの Knowledgebase。続いて、

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.(pdf|csv|atom)$ - [G,L]

PDF、CSV、Atom のエクスポート。さらに、

RewriteCond %{QUERY_STRING} (sort=|set_filter=|per_page=) [NC]
RewriteRule ^/(projects/[^/]+/)?issues - [G,L]

検索エンジンが総当たりしている動的クエリも Apache 側で受け止めることにしました。

404ではなく410を選んだ理由

404でも目的は達成できます。ですが、今回は410 Goneを選びました。

404エラーは、「今は見つからない」という意味です。検索エンジンから見ると、

「また後で来れば復活しているかもしれない」

という扱いになります。一方、410 Gone は、

「もう永久にありません。」

という宣言です。今回削除した Knowledgebase は、将来復活させる予定はありません。だったら、その意思を HTTP ステータスとして返した方が正確です。

robots.txtも追加した

もちろん、Apache の設定だけではありません。今後の巡回そのものを減らすため、robots.txt にも、

Disallow: /projects/*/knowledgebase*
Disallow: /*?*sort=
Disallow: /*?*set_filter=

などを追加しました。ただし、ここは誤解しやすいところです。robots.txt は即効性のある仕組みではありません。

既に巡回予定へ入っているリクエストは、そのまま飛んできます。まず Apache で止める。その上で robots.txt により今後の巡回を減らす。この二段構えにしました。

効果はすぐに現れた

設定を反映し、Passenger(mod_passengerなのでapacheそのもの)を再起動します。

その後、再び CPU 使用率を確認すると、先ほどまで70〜90%を推移していた Passenger が数%程度で落ち着いていました。

production.log からも

  • Knowledgebase の404、
  • IssuesController の大量アクセス、
  • 500.html

の RoutingError は姿を消しています。試しに curl から Googlebot の User-Agent を付けてアクセスしてもApache が Rails を起動することなく、即座に

HTTP/1.1 410 Gone

を返しました。狙い通りです。Passengerに到達すること無くは何も仕事をしていません。

今回学んだこと

今回改めて感じたのは、「プラグインを削除すること」と、「削除したことを検索エンジンへ伝えること」は全く別の話だということでした。

Redmine 側では綺麗にプラグインをアンインストールできていても、Google は昔の URL を覚えています。そして真面目に巡回を続けます。そのリクエストを Rails まで届けてしまえば、存在しないページであっても Passenger は起き、CPU を使います。

だからこそ、410エラーにより「もうありません。」という判断を Apache が先に行う。

今回のケースでは、この水際防衛が一番効果的でした。

Redmine に限らず、長く運用してきた Web アプリケーションでは、大きな整理やプラグインの削除を行った後、一度アクセスログを眺めてみることをおすすめします。

自分ではとっくに忘れていた URL を、検索エンジンは驚くほど律儀に覚えているものです。そして、それが思わぬサーバー負荷につながっていることも、決して珍しくありません。

余談「Apacheは実は柔軟」

これは私の持論なのですが、超大手プラットフォームや大手配信サイトではapacheは重い。nginxこそ主流だ的な記事を見かけますが筆者のように

多数のサーバを立てられない
それでもWebアプリを多数動かしたい
セキュリティも譲れない
場合はApacheの方が驚くほど柔軟なパターンがあります。その辺の話はまた改めて。

教材価値の高いログ:なぜ攻撃者は最初にWordPressを探すのか

筆者に限らず、Webサーバを運用すると必ずと言っていいほど見かけるアクセスがあります。

GET /wp-login.php
GET /wp-admin/
GET /wp-config.php
GET /config.php
GET /.env

言わずと知れたWordpress/laravel管理画面のURLやDB情報などが飛び交う重要ファイル。

「うちはWordPressじゃないんだけど?」

と思う人も多いでしょう。実際、筆者のVPSもWordPressを詰んでいません。(公開しているWordpressは専用サービスを借りています)

それでも毎日のように飛んできます。では、攻撃者は何を考えて「見たらアクセスするのが礼儀」レベルでアクセスしてきているのでしょうか。

インターネットは「総当たり」の世界

多くの人は、「誰かが自分のサイトを狙っている」と考えがちです。

しかし実際には逆です。攻撃者は運営元をまず見ません。彼らが見ているのは

「世界中に存在する何千万台ものWebサーバ」

です。ですから

WordPress
Laravel
Node.js
Next.js
Joomla
Drupal

といった利用者が非常に多い技術から順番に試します。

WordPressが最優先になる理由

WordPressは世界中のWebサイトのかなりの割合を占めています。つまり、攻撃者は

「WordPressだけ狙う」

だけでも膨大な成果が期待できます。さらに

  • 古いプラグイン
  • 更新されていないテーマ
  • 初期設定のままの管理画面

なども珍しくありません。攻撃者から見ると、WordPressは「狙う価値が非常に高い標的」です。

そして、Wordpressは

/wp-config.php
/wp-config-sample.php
/wp-login.php

など、「アキレス腱となるファイル」がほぼ決まっているという「やりやすい」材料です。

実際のログ

以下は実際に筆者のサーバへ届いたログを、IPアドレスやホスト名をダミー化したものです。

なぜ、この手の攻撃者情報を晒さないかというと「テロリストに名前を与える必要はない」という哲学と、「誰がやったかは問題ではない。私が興味があるのは何をしたかである」という考えです。

[Mon Jul 06 01:06:34.509624 2026] [security2:error] [pid 111111:tid 222222222222222] [client 192.0.2.1:40372] [client 192.0.2.1] ModSecurity: Warning. Matched phrase "config.php" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: config.php found within REQUEST_FILENAME: /config.php"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "example.com"] [uri "/config.php"] [unique_id "DUMMY_UNIQUE_ID_000001AAAAA"]
[Mon Jul 06 01:06:34.510601 2026] [security2:error] [pid 111111:tid 222222222222222] [client 192.0.2.1:40372] [client 192.0.2.1] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/config.php"] [unique_id "DUMMY_UNIQUE_ID_000001AAAAA"]
[Mon Jul 06 01:06:34.511256 2026] [security2:error] [pid 111111:tid 222222222222222] [client 192.0.2.1:40372] [client 192.0.2.1] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "119"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=5, detection=5, per_pl=5-0-0-0, threshold=5) - (Outbound Scores: blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) - (SQLI=0, XSS=0, RFI=0, LFI=5, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=5)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/__jailhouse_lock/topgear.html"] [unique_id "DUMMY_UNIQUE_ID_000001AAAAA"]
[Mon Jul 06 01:06:40.426322 2026] [security2:error] [pid 333333:tid 444444444444444] [client 192.0.2.1:35882] [client 192.0.2.1] ModSecurity: Warning. Matched phrase "wp-config-" at REQUEST_FILENAME. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: wp-config- found within REQUEST_FILENAME: /wp-config-sample.php"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "example.com"] [uri "/wp-config-sample.php"] [unique_id "DUMMY_UNIQUE_ID_000002BBBBB"]
[Mon Jul 06 01:06:40.427672 2026] [security2:error] [pid 333333:tid 444444444444444] [client 192.0.2.1:35882] [client 192.0.2.1] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/usr/share/modsecurity-crs/coreruleset/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.28.0"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/wp-config-sample.php"] [unique_id "DUMMY_UNIQUE_ID_000002BBBBB"]
[Mon Jul 06 01:06:40.428290 2026] [security2:error] [pid 333333:tid 444444444444444] [client 192.0.2.1:35882] [client 192.0.2.1] ModSecurity: Warning. Unconditional match in SecAction. [file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-980-CORRELATION.conf"] [line "119"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=5, detection=5, per_pl=5-0-0-0, threshold=5) - (Outbound Scores: blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) - (SQLI=0, XSS=0, RFI=0, LFI=5, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=5)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "example.com"] [uri "/__jailhouse_lock/topgear.html"] [unique_id "DUMMY_UNIQUE_ID_000002BBBBB"]

要約するとこんな形です。

[client 192.0.2.1]
GET /config.php

REQUEST-930
Restricted File Access Attempt

↓

REQUEST-949
Inbound Anomaly Score Exceeded

↓

HTTP 403

数秒後、

GET /wp-config-sample.php

REQUEST-930
Restricted File Access Attempt

↓

REQUEST-949
Inbound Anomaly Score Exceeded

↓

HTTP 403

となりました。

このログから読み取れること

筆者がこのログに着目したのは

config.php
↓

wp-config-sample.php

という順番です。これは、

  1. 「PHPかな?」
  2. 「WordPressかな?」

という判定ルーチンそのもの。人間ならこの順番にはなりません。典型的な自動スキャナです。

さらに注目したい点

以前紹介した、ModSecurityのカスタム設定。

HostヘッダーがIPアドレスなら遮断

Hostヘッダーが存在しなければ遮断

というカスタムルール。今回は一切反応しませんでした。

つまり攻撃者は

  • DNSで名前解決
  • 正しいHostヘッダーを付与
  • 普通のHTTPリクエスト

を組み立てています。IPアドレスへ適当に投げるだけのBOTより一段階上です。

そのためにModSecurityは通常の検査を実施し、

REQUEST-930
↓
REQUEST-949
↓
403

という流れで処理しています。

筆者が「教材」と述べた面白い理由

このログには、

OWASP CRSの典型的な動きがそのまま残っています。

CRS:930
危険なリクエストを検知
↓
CRS:949
異常スコアが閾値を超えた
↓
処理:403アクセス拒否

つまり、「ModSecurityがどのように判断して遮断するのか」を学ぶ教材として非常に分かりやすいログです。

そして一番重要なこと

今回の攻撃は、筆者個人を狙ったものではありません。世界中のWordPressサイトを探す途中で、たまたま筆者のLAMPサーバへ届いただけです。(何せ、動いているのは全く別のCMSです)

しかし、その「たまたま」からでも学べることはたくさんあります。

ログには、

  • 攻撃者が何を探しているのか
  • どんな順番で調べるのか
  • WAFがどこで判断したのか

がすべて残っています。

サーバ管理者にとってログは「嫌なもの」ではなく、攻撃者の思考を無料で教えてくれる教材です。

Jailhouse Lockシステム技術スタック解説:mod_rewriteによるクローラー誘導作戦。

こちらの記事では、最速のナイフであるmod_aliasと、強力な拳銃であるmod_rewriteの特徴、そして「速度と役割のトレードオフ」についてお話ししました。

ここからは、筆者が実際に運用している atelier.reisalin.com の設定ファイルを元に、この2つの武器をどのように組み合わせ、悪意あるボットや過剰なクローラーを迎撃しているのか。その具体的な防衛システム、名付けて「Jailhouse Lock(ジェイルハウスロック)」の核となるmod_rewriteモジュールについてお話しします。

mod_rewriteとは?

mod_rewriteは、リクエストされたURLをファイルシステム上の特定の場所にマッピング(転送・代替)したり、別のURLへリダイレクトしたりするための、Apacheの拡張モジュールです。(そのため、多くのディストリビューションではインストールと同時に機能が有効化されていないことが多数あります。)

Ubuntu系であれば

sudo a2enmod rewrite
sudo systemctl restart apache2.service

で有効化されます。

再掲:mod_alias との違い

どちらも「URLを操作する」という意味では同じですが、そのアプローチと内部処理の複雑さに大きな違いがあります。

項目mod_aliasmod_rewrite
コンセプト単純なマッピングとリダイレクト強力なURLカスタマイズと書き換え
判定基準単純な前方一致(接頭辞マッチ)正規表現、Cookie、環境変数、時間など
処理速度非常に高速(軽量)やや低速(ルール毎に正規表現エンジンが動く)
設定の難易度簡単(初心者向け)複雑(記述ミスで無限ループが起きやすい)
主な用途フォルダの共通化、単純なサイト引っ越し綺麗に整形されたURL(Smart URL)の実現、複雑な条件分岐

最大の違い:正規表現と条件分岐の有無

mod_aliasは、URLの「先頭が一致しているか」という単純な比較しか行いません。

一方、mod_rewriteは「リクエストがGoogle Chromeから来たら」「平日の昼間なら」「Cookieに特定の文字が含まれていたら」といった、高度な条件分岐(RewriteCond)や、正規表現を使った自由自在なURLの作り替え(RewriteRule)が可能です。

筆者の設定内容抜粋

まずは、実際に稼働している設定の抜粋をご覧ください。

<VirtualHost *:80>
    ServerName atelier.reisalin.com
    RewriteEngine On
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName atelier.reisalin.com

    # ----- ログの選別と抑制 -----
    SetEnvIf Remote_Addr "192.0.2.1" dontlog
    SetEnvIfNoCase User-Agent "Googlebot" dontlog
    SetEnvIfNoCase User-Agent "GoogleOther" dontlog

    # ModSecurity等、WAFで捕獲したエラーはログを肥大化させない
    SetEnvIf MSC_RULE_ID "4009" dontlog
    CustomLog /var/log/apache2/atelier_access.log combined env=!dontlog

    DocumentRoot /home/www-data/atelier/public

    # --------------------------------------------------
    # 第一層:【mod_alias】によるハニーポットへの一撃転送
    # --------------------------------------------------
    <IfModule mod_alias.c>
        # 監獄(Jailhouse)となる隔離ディレクトリを定義
        Alias /__jailhouse_lock /home/www-data/nepenthes

        # 存在しないはずの脆弱性パスへのアクセスを最速でトラップへ
        Alias /wp-login.php   /home/www-data/nepenthes/login.html
        Alias /wp-admin       /home/www-data/nepenthes/login.html
        Alias /.git           /home/www-data/nepenthes/git.html
        Alias /assets-archive /home/www-data/nepenthes/login.html

        <Directory /home/www-data/nepenthes>
            Options -Indexes
            AllowOverride None
            Require all granted
        </Directory>
    </IfModule>

    # --------------------------------------------------
    # 第二層:【mod_rewrite】による動的・精密迎撃
    # --------------------------------------------------
    <Directory /home/www-data/atelier/public>
        Options -MultiViews
        AllowOverride All
        Require all granted

        <IfModule mod_rewrite.c>
            RewriteEngine On

            # 【迎撃フェーズ1】悪質botフラグが立っている通信の隔離
            RewriteCond %{ENV:bad_bot} =1
            RewriteCond %{REQUEST_URI} !^/__jailhouse_lock/
            RewriteRule ^.*$ /__jailhouse_lock/topgear.html [L,E=dontlog:1]

            # 【迎撃フェーズ2】特定の重いコンテンツを狙った過剰クローラー対策
            SetEnvIf Request_URI "^/projects/zettel" is_zettel

            # 大量のカンマ区切り(%2C)や、極端に長い(150文字以上)悪意あるクエリを検知
            SetEnvIf Query_String "tag=.*(%2[cC]|,|%e3%80%81).*(%2[cC]|,|%e3%80%81).*(%2[cC]|,|%e3%80%81)" bad_tag_stacking
            SetEnvIf Query_String "tag=.{150,}" bad_tag_stacking

            # 特定ページ、かつ異常クエリというパズルが揃った瞬間に500エラーで即撃破
            RewriteCond %{ENV:is_zettel} 1
            RewriteCond %{ENV:bad_tag_stacking} 1
            RewriteRule ^ - [E=dontlog:1,R=500,L]
        </IfModule>
    </Directory>
</VirtualHost>

防衛ストーリー:ナイフで受け流し、拳銃で仕留める

この「Jailhouse Lock」システムは、先の記事で述べたmod_alias(ナイフ)とmod_rewrite(拳銃)の特性をコンビネーションとして昇華させたものです。

この言葉は大好きなので何度でも再掲しますが:『MASTERキートン』のプロフェッサーの言葉。

「やめておけ」
「…………」
「拳銃の方が、ナイフよりも速いと思っているんだろう。
 だが、拳銃はデリケートな道具だ。
 弾が出ないかもしれないし、
思い通り的に当たるとは限らん。
おまけに拳銃は、
抜き、構え、引き金を引くまでに三動作(スリーアクション)……
 その点ナイフは、一動作(ワンアクション)で終わる。
この距離なら、絶対に俺が勝つ!!
どうする? それでもやってみるかね?」

これはナイフの方が有利という単純な比較論ではなく、

「この距離ではナイフが有用」だが、「ある程度のアクションや破壊力が必要な場合は拳銃が必要なるケース」というお話と筆者は捉えています。

第一層(mod_alias):無差別スキャンを「無駄なく」いなす

サイトがWordPressで作られていようがいまいが、botは機械的に /wp-login.php や /.git を探して叩いてきます。これらはURLの文字列そのものが標的であるため、複雑な条件分岐は不要です。

ここで登場するのがナイフの速度を持つ mod_alias です。

        # 監獄(Jailhouse)となる隔離ディレクトリを定義
        Alias /__jailhouse_lock /home/www-data/nepenthes

        # 存在しないはずの脆弱性パスへのアクセスを最速でトラップへ
        Alias /wp-login.php   /home/www-data/nepenthes/login.html
        Alias /wp-admin       /home/www-data/nepenthes/login.html

この部分。ボット、クローラー、悪意ある攻撃者が挨拶代わりに上記のサイトへのURLに一致した瞬間に、メインアプリケーション(PHPなど)を一切起動させることなく、最速でハニーポット(ダミーのHTML空間 nepenthes)へと受け流します。サーバーのリソースを1ミリも無駄にしない、ナイフならではの「一動作(ワンアクション)」の防御です。

第二層(mod_rewrite):パズルを解き明かし「精密に」射抜く

しかし、中には特定のコンテンツ(例:負荷の重い検索機能など)に対して、URLの引数(クエリ)を巧妙に変えながら何度もクロールしてくる、たちの悪いボットがいます。

URLの文字しか見られない mod_alias では、URLの後ろに付く ?tag=xxx の中身まで検知することはできません。ここで拳銃である mod_rewrite を引き抜きます。

RewriteCond %{ENV:is_zettel} 1
RewriteCond %{ENV:bad_tag_stacking} 1
RewriteRule ^ - [E=dontlog:1,R=500,L]

「特定のコンテンツへのアクセスである(is_zettel)」
「かつ、タグの指定が異常に長い、または大量に詰め込まれている(bad_tag_stacking)」

この複数の条件が上から順にカチリと噛み合った瞬間、mod_rewrite は狙い違わず [R=500,L] という弾丸を放ち、リクエストを強制終了させます。同時に [E=dontlog:1] フラグを立てることで、ボットの嫌がらせによる「ログファイルの肥大化(ディスク容量の圧迫)」という二次災害まで綺麗に防いでみせるのです。

なぜ nginx ではなく、Apache なのか?

近代的なWebサーバーとしてnginxが台頭する中、なぜ筆者がApacheを用いる理由は以下の通り。

Apacheの持つ「上から下に流れる手続き型の思考」が、このような防衛ロジックを組む際に圧倒的に人間にとって直感的だからです。

もし、これと同じ「AかつBの条件を満たした時だけ、500エラーで弾いてログを消す」という処理をnginxで実装しようとすると、事情が変わってきます。nginxには条件を連鎖させる RewriteCond のような仕組みがなく、また if 文の挙動が非常にデリケート(通称:If is Evil)であるため、以下のような不格好な「変数ハック」を強いられます。

例えば、これと同等の機能をnginxでやろうとすると

# nginxで複数の動的条件を重ねる場合の苦肉の策
set $block_trigger "";
if ($is_zettel) { set $block_trigger "Y"; }
if ($bad_tag_stacking) { set $block_trigger "${block_trigger}Y"; }
if ($block_trigger = "YY") {
    return 500;
}

かなりifの入れ子構造が面倒になります。それに対し、Apacheは「この条件を満たし、さらにこの条件も満たしたら、このルールを適用して終了!」と、設計思想をそのまま設定にぶち込む強みがあります。

結び:適材適所の美学

強力な道具は、それ単体で振り回しても真価を発揮しません。

  • 固定の攻撃パスは、システムに最も負荷をかけない mod_alias で門前払いする。
  • 動的なパラメータや環境変数が絡む高度な迎撃は、mod_rewrite の柔軟性を活かしてピンポイントで処理する。

速度のナイフと、射程の拳銃。

この2つのモジュールを「適切なコンテキスト」で併用することで、設定ファイルそのものである程度の防御は出来るというお話です。

余談ではありますが:mod_rewriteは強力ですが、設定を一行誤れば自分自身が500エラーの被害者になります。

「『いい鉄砲は打ち手を選ぶ』ってことわざ知ってるか?
威力のある鉄砲は その分扱いも難しく危険
だから未熟者が使うと打ち手の方がケガをするってことさ」

という『ONE OUTS』で渡久地が言った言葉を持って、本記事を締めくくります。

Apacheモジュール「mod_alias」解説。

筆者がWebサイトの防衛に、そして過剰にクロールをするボットを他の場所へとご案内するために用いているmod_aliasのご紹介です。

というのも、筆者がこれから述べる対クローラー迎撃システム「Jailhouse Lock」には、このモジュールが必要だからです。

1. mod_aliasとは何か?

mod_aliasは、リクエストされたURLをファイルシステム上の特定の場所にマッピング(転送・代替)したり、別のURLへリダイレクトしたりするための、Apacheの標準モジュールです。(そのため、多くのディストリビューションではインストールと同時に機能が有効化されています)

主な役割は以下の2つです。

  • エイリアス(別名)の設定:
    • ドキュメントルート(公開ディレクトリ)の外側にあるフォルダを、あたかも内側にあるかのようにURLに紐付けます。
  • 単純なリダイレクト:
    • 特定のURLに来たアクセスを、別のURL(別ドメインなど)へ転送します。

代表的なディレクティブ

  • Alias /images /var/www/shared/images (URLの/imagesを特定のフォルダに対応付ける)
  • Redirect permanent /old.html http://example.com/new.html (永続的な301リダイレクト)

2. mod_rewrite との違い

どちらも「URLを操作する」という意味では同じですが、そのアプローチと内部処理の複雑さに大きな違いがあります。

項目mod_aliasmod_rewrite
コンセプト単純なマッピングとリダイレクト強力なURLカスタマイズと書き換え
判定基準単純な前方一致(接頭辞マッチ)正規表現、Cookie、環境変数、時間など
処理速度非常に高速(軽量)やや低速(ルール毎に正規表現エンジンが動く)
設定の難易度簡単(初心者向け)複雑(記述ミスで無限ループが起きやすい)
主な用途フォルダの共通化、単純なサイト引っ越し綺麗に整形されたURL(Smart URL)の実現、複雑な条件分岐

最大の違い:正規表現と条件分岐の有無

mod_aliasは、URLの「先頭が一致しているか」という単純な比較しか行いません。

一方、mod_rewriteは「リクエストがGoogle Chromeから来たら」「平日の昼間なら」「Cookieに特定の文字が含まれていたら」といった、高度な条件分岐(RewriteCond)や、正規表現を使った自由自在なURLの作り替え(RewriteRule)が可能です。

3. mod_aliasが「優れているパターン」

「mod_rewriteがあれば、mod_aliasはいらないのでは?」と思われがちですが、Apache公式も「単純なタスクであれば、mod_rewriteではなくmod_aliasを使うべき」と推奨しています。

mod_aliasを選ぶべき(優れている)具体的なパターンは以下の3つです。

① サーバーのパフォーマンス(速度)を最優先したいとき

mod_rewriteは、リクエストが来るたびに複雑な正規表現エンジンを動かすため、CPUリソースを消費します。

大規模サイトやアクセスが集中する環境で、単なるURLの転送やフォルダの紐付けを行う場合、mod_aliasの方が圧倒的に処理が軽く、サーバーの負荷を抑えられます。

② ドメイン全体の引っ越しや、単純なURL変更(リダイレクト)

サイトのリニューアルで、古いページから新しいページへ1対1で転送したい場合や、古いドメインから新ドメインへ丸ごと転送したい場合は、mod_aliasの Redirect で十分対応できます。

  • サイト全体を新ドメインへリダイレクト(mod_alias)
Redirect permanent / https://new-example.com/

これをmod_rewriteで書くと記述が複雑になり、設定ミスのリスク(無限ループなど)が高まります。

③ 複数サイトで画像やアセット用のフォルダを安全に共有したいとき

例えば、サーバー内の /var/www/common_assets/ というフォルダを、複数のWebサイトで /assets というURLで共通利用したい場合です。

  • 安全かつ高速に共通フォルダをマッピング(mod_alias)
Alias /assets /var/www/common_assets

mod_aliasは設定がシンプルなため、ヒューマンエラーによるセキュリティホールの発生(意図しないシステムファイルの公開など)を防ぎやすいというメリットもあります。

それぞれのイメージ

1. mod_alias のシーケンス(単純・高速)

URLの書き換えは行わず、リクエストされたURLの先頭(接頭辞)だけをチェックして、即座にファイルパスへマッピングするか、リダイレクトを返すシンプルな流れです。

sequenceDiagram autonumber participant Client as クライアント (ブラウザ) participant Core as Apache コア (リクエスト受付) participant Alias as mod_alias participant FS as ファイルシステム Client->>Core: HTTPリクエスト (例: /images/logo.png) Core->>Alias: URLの評価を依頼 Note over Alias: URLの先頭(接頭辞)が<br>設定と一致するか単純比較 Alias-->>Core: マッピング先を返却<br>(例: /var/www/shared/images/logo.png) Core->>FS: 指定されたパスのファイルを読み込み FS-->>Core: ファイルデータ Core-->>Client: HTTP 200 OK (レスポンス返却)

2. mod_rewrite のシーケンス(複雑・高機能)

リクエスト受付後、内部でループ(再エントリー)が発生する可能性や、条件判定(RewriteCond)のステップが挟まるため、柔軟ですが処理の工程が多くなります。

sequenceDiagram autonumber participant Client as クライアント (ブラウザ) participant Core as Apache コア (リクエスト受付) participant Rewrite as mod_rewrite participant FS as ファイルシステム Client->>Core: HTTPリクエスト (例: /user/profile) rect rgb(240, 245, 255) Note over Core, Rewrite: [書き換えループの開始] Core->>Rewrite: URLの評価を依頼 Note over Rewrite: 1. 正規表現でURLパターンをマッチング Note over Rewrite: 2. RewriteCondの条件をチェック<br>(Cookie、ブラウザ、環境変数など) Rewrite->>Rewrite: 3. URLの書き換え処理を実行<br>(例: /index.php?mod=user&act=profile) Rewrite-->>Core: 書き換え後の内部URLを返却 end Core->>Core: 内部リダイレクト(新しいURLで再処理) Core->>FS: 最終的なスクリプトやファイルを呼び出し FS-->>Core: 実行結果・データ Core-->>Client: HTTP 200 OK (レスポンス返却)
  • mod_alias(図1):
    • 判定が「前方一致のみ」の一方通行であるため、ステップ数が少なく、非常に高速に処理が終わります。
  • mod_rewrite(図2):
    • 条件チェックのフェーズ(薄い青枠の部分)が多く、さらに書き換えたURLでApache内部で「もう一度リクエストを処理し直す(内部リダイレクト)」という挙動が発生するため、機能性と引き換えに処理が重くなる構造が見て取れます。

まとめ:使い分けの基準

それぞれの使い分けです。

  1. まずは mod_alias で実現できないか考える。(単なる転送、単なるフォルダの紐付けなど)
  2. 「クエリストリング(?id=123など)を判定したい」「特定のブラウザだけ除外したい」「正規表現でURLを激しく書き換えたい」といった複雑な要件が出てきたとき初めて mod_rewrite を導入する。

どちらが優れているか、否かという問題ではなく、これは速度の問題です。

漫画『MASTERキートン』に以下のやりとりがあります。

「やめておけ」
「…………」
「拳銃の方が、ナイフよりも速いと思っているんだろう。
 だが、拳銃はデリケートな道具だ。
 弾が出ないかもしれないし、
思い通り的に当たるとは限らん。
おまけに拳銃は、
抜き、構え、引き金を引くまでに三動作(スリーアクション)……
 その点ナイフは、一動作(ワンアクション)で終わる。
この距離なら、絶対に俺が勝つ!!
どうする? それでもやってみるかね?」

これは「至近距離でのナイフの有用性」を示したものであり、実際にその通りだという説得力があるものです。(調査結果はこの記事です)

つまり、mod_alias は「できることが少ないから速い」のではなく、「役割を絞っているから速い」のです。

mod_aliasはナイフの速度。mod_rewriteは拳銃のような強力さがあります。「適切な距離感」でこれらのツールの利用、併用を行いましょうというお話しです。

AdGuardの管理画面をSSL化するための手順

前回の続き、導入したAdGuardの管理画面を常時SSL化していく作業です。

自宅内NWに立てることを前提としていても

「SSL化しておかないと寝覚めが悪い」

という性分のため、これを実施します。

環境

  • Ubuntu 26.04
  • Apache 2.4
  • AdGuard導入済み(ポートを8080に変更)

そもそもの問題として

「なんでリバースプロキシーなのにnginxじゃあないのか」

という方がいるかと思いますが、

「apacheでもこの設定は十分可能」

という例示のためです。

前提

  • AdGuardの管理画面に紐付くDNS設定が完了している。
  • そのドメインに即した証明書がある。

さっくりとした手順

  1. Apacheのプロキシモジュールを有効化します。
  2. Apacheのログディレクトリを作成します。
  3. AdGuard管理画面用のバーチャルサイトを作成します。
  4. ログのローテーション設定を行います。
  5. 設定を反映させます。
  6. 動作を確認します。

Apacheのプロキシモジュールを有効化する

まずはApacheがポート:8080を待ち受けてリバースプロキシサーバーとして動けるように、必要なモジュールを有効化します。

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod rewrite
sudo a2enmod ssl

有効化したら、一度Apacheを再起動しておきます。

sudo systemctl restart apache2

ログディレクトリの作成

この段階でログディレクトリを作成する理由は

「この段階でやっておかないと忘れるから」

に尽きます。ログは障害の切り分けとして極めて重要です。特にAdGuardは自宅内のNWをほぼ司ります。このときに何か異常が無いかを調べるためにも今の段階で作ります。

  • ログディレクトリの作成
sudo mkdir /var/log/adguard

※名前は自分が管理しやすい者に変更してください。

  • ログディレクトリの所有者変更
sudo chown -R www-data:www-data /var/log/adguard

これは筆者の好みの問題です。(ログローテートの際にwww-dataが参照できるようにするため)

  • ログディレクトリの所有者変更確認
ls -ld /var/log/adguard

所有者とグループがwww-dataを確認します。

管理画面用のバーチャルサイト設定ファイル作成

/etc/apache2/sites-available 内に、adguard.conf等の分かりやすい名前のファイルを管理者権限で作成します。

※ドメイン名は確実に自分の環境に合わせてください

<VirtualHost *:80>
    ServerName adguard.example.com
    # HTTPでアクセスされた場合は自動的にHTTPSへリダイレクト
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName adguard.example.com

    #ログ設定
    ErrorLog /var/log/adguard/adguard_ssl_error.log
    CustomLog /var/log/adguard/adguard_ssl_access.log combined

    # SSL設定
    SSLEngine on
    SSLCertificateFile    /etc/certs/あなたの証明書.crt
    SSLCertificateKeyFile /etc/private/あなたの秘密鍵.key
    # 中間証明書(CA Bundle)がある場合は、下の行のコメントアウトを解除してパスを指定してください
    # SSLCertificateChainFile /etc/certs/中間証明書.crt

    # プロキシ設定(Apacheが受けて後ろのAdGuardに流す)
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>

ログローテーションファイルの作成

/etc/logrotate.d/配下にadguard等の名前をつけて、以下の通り設定します。

※ログディレクトリは設定したディレクトリです。以下は一例なので好みに合わせて設定ください。

/var/log/adguard/*.log {
    daily
    missingok
    rotate 10
    compress
    copytruncate
    notifempty 
    su www-data www-data
}

設定反映

  • バーチャルサイト設定ファイルの登録
sudo a2ensite adguard.conf

※作成したファイル名です

  • ファイルの整合性確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • Apache再起動
sudo systemctl restart apache2.service
  • Apache再起動確認
systemctl status apache2.service

active(running)を確認します。

動作確認

自分のブラウザで

https://adguard.example.com

など、設定したURLにアクセスします。

  1. 設定した管理画面にドメインだけで入れること(ポート番号の付与などが必要ないこと)
  2. SSLで通信できること

を確認します。

ログの所有者変更

これは、筆者の設定の問題。www-dataにしている人向け。

なぜなら、サイトの設定反映後、基本的に/var/log/adguard配下で作成されたログはroot権限で作成されます。しかし、ログローテーションはwww-dataでローテーションするようになっています。これを合わせます。

sudo chown www-data:www-data /var/log/adguard/*.log

以上で「AdGuardを本格的に運用する準備」が整いました。

設定したApacheによる動きの解説

先ほどのエントリーの続きです。

「実際に筆者が施している」

Apache設定を元に「どういう動きをしているのか」を紹介します。

サンプル例

サイト名やIPアドレスなどはダミーにしています。

# ============================================================
# 【HTTP: 80番ポート】常時HTTPS(SSL/TLS)へのリダイレクト
# ============================================================
<VirtualHost *:80>
    ServerName example.com

    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

# ============================================================
# 【HTTPS: 443番ポート】要塞化されたメインサーバー設定
# ============================================================
<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/html/public

    # --------------------------------------------------------
    # 1. アクセスログの間引き(ノイズカット)設定
    # --------------------------------------------------------
    # 自身の監視用IPなど、ログに記録させたくないIPを指定
    SetEnvIf Remote_Addr "192.168.1.100" dontlog
    SetEnvIf Remote_Addr "10.0.0.1" dontlog

    # 検索エンジンのクローラー(健全なBot)もログから除外してノイズを減らす
    SetEnvIfNoCase User-Agent "Googlebot" dontlog
    SetEnvIfNoCase User-Agent "GoogleOther" dontlog

    # 【外部ファイル連携】定期的・自動的に更新する悪質ボットのリストを読み込む
    # ※ファイル内で「SetEnv bad_bot 1」などのフラグを立てる想定
    Include /etc/apache2/conf-available/blacklist-bots.txt

    # dontlogフラグが付いたアクセスは記録しない
    CustomLog /var/log/apache2/example_access.log combined env=!dontlog
    ErrorLog /var/log/apache2/example_error.log

    # --------------------------------------------------------
    # 2. メインディレクトリ制御(mod_rewrite × 環境変数)
    # --------------------------------------------------------
    <Directory /var/www/html/public>
        Options -MultiViews
        AllowOverride All

        # 大量の拒否アクセスによるエラーログ肥大化を抑制(重大エラーのみ記録)
        LogLevel authz_core:crit

        <IfModule mod_rewrite.c>
            RewriteEngine On

            # 【攻撃検知】特定の動的ページに対する「大量のタグ連結」などのボット挙動を迎撃
            # 例:特定のURLパスにアクセスがあり、かつクエリにURLエンコードされたカンマ等が大量に含まれる場合
            SetEnvIf Request_URI "^/search/tags" is_target_page
            SetEnvIf Query_String "tag=.*(%2c|,|%e3%80%81).*(%2c|,|%e3%80%81).*(%2c|,|%e3%80%81)" bad_tag_stacking

            # 条件に合致したら、ログを残さず「404 Not Found(存在しない)」を返して虚無へ葬る
            RewriteCond %{ENV:is_target_page} 1
            RewriteCond %{ENV:bad_tag_stacking} 1
            RewriteRule ^ - [E=dontlog:1,R=404,L]

            # 【外部ファイル連携】スパムIPアドレスのブラックリストを読み込んで404を返す
            Include /etc/apache2/conf-available/spam-ips.txt
        </IfModule>

        # 【アクセス拒否】上記の設定や外部リストで「bad_bot」と判定された通信を遮断
        <RequireAll>
            Require not env bad_bot
            Require all granted
        </RequireAll>
    </Directory>

    # 403 Forbidden(拒否)を返すと攻撃者に「防御されている」とバレるため、
    # 403の際も「404 Not Found(最初から何も無い)」として処理する
    ErrorDocument 403 /404.html

    # --------------------------------------------------------
    # 3. セキュリティヘッダーの強化(mod_headers)
    # --------------------------------------------------------
    Header always set Strict-Transport-Security "max-age=63072000"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-XSS-Protection "1; mode=block"

    # --------------------------------------------------------
    # 4. Nepenthes(ウツボカズラ)トラップ設定(mod_alias)
    # --------------------------------------------------------
    # 攻撃者が盲目的にスキャンしてくる「脆弱性のあるパス」を、ダミーの静的ファイルへ吸い込ませる
    <IfModule mod_alias.c>
        # WordPressを使っていない(または構成が違う)のに狙われるパス
        Alias /wp-login.php /var/www/nepenthes/dummy_login.html
        Alias /wp-admin     /var/www/nepenthes/dummy_login.html
        Alias /wordpress    /var/www/nepenthes/dummy_login.html

        # 漏洩すると致命的な .git ディレクトリへのスキャン対策
        Alias /.git         /var/www/nepenthes/dummy_git.html

        # robots.txtで「巡回禁止」にしているにもかかわらず、
        # 行儀悪くスクラップ(収集)しにくるAI自動巡回エージェント用のトラップ
        Alias /assets-archive /var/www/nepenthes/dummy_login.html

        # トラップ用ディレクトリへのアクセスを許可
        <Directory /var/www/nepenthes>
            Require all granted
        </Directory>
    </IfModule>

    # --------------------------------------------------------
    # 5. SSL/TLS暗号化プロトコル設定
    # --------------------------------------------------------
    SSLEngine on
    Protocols h2 http/1.1

    # 安全性の低い古いプロトコルを徹底排除(TLS 1.2 / 1.3 のみ許可)
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384
    SSLHonorCipherOrder off
    SSLSessionTickets off

    SSLCertificateFile    /etc/ssl/certs/example.com.crt
    SSLCertificateKeyFile /etc/ssl/private/example.com.key

    # --------------------------------------------------------
    # 6. WAF(ModSecurity)のチューニング
    # --------------------------------------------------------
    <IfModule security2_module>
        SecRuleEngine On

        # ファイルアップロードの容量制限を引き上げ(必要に応じて調整)
        SecRequestBodyInMemoryLimit 524288000
        SecRequestBodyLimit         524288000
        SecRequestBodyNoFilesLimit  524288000

        # --- 偽陽性(誤検知)の除外ルール ---
        # ※システム(RedmineやWordPressなど)の特性に応じて、正常な通信が遮断されないよう調整
        SecRuleRemoveById 920420 # 特定JavaScriptの誤検知対策
        SecRuleRemoveById 200004 # マルチパート解析エラーの除外
        SecRuleRemoveById 920300 # HTTPプロトコル違反の除外
        SecRuleRemoveById 920260
        SecRuleRemoveById 920270
        SecRuleRemoveById 920240

        # --- 記憶抹消刑(Damnatio Memoriae)システム ---
        # あらかじめリスト化した凶悪なIP(negativelist.txt)からのアクセスは、
        # 404エラーを返しつつ、Apacheのログ(auditlog等含む)に一切記録させず存在を抹消する
        SecRule REMOTE_ADDR "@pmFromFile negativelist.txt" "phase:1,id:2,status:404,msg:'Damnatio Memoriae',nolog,noauditlog"
    </IfModule>
</VirtualHost>

では、各設定の意図は後回しにして、「どういう動きなのか」を見てみましょう。

パターン1: http→httpsへの変更

http(80番)で来たアクセスを強制的にhttps(443)に転送します。

sequenceDiagram autonumber participant Client as ユーザー / 攻撃者 participant Apache80 as Apache (ポート80) Client->>Apache80: HTTPでアクセス (http://example.com) Note over Apache80: RewriteEngine On<br/>RewriteCond %{HTTPS} off Apache80-->>Client: 301 Moved Permanently (https://...)

パターン2: 通常アクセス

悪意を持たないアクセスには正常に通信します。

sequenceDiagram autonumber participant User as 通常ユーザー (または Googlebot) participant Apache443 as Apache (ポート443) participant WebPage as メインコンテンツ (/public) User->>Apache443: HTTPSでアクセス (正常なリクエスト) alt Googlebot や 監視IP の場合 Note over Apache443: SetEnvIf で「dontlog」フラグを付与 Note over Apache443: アクセスログ(example_access.log)へは記録しない else 一般の通常ユーザーの場合 Note over Apache443: 通常通りアクセスログに記録 end Apache443->>WebPage: コンテンツ読み込み WebPage-->>Apache443: ページデータ返却 Note over Apache443: セキュリティヘッダーを付与<br/>(HSTS / X-Content-Type-Options / X-Frame-Options / X-XSS) Apache443-->>User: 200 OK (安全な通信でWebページを表示)

パターン3:攻撃者、ボットなど

スクレイピングや事前に設定していた悪質なボットにどう立ち向かうのかがこちらです。

sequenceDiagram autonumber participant Attacker as 攻撃者 / 悪質ボット participant Apache443 as Apache (ポート443) participant Trap as トラップエリア (/nepenthes) alt パターンA:特定の動的ページへの大量タグ攻撃 Attacker->>Apache443: /search/tags に大量のカンマを含むクエリでアクセス Note over Apache443: bad_tag_stacking 検知 Note over Apache443: E=dontlog:1 (ログを残さない) Apache443-->>Attacker: 404 Not Found (虚無へ葬る) else パターンB:ブラックリスト(blacklist-bots.txt / spam-ips.txt)に該当 Attacker->>Apache443: スパムIPや悪質Botからのアクセス Note over Apache443: Require not env bad_bot で拒否 Note over Apache443: ErrorDocument 403 /404.html Apache443-->>Attacker: 404 Not Found (403を隠蔽して最初から何も無いと騙す) else パターンC:ウツボカズラ(Nepenthes)トラップへの盲目スキャン Attacker->>Apache443: /wp-login.php や /.git へのアクセス Note over Apache443: mod_alias でダミーファイルへ転送 Apache443->>Trap: dummy_login.html などを読み込み Trap-->>Apache443: ダミーデータ Apache443-->>Attacker: 200 OK (ダミーファイルを返して時間を稼ぐ) else パターンD:凶悪なIPリスト(negativelist.txt)に該当 [WAF制御] Attacker->>Apache443: 凶悪リストに載っているIPからのアクセス Note over Apache443: ModSecurity: 'Damnatio Memoriae' 発動 Note over Apache443: nolog, noauditlog (全てのログ・監査ログから存在を抹消) Apache443-->>Attacker: 404 Not Found (記憶抹消刑) end

と、このように、

「“アクセス”は通す
 “攻撃”は阻止する
 ミドルウェアで“両方”やるというのは
 そう難しいことじゃあないな」

と言えるのが「個人VPSでApacheを使う理由」と言えます。

次は、それらを可能にする仕組みについて解説します。

NginxとApacheの違い(と筆者がApacheを使う理由)

Webサーバの中核ミドルウェアは

  • Apache
  • Nginx

の二つですが、近年のトレンドは間違いなくNginx。

Nginxはなぜここまでもてはやされるのかを軽くひもといて見ます。

Nginxがもてはやされる理由

Nginx(エンジンエックス)が現在、Webサーバーのシェアでトップ争いをするほどもてはやされ、多くの開発者や企業に支持されているのには明確な理由があります。

主な意見やメリットをWeb上の情報からまとめると、理由は大きく分けて以下の4点に集約されます。

1. 「C10K問題」を解決する圧倒的な同時接続処理能力

かつて主流だったWebサーバー(Apacheなど)は、アクセス(リクエスト)ごとに「プロセス」や「スレッド」を立ち上げて処理する方式でした。そのため、同時に大量のアクセスが来るとサーバーのメモリを使い果たしてしまい、サーバーがダウンしたり極端に遅くなったりする「C10K問題(クライアント1万台問題)」が発生していました。

これに対し、Nginxは「イベント駆動型(非同期シングルスレッド)」というアーキテクチャを採用しています。

  • 1つのプロセスで、大量のリクエストをイベントとして次々と効率よく処理します。
  • アクセスが急増してもメモリの消費量がほとんど増えないため、「大量の同時接続があっても、少ないメモリで高速に処理を維持できる」という最大の強みを持っています。

2. 静的コンテンツの配信が劇的に速い

HTMLファイル、画像、CSS、JavaScriptなどの「静的コンテンツ」をユーザーに返すスピードが圧倒的です。
ベンチマークによっては、従来のWebサーバーと比べて数倍〜十数倍以上のパフォーマンスを発揮すると言われており、リソースの消費を抑えながらWebサイトの表示速度を大幅に向上させることができます。

3. リバースプロキシ・ロードバランサーとしての優秀さ

Nginxは単なるWebサーバーとしてだけでなく、

  • リバースプロキシ(中継サーバー)」
  • ロードバランサー(負荷分散装置)

としての機能が非常に優れています。

現代のWebシステムでは、以下のような「役割分担」をする構成(構成例を以下に示します)がトレンドとなっています。

sequenceDiagram autonumber participant User as ユーザー<br/> (大量のアクセス) participant Nginx as Nginx<br/>(リバースプロキシ) participant App as アプリサーバー<br/>(Node.js / PHP等) User->>Nginx: リクエスト送信 alt 静的ファイルの場合 Nginx-->>User: 静的ファイルを自分で高速に返す else 動的処理の場合 Nginx->>App: 処理を丸投げ (プロキシ) App-->>Nginx: 処理結果を返却 Nginx-->>User: レスポンスを返却 end

動的な処理(WordPressや各種Webアプリなどの重い処理)は後ろのアプリケーションサーバーに任せ、Nginxは「表に立って大量のアクセスを交通整理する」という役割を担うことで、システム全体の安定性と速度を極限まで高めることができます。

4. 設定ファイルがシンプルで扱いやすい

長年の歴史を持つApacheなどは、多機能である反面、設定ファイルが複雑化しがちでした。
一方、Nginxの設定は記述ルールがスマートで、直感的で分かりやすい構成になっています。軽量でコンテナ(Dockerなど)との相性も抜群に良いため、現代のクラウドネイティブな開発環境において非常に好まれています。

では、なぜ筆者はApacheを使っているのか?

と、ここまで書いたらメリットしかないように感じられるNginxですが、

  • 小規模サイト
  • がっつりカスタマイズしたい

方にはApacheの方が優れているケースが多々あります。

「単一VPS(リソースが限られている)での個人利用」という条件に絞ると、実はApacheを選ぶ明確なメリット(むしろApacheの方が楽で優れている点)があります。

なぜ筆者のように単一VPSの個人利用ではApacheが有利になるのか、主な理由を3つに分けて解説します。

1. .htaccess (include)による手軽さと柔軟性

個人利用、特に1台のVPSでブログやポートフォリオなど複数のサイトを試行錯誤しながら運用する場合、Apacheの .htaccess(分散設定ファイル)や include(内部ファイル) は圧倒的に便利です。

  • ディレクトリ単位で設定できる:
    • リダイレクト(転送)の設定、Basic認証(パスワード制限)、アクセス拒否などを、特定のフォルダ内に .htaccess や include(内部ファイル)ファイルを置くだけで即座に反映できます。
    • Apacheの .htaccess(分散設定ファイル)や、設定を細かく分割して管理できる Include 機能は圧倒的に便利です。
  • Nginxの場合:
    • 全て中央の設定ファイル(nginx.conf)に記述する必要があり、変更のたびにサーバーの再起動(リロード)が必要です。記述を間違えるとサーバー全体が落ちるリスクがあります。

2. WordPressなどの動的サイト(PHP)との相性が抜群

個人VPSでよく使われるWordPress(PHP)を動かす場合、Apacheの方が構築が圧倒的にシンプルです。

  • mod_php による一体型処理:
    • Apacheはサーバー内部にPHPを組み込んで(モジュールとして)動かすことができるため、Apacheをインストールするだけで、PHPがそのままサクサク動きます。
    • ちなみに筆者は、さらなるパフォーマンス最適化のためにApache+PHP-FPMという『いいとこ取り』の構成で運用していますが、こうした高度な組み合わせへの柔軟性が高いのもApacheの魅力です。
  • Nginxの場合:
    • Nginx単体ではPHPを動かせないため、必ず「PHP-FPM」という別の仕組みと連携させる必要があります。この連携設定(Unixソケットやポートの設定)が初心者にはやや難解で、トラブルシューティングのハードルが上がります。

3. Web上の「知見(情報量)」が圧倒的に多い

歴史が長いApacheは、ネット上にあるトラブルシューティングや逆引きの設定情報が膨大です。

  • コピペで動く情報が多い:
    • 特にWordPressのプラグイン(セキュリティ系やキャッシュ系、SEO系)の多くは、「インストール時に .htaccess を自動書き換えする」 仕様になっています。Apacheであればプラグインを入れるだけで勝手に設定が完了します。
  • Nginxの場合:
    • プラグインが自動生成した .htaccess の記述(Apache用)を、自分でNginx用の設定構文に翻訳して nginx.conf に手動で追記しなければならないケースが多々あります。

単一VPSにおける「性能差」の現実

「でも、Nginxの方が軽いし速いのでは?」と思われるかもしれませんが、「個人利用のアクセス規模」であれば、体感できるほどの差はまず出ません。

  • 同時アクセスが数十〜数百程度なら:
    • Apacheでもメモリは十分に足りますし、ページ表示速度もほぼ変わりません。
  • むしろメモリ消費:
    • Nginx+PHP-FPMの構成は、プロセス管理を適切に設定しないと、限られたVPSのメモリをじわじわと圧迫することがあります。設定の手間を考えると、Apacheの方が「ほったらかし」で安定して動きやすいです。

筆者がApacheを選ぶ最大の理由

  • mod_rewrite
  • mod_security

が余りにも強いから、に尽きます。

1. mod_rewrite という「最強のアーミーナイフ」

mod_rewrite は、URLの書き換えやリダイレクトを自由自在に行える、Apache史上最も強力なモジュールのひとつです。個人VPSでWebサイトを運営する際、これほど頼りになるツールはありません。

  • 正規表現による超柔軟なコントロール:
    • 「特定のIPアドレス以外からのアクセスを全てメンテナンス画面に飛ばす」
    • 「スマホからのアクセスだけ専用ページにリダイレクトする」
    • 「拡張子なしのURLを裏側でPHPにマッピングする(美しいURLの実現)」
      といった複雑な処理が、数行の記述で完結します。
  • .htaccess(inlude) との相乗効果:
    • これを各ディレクトリの .htaccess に書けるため、Webサーバー全体の設定を汚すことなく、Webアプリケーション側(WordPressなど)が自身をコントロールするためにフル活用できます。
  • Nginxの場合:
    • Nginxにも rewrite 指令はありますが、Apacheの mod_rewrite ほど複雑な条件分岐(複数条件の組み合わせや、ファイル・ディレクトリの存在チェックを挟んだ高度なルーティング)をスマートに書くのが難しく、設定が肥大化・複雑化しやすいという弱点があります。

2. mod_security(WAF)との「歴史的・機能的な相性の良さ」

個人VPSは常に世界中からのサイバー攻撃(ブルートフォースアタック、SQLインジェクション、クロスサイトスクリプティングなど)に晒されるため、オープンソースのWAF(Webアプリケーションファイアウォール)である mod_security の存在は非常に大きいです。

  • Apacheが「本家」という安心感:
    • もともと mod_security はApacheのモジュールとして開発された歴史があるため、Apacheとの親和性は抜群です。インストールも容易で、挙動も非常に安定しています。
  • OWASP CRS(コアルールセット)の運用が楽:
    • セキュリティの標準ルールであるOWASP CRSなどを導入する際、Apacheであればドキュメントや実績が豊富にあるため、個人でも比較的迷わずに導入・チューニング(誤検知の修正など)が可能です。
  • Nginxの場合:
    • Nginxで mod_security(最新はv3)を動かすには、多くの場合ソースコードからモジュールを自分でコンパイル(ビルド)して組み込む必要があり、バージョンアップのたびにメンテナンスの手間が発生するなど、個人運用のハードルがかなり高くなります。

結論:個人VPSにおけるApacheは「要塞であり、何でも屋」

Nginxが「大量の荷物を最速で右から左へ捌くプロフェッショナル」だとすれば、
Apacheは「どんな要求にもその場で柔軟に応え、自分で自分の身を守る武装も完璧な万能戦士」です。

個人VPSという「限られた1台の環境」だからこそ、

  • mod_security でガッチリ身を守り、
  • mod_rewrite で複雑なURL設計をスマートにこなし、
  • .htaccess で手軽に設定を変える

というApacheの特性が120%活きます。「もてはやされているから」という理由だけでNginxを選ぶと、このあたりの「痒いところに手が届く便利さ」を全て手動の泥臭い設定(あるいはコンパイル作業)で補うことになり、途中で挫折してしまうケースも少なくありません。

では次回の記事から、筆者が実際にVPSで運用している「mod_rewrite による超柔軟なURL制御」と「mod_security による鉄壁の防御設定」の具体例を、コードを交えてご紹介します

ONE OUTS システム番外:AI時代の下品なスクレイパー「Paprika」から、3ステップでWebサイトを護るApache防衛のケーススタディ。

概要:新型AI自動化プラットフォーム「Paprika」とは?

現Xで見かけてしまったツール、Paprika。

Paprikaは、分散ワーカー上のChrome(実ブラウザ)をPlaywright経由で操作し、LLM/Vision(AI)を使ってページ内の画像・動画・構造化データを根こそぎ剥ぎ取る、極めて執拗な自動化プラットフォームです。

なぜ「趣味が悪い」のか:

  • 境界線の蹂躙:
    • ログイン必須サイトや年齢確認、JavaScript描画など、管理者が明示的に引いた「機械的な巡回を拒む壁」を、クッキー偽装やセッション維持で強引に突破することを目指している点。
  • 品性のない執着:
    • CSSの変更によるクローラー避けが効かない。AIが「人間の目で画面を見て」ボタンを探し、クリックしてくるため、これまでの構造的な防護策を無効化しようとする点。
  • 他者リソースへの強欲な寄生:
    • scroll=True で遅延ロードを発火させ、画像や動画ストリームを「丸ごと一括ダウンロード」するため、サーバーの帯域やCPU(コスト)に莫大な負荷をかける点。
  • リーガルリスク: 「正当な目的のための自動化」を気取っている連中であるため、下手に通信を拒否(403等)したり速度制限をかけたりすると、「正当なアクセスを妨害された」などと言いがかり(難癖)をつけてくる厚顔無恥なリスク.

悪用できる建前

仕様書には「利用規約の遵守」や「正当な目的での利用」と美しく免責事項が書かれていますが、提供されている機能はあきらかに「大量かつ高速なコンテンツのブッコ抜き」を目的としています。この「建前と本音の圧倒的なギャップ」が「趣味が悪い」と断じた理由です。

一般的なクローラー(Googlebotなど)は、robots.txt のルールを守り、正体を名乗って巡回します。
しかしPaprikaは、JavaScriptの完全実行、遅延ロード(スクロール動作)への追従、Cookieの永続化による会員限定ページの突破を平然と行います。

サイト側が「毎日ボタンの配置(CSS)を変える」といったボット対策をしても、AIエージェントがそれを学習して乗り越えてきます。さらに、分散IPで「一見、たくさんの一般ユーザーが同時にアクセスしてきた」ように見せかけるため、従来のWAFやIP制限が非常に効きにくいのが最大の問題です。

AIエージェントが画面スクロールや walk(サイト内巡回)を繰り返すことで、一般ユーザーの快適な閲覧環境を圧迫する、一種のDoS状態のツールです。

3. ここからサイトを護るための「Apacheの防衛」

以下、筆者環境です。

  • Apache
  • mod_rewrite
  • Ubuntu 24.04

の2つがあれば基本的には対処可能です。ApacheのMod_rewriteは、アーミーナイフのような問題です。

sudo a2enmod rewrite
sudo systemctl reload apache2.service

ステップ1:User-Agentによる水際対策(デフォルトを即座に切って捨てる)

彼らがもしデフォルトの名称(PaprikaやPlaywright)をUser-Agentに残して突っ込んできた場合、もっとも軽量な処理で済みます。

apacheの.confファイルに以下を突っ込んでおきます。

# ─── 層1: 既知のAI自動化ツール・ライブラリのUAを拒否 ───
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTP_USER_AGENT} (paprika|Playwright|Stagehand|Browser-Use|Browserable) [NC]
    RewriteRule ^ - [F,L]
</IfModule>

ステップ2:利用規約(ポリシー)の明文化(法的な盾)

「言いがかり」を完全に無力化するため、サイトのフッター等に「AIエージェントや実ブラウザ偽装による一括ダウンロードをDoS行為とみなし、検知した場合は即座にアクセス制限を適用する」旨を記載しておきましょう。

そもそも、上記のDoSツールを使ってくるものが「正常な閲覧者」である理由はありません。

ステップ3:robots.txtを逆手に取った「404ハニートラップ」

  • robots.txt に罠を仕掛ける:

まともなクローラーは rogotx.txt に書かれたDisallow を守ります。しかし、こんな輩がこれを律儀に守るということはまずありません。

そこで、それを逆手に取り、品性のないAIエージェントだけを炙り出すための罠のパスを設定します。

robots.txtに以下のような罠を設けておきます。

User-agent: *
Disallow: /assets-archive/

悪意あるクローラーは「robots.txtにわざわざ書くということは、ここに大事な情報があるに違いない」と判断します。

  • Apache(.htaccess または .conf)に罠の行き先を刻む:

踏んだクローラーに対し、403(拒否)ではなく「404(存在しない)」を返すことで、「そんなものはありません」と先んじておきます。

# robots.txtを無視して歩き回る(walk)AIエージェントへの罠 
<IfModule mod_rewrite.c>
    RewriteEngine On
    # robots.txtで禁止した領域に足を踏み入れた者は、一律で404(Not Found)
    RewriteRule ^assets-archive/?$ - [R=404,L]
</IfModule>

4. まとめ

「正当な目的」を謳えば他人のリソースを奪えばいいというツール。

崇高な目的とやらにDoSツールをばらまくというその厚顔無恥ぶりは『メリー・ポピンズ』の

Not at all attractive to my way of thinking

この言葉を借りるまでもなく悪趣味の一言。

Web管理者側もただ怯えるのではなく、相手の仕様の「執着」を逆手に取り、エレガントに虚無(404)へ引きずり込む防衛術がお役に立てれば幸いです。

Page 1 of 7

次

Powered by WordPress & Theme by Anders Norén