タグ: apache Page 1 of 6

教材価値の高いログ:なぜ攻撃者は最初に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認証(パスワード制限)、アクセス拒否などを、特定のフォルダ内に .htaccessinclude(内部ファイル)ファイルを置くだけで即座に反映できます。
    • 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)へ引きずり込む防衛術がお役に立てれば幸いです。

Apacheのインストールと初期設定(Ubuntu 26.04)

概要

Ubuntu26.04にWebサーバーApacheをインストールします。最近のトレンドではNginxではあるものの、

  1. 豊富なモジュールとカスタマイズ
  2. 動的コンテンツの設定をしやすい
  3. 小規模サイトを立ち上げる上での手間の少なさ
  4. 外部ファイルやモジュールの連携により、以下のような細かい設定が可能
  • 自宅等からのアクセスログを残さず、ログの透明化を図る
  • Robots.txtを無視する悪質なクローラーの排除
  • mod_securityに代表されるWAF(Web Application Firewall)の設置

を考慮してのApache設定です。

さっくりとした手順

  1. (未実施の場合必須)UFWの設定を行います。
  2. Apacheのインストールを行います。
  3. Apacheの設定を行います。
  4. 設定の反映を確認します。

(未実施の場合必須)UFWの設定

この作業、サーバ移設などになれている人ほど陥る罠です。「設定はしっかりしている。なのにサンプルページすら引っかからない!」という場合、大概が「UFWでポート80/443を空けていない」パターンが大半を占めます。

大前提

SSH接続を許可(ポート22はSSH記事で許可済みを前提とする)。

設定の前の心構え:

UFWは堅牢であると同時に融通の利かない門番です。設定を間違えると「自分のサーバにログインできない」事態が易々と発生します。

そのため、この作業に臨む際は落ち着いて臨みましょう。コマンドを打つ際に3回ぐらい深呼吸してもいいぐらいの心構えです。

  • http通信を許可する
sudo ufw allow http comment 'Allow HTTP traffic for Apache'
  • https通信を許可する
sudo ufw allow https comment 'Allow HTTPS traffic for Apache'
  • 設定を確認する
 sudo ufw status verbose

上記、http/httpsが有効になっていることを確認します。

  • UFWが有効になっていない場合:有効化
sudo ufw enable 

インストールを行います。

  • パッケージ全体のアップデート
sudo aptitude update 
  • apacheのインストール
sudo aptitude install apache2
  • バージョン確認
apache2ctl -v
  • 表示例
Server version: Apache/2.4.62 (Ubuntu)
Server built:   2024-07-22T12:37:10
  • サービス稼働確認
systemctl status apache2.service

enabledactive (running)を確認します。

設定を行います。

  • 設定ファイルのバックアップ
sudo cp -pi /etc/apache2/apache2.conf /path/to/backup/directory/apache2.conf.$(date +%Y%m%d)

任意のバックアップディレクトリを指定します。

  • 設定ファイルのバックアップ確認
diff -u /path/to/backup/directory/apache2.conf.$(date +%Y%m%d) /etc/apache2/apache2.conf

差分が無いことでバックアップを確認します。

  • 設定ファイル追記
sudo tee -a /etc/apache2/apache2.conf > /dev/null << 'EOF'
ServerSignature Off
ServerTokens Prod
ServerName example.com
EOF

自分のサーバー名を英数字で置き換えてください。

  1. サーバーの署名をオフにして
  2. 最小限の情報のみを公開し
  3. Webサーバの名前を指定する

内容です。

  • 追記確認
diff -u /path/to/backup/directory/apache2.conf.$(date +%Y%m%d) /etc/apache2/apache2.conf
  • 差分内容
+ ServerSignature Off
+ ServerTokens Prod
+ ServerName 自分のサーバー名

設定反映を確認します。

  • 構文確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • サービス再起動
sudo systemctl restart apache2.service
  • サービス再起動確認
systemctl status apache2.service

active (running)を確認します。

  • 設定反映確認
curl -I http://localhost

以下のように、ServerヘッダーにApacheのみが表示されていることを確認します。

Server: Apache

Apacheのインストールと初期設定(RHEL系)

概要

RHEL系(AlmaLinux / Rocky Linux 9等)にWebサーバーApacheをインストールします。最近のトレンドはNginxではあるものの、以下のメリットを考慮してApacheを選択します。

  1. 豊富なモジュールとカスタマイズ: 歴史が長く、情報の蓄積が膨大。
  2. 動的コンテンツの設定のしやすさ: PHP等との親和性が高い。
  3. 運用の手軽さ: 小規模サイトを迅速に立ち上げるのに適している。
  4. 高度なセキュリティ・ログ設定:
    • 自宅等からのアクセスログを除外するなどのログカスタマイズ。
    • 悪質なクローラーの排除。
    • mod_security(WAF)による防御。

さっくりとした手順

  1. firewalldの設定: 外部からのアクセス許可を与えます。
  2. Apacheのインストール: dnfを使用してインストールします。
  3. Apacheの設定: セキュリティとサーバー名の設定を行います。
  4. 設定の反映確認: 正常に動作しているかチェックします。

1. firewalldの設定

サーバー移設などでハマりやすいのが「設定は正しいのにページが表示されない」現象です。RHEL系ではデフォルトで強力なファイアウォール(firewalld)が動作しており、ポート80/443を明示的に開放する必要があります。

大前提

SSH接続(ポート22)は許可されている前提で進めます。設定を誤るとリモート操作ができなくなるため、慎重に行いましょう。

  • HTTP通信を許可する
sudo firewall-cmd --permanent --add-service=http
  • HTTPS通信を許可する
sudo firewall-cmd --permanent --add-service=https
  • 設定を反映させる
sudo firewall-cmd --reload
  • 設定を確認する
sudo firewall-cmd --list-all

services の欄に httphttps が含まれていればOKです。

2. インストールを行います

RHEL系ではApacheのパッケージ名は httpd です。

  • パッケージ全体のアップデート
sudo dnf update -y
  • Apache (httpd) のインストール
sudo dnf install httpd -y
  • バージョン確認
httpd -v

-(表示例)-
Server version: Apache/2.4.57 (AlmaLinux)

  • サービスの起動と自動起動設定
sudo systemctl enable --now httpd
  • サービス稼働確認
systemctl status httpd

enabledactive (running) を確認します。

3. 設定を行います

  • 設定ファイルのバックアップ

RHEL系の設定ファイルは /etc/httpd/conf/httpd.conf です。

sudo cp -pi /etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf.$(date +%Y%m%d)

※任意のバックアップディレクトリを指定してください。

  • 設定ファイルのバックアップ確認
diff -u /etc/httpd/conf/httpd.conf.$(date +%Y%m%d) /etc/httpd/conf/httpd.conf

エラーがないことを確認します。

  • 設定ファイルの書き換え(追記)

セキュリティ向上のため、署名の非表示化とサーバー名を追記します。

sudo bash -c "cat >> /etc/httpd/conf/httpd.conf" << 'EOF'

# Custom Settings
ServerSignature Off
ServerTokens Prod
ServerName example.com:80
EOF

example.com の部分は、ご自身のドメイン名またはホスト名に置き換えてください。

  • 差分の確認
diff -u /etc/httpd/conf/httpd.conf.$(date +%Y%m%d) /etc/httpd/conf/httpd.conf

末尾に指定した3行が追加されていることを確認します。

4. 設定反映を確認します

  • 構文確認
sudo httpd -t

Syntax OK と表示されることを確認します。

  • サービス再起動
sudo systemctl restart httpd
  • 設定の反映確認(ヘッダー確認)
curl -I http://localhost

以下のように、Server ヘッダーが Apache のみ(バージョン情報なし)になっていれば成功です。

HTTP/1.1 200 OK
Date: ...
Server: Apache
...

Grwoi/Apacheリバースプロキシ、セキュリティ判断FからB+まで改善した記録

昨日の続き。

HTTP ObservatoryでRedmineサイトをB-→B+に改善しましたが、

筆者環境の

  • Apache 2.4によるリバースプロキシ
  • Growi v7.4.7

環境のHTTP診断もそのぐらいであろうと思ったらまさかのF判定を食らいました。

Fランクの内訳

最初の診断結果では、以下の項目で派手に減点を食らっていました。

  • CSP (Content Security Policy):
    • 未設定 (-25)
  • Cookies:
    • Secure属性なし (-20)
  • HSTS: 認識されず
    • (-20)
  • X-Frame-Options / X-Content-Type-Options:
    • 認識されず (-25)

特に謎だったのが、「Apache側でHeader設定を入れているはずなのに、認識されない(Failed)」という点でした。

アプリとApacheの二重出力。

原因を調査するため、curl -I コマンドで生のレスポンスヘッダーを確認しました。

curl -I https://growi-site
Strict-Transport-Security: max-age=63072000
Strict-Transport-Security: max-age=15552000; includeSubDomains  # <-- 2重に出ている
X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff # <-- 2重に出ている
Set-Cookie: connect.sid=...; Path=/; HttpOnly  # <-- Secure属性がない

原因の分析:

リバースプロキシ構成では、「バックエンド(アプリ側)が吐き出すヘッダー」と「Apacheが追加しようとするヘッダー」が両方ブラウザに届いてしまい、重複が発生。

診断ツール側が「どの設定を信頼すべきか判断できない」としてエラーになっていたのです。

これを回避するための手段が以下の通りです。

さっくりとした手順

  1. Apacheの設定ファイルを書き換えます。
  2. 設定を反映します。
  3. 設定反映を確認します。

Apacheの設定ファイルを書き換え

  • 設定ファイルのバックアップ
sudo cp -pi /etc/apache2/sites-available/growi-site.conf /path/to/backup/growi-site.conf.$(date +%Y%m%d)

.confの名前やバックアップディレクトリは自分の環境に合わせます。

  • 設定ファイルのバックアップ確認
diff -u /path/to/backup/growi-site.conf.$(date +%Y%m%d) /etc/apache2/sites-available/growi-site.conf

エラーがないことを確認します。

/etc/apache2/sites-available/growi-site.confを以下のように修正していきます。(要管理者権限)

  • 修正前
    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"
  • 修正後
        # 重複を防ぐため、通常のレスポンスとProxyレスポンスの両方から一旦削除
        Header unset Strict-Transport-Security
        Header always unset Strict-Transport-Security
        Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

        Header unset X-Content-Type-Options
        Header always unset X-Content-Type-Options
        Header always set X-Content-Type-Options "nosniff"

        Header unset X-Frame-Options
        Header always unset X-Frame-Options
        Header always set X-Frame-Options "SAMEORIGIN"

        # 足りなかった重要ヘッダーを新規追加
        Header always set Referrer-Policy "strict-origin-when-cross-origin"

        # CSP: アプリの動作を維持しつつ、安全性を高める(object-src 'none' を追加)
        Header always unset Content-Security-Policy
        Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:; object-src 'none';"

        # CookieのSecure属性をApache側で強制付与
        Header edit Set-Cookie ^(.-)$ "$1; Secure; SameSite=Lax"

この「2重出力」を解消し、かつアプリ側で足りない属性を付与するために、Apacheの設定を「既存ヘッダーの完全掃除 + 強制セット」という戦略に変更しました。

  • 修正後の差分確認
diff -u /path/to/backup/growi-site.conf.$(date +%Y%m%d) /etc/apache2/sites-available/growi-site.conf

以下のような差分を確認します。

-    Header always set X-Content-Type-Options "nosniff"
-    Header always set X-Frame-Options "SAMEORIGIN"
-    Header always set X-XSS-Protection "1; mode=block"
+        # 重複を防ぐため、通常のレスポンスとProxyレスポンスの両方から一旦削除
+        Header unset Strict-Transport-Security
+        Header always unset Strict-Transport-Security
+        Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
+
+        Header unset X-Content-Type-Options
+        Header always unset X-Content-Type-Options
+        Header always set X-Content-Type-Options "nosniff"
+
+        Header unset X-Frame-Options
+        Header always unset X-Frame-Options
+        Header always set X-Frame-Options "SAMEORIGIN"
+
+        # 足りなかった重要ヘッダーを新規追加
+        Header always set Referrer-Policy "strict-origin-when-cross-origin"
+        
+        # CSP: アプリの動作を維持しつつ、安全性を高める(object-src 'none' を追加)
+        Header always unset Content-Security-Policy
+        Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:; object-src 'none';"
+
+        # CookieのSecure属性をApache側で強制付与
+        Header edit Set-Cookie ^(.-)$ "$1; Secure; SameSite=Lax"

設定反映を確認します。

  • 構文確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • サービス再起動
sudo systemctl restart apache2.service
  • サービス再起動確認
systemctl status apache2.service

active (running)を確認します。

設定反映の確認

curl -I https://growi-site
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:; object-src 'none';

など、ヘッダーのダブりがないことを確認します。

HTTP Observe に再度アクセスし、セキュリティ診断を行いました。

再スキャンの結果、主要な項目はすべて Passed となり、無事に B+ を獲得。

  • HSTS / XFO / NoSniff: 重複が解消され、正しく認識された。
  • Cookies: Secure/SameSite属性が付き、満点。
  • Referrer Policy: 合格。

なぜ「A+」ではないのか?

CSPにおける 'unsafe-inline''unsafe-eval' が「安全ではない」として減点対象になっています。しかし、これらを外すとモダンなWebアプリ(Wikiのエディタなど)は動かなくなることが多いため、利便性とのトレードオフとして 「B+」は現実的な落とし所と言えます。

まとめ

  1. 診断ツールで「Recognizedできない」と言われたら、まずは curl -I で重複を疑う。
  2. Apache側で Header always unset してから set することで、バックエンドとの競合を確実に排除できる。
  3. CookieのSecure化もApacheの Header edit で一発解決。

Page 1 of 6

Powered by WordPress & Theme by Anders Norén