タグ: apache Page 1 of 6

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 で一発解決。

Nextcloudで大容量ファイルを安定して扱うためのApacheチューニング:RequestReadTimeoutの調整

スマートフォンからの画像を自分のPCに転用する形で用いているNextcloud。

アップロードが途中で失敗したり、特定の条件下で接続が切れたりすることがあります。Apacheビルトインのmod_reqtimeout設定を最適化し、サーバーの安定性と利便性を両立させるためのチューニングを行いました。

環境

  • Nextcloud 32
  • Apache 2.4
  • PHP-FPM8.3
  • Ubuntu 24.04

そもそもmod_reqtimeoutとは?

Slowloris(スローロリス)攻撃への対策

このモジュールがビルトインされたのは、Slowlorisという、非常に低コストで強力な攻撃手法への対抗があります。

通常のDoS攻撃は大量の通信を送りますが、Slowlorisは逆に「極めてゆっくり」通信します。

  1. サーバーに接続を開始する。
  2. リクエスト(ヘッダーやボディ)を、タイムアウトにならないギリギリの遅さで、1バイトずつ小出しに送る。
  3. サーバー側は「まだデータが来るはずだ」と判断し、その接続(スレッドやプロセス)を維持し続ける。

結果 サーバーの同時接続数の上限が攻撃者の「待ち」状態で埋まってしまい、正規のユーザーがアクセスできなくなります。

mod_reqtimeout は、この「嫌がらせ」を許さないための防衛線です。

サーバーリソースの適正管理

Apacheサーバーが1つの接続を維持するには、メモリやCPUリソースを消費します。
もしタイムアウト設定がない、あるいは極端に長い場合、以下のような問題が発生します。

  • ゾンビ接続の蓄積: ネットワークが切れたのにクローズ処理が終わっていない接続が残り続ける。
  • リソースの浪費: 応答の遅いクライアント(意図的か否かに関わらず)のために、サーバーの貴重な作業枠をいつまでも空けておくことになります。

なぜこれがNextcloudで徒になるのか

Nextcloudのデータの性質と通信の仕組みにあります。

巨大なファイルを細切れにする

Nextcloudは、数MBから数GBあるような大きなファイルを送る際、一気に送らずにファイルを分割して、「チャンク(塊)」に分割して送信します。

  • 動作の流れ:
    • チャンクAを送信 → サーバーが処理 → チャンクBを送信……
  • 問題点:
    • チャンクとチャンクの間に、クライアント側でのファイル読み込みやハッシュ計算などで一瞬「無通信」の時間が流れます。
  • 結果:
    • Apacheのmod_reqtimeoutは、この無通信を先のSlowlorisと誤認して、接続をバッサリ切ってしまいます。

モバイル回線の不安定さ

Nextcloudは外出先のスマートフォンから使うことを想定しています。

  • 電波の瞬断:
    • 移動中にWi-Fiから4G/5G回線に切り替わったりする。
  • 上り速度の制限:
    • モバイル回線は「下り」は速くても「上り」が極端に遅いことが多い。

これも、先のSlowloris攻撃と見做されてしまいます。

チューニングの必要性

だからといって、このmod_reqtimeoutを無効にすると、それらの攻撃への備えがなくなります。

Nextcloudの利便性を高めつつ不審な攻撃を守るというのは

「任務」は遂行する「部下」も守る
お前ごときに「両方」やるというのはそう難しいことじゃあないな

ぐらいの精神でやっていきましょう。

さっくりとした手順

  • mod_reatimeoutの設定ファイルのバックアップを取る。
  • mod_reatimeoutの設定ファイルを修正する。
  • サービス再起動を行う。

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

  • 念のためのモジュール確認
sudo apache2ctl -M |grep req

reqtimeout_module (shared)を確認します。(apacheをapt等で入れていれば、まず入っています)

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

/path/to/backup/directoryは自分の環境に合わせます。

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

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

mod_reatimeoutの設定ファイルを修正

/etc/apache2/mods-available/reqtimeout.confを管理者権限で修正します。

筆者は以下のように行いました。

-RequestReadTimeout body=10,minrate=500
+RequestReadTimeout body=20,minrate=500

具体的には、リクエストボディ(データの送信本体)の読み取り開始を待機する時間を10秒から20秒へ延長しています。

  • body=20:
  • リクエストボディの最初の1バイトを待つ時間を20秒に設定。
  • minrate=500:
  • データが送り始められた後、最低でも秒間500バイトの転送速度を要求する。これより遅い状態が続くとタイムアウトします。
  • ファイルの差分確認
diff -u /path/to/backup/directory/reqtimeout.conf.$(date +%Y%m%d) /etc/apache2/mods-available/reqtimeout.conf 

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

-RequestReadTimeout body=10,minrate=500
+RequestReadTimeout body=20,minrate=500

設定反映

  • 構文チェック
sudo apache2ctl configtest

Syntax OKとなることを必ず確認してください。でないと、apacheサービスが停止したままとなってしまい、サービス断が発生します。

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

active(running)を確認します。

まとめ

Nextcloudサーバーにおいて、RequestReadTimeout のbody待ち時間を延長することは、「モバイル環境や大容量ファイル送信時の安定性」を向上させるために非常に有効な手段です。

もし、ログ(Apacheのerror_log)に The timeout specified has expiredrequest body read timeout といった記録が残っている場合は、この値を調整してみることをお勧めします。

  • 参考コマンド
sudo grep "request body read timeout" /var/log/apache2/error.log

Matomo連携時にハマったこと。(連携手順とSSL設定)

概要

オープンソースのWeb解析、

WordPressとMatomoを別々のサーバーで運用し、wp-matomo(現在の名称は「Connect Matomo」)を使用して連携させる手順を整理しました。

この設定により、WordPress側で収集したデータを別サーバーのMatomoへ送信し、WordPressの管理画面内でアクセス統計を確認できるようになります。

環境

Matomo側

  • Ubuntu 24.04
  • Apache 2.4
  • Mod Security v2

WordPress専用サーバ

  • Connect Matomoプラグイン

さっくりとした手順

  1. Matomo サーバでセキュリティトークンを発行します。
  2. WordPressのプラグインを設定します。

1. Matomoサーバー側での準備

まず、データの受け皿となるMatomo側で「接続許可証(トークン)」と「サイトID」を確認します。

  1. Matomoにログインします。
  2. サイトの追加: まだWordPressサイトを登録していない場合は、[管理(歯車アイコン)] > [ウェブサイト] > [管理] から「新しいウェブサイトを追加」でWordPressサイトを登録します。この際表示される 「サイトID」(通常は1, 2…といった数字)をメモしておきます。
  3. Authトークンの発行:
    [管理(歯車アイコン)] > [個人] > [セキュリティ] を開きます。
    ページ下部の 「認証トークン(Auth tokens)」 セクションで「新しいトークンを作成」をクリックします。
    パスワードを入力し、用途(例: WP-Connection)を入力して作成します。
    表示された 長い英数字の文字列(トークン) をコピーして控えておきます。

注意
※注意
トークンは一度しか表示されません。必ずこのタイミングでコピーしてください。

2. WordPress側での設定

次に、WordPressにインストールしたプラグインにMatomoの情報を入力します。

  1. 設定画面を開く: WordPress管理画面の [設定] > [WP-Matomo](または Connect Matomo)を開きます。
  2. 接続設定(Matomoへ接続o)タブ:
  • Matomo モード: 「自己ホスト (HTTP API, デフォルト)」を選択します。
  • Matomo URL: MatomoがインストールされているサーバーのURLを入力します(例: https://analytics.example.com/)。末尾に / を含めてください。
  • 認証トークン: 先ほどMatomo側でコピーしたトークンを貼り付けます。
  1. 変更を保存: 「変更を保存」をクリックします。
  • 接続に成功すると、上部に緑色のメッセージが表示され、「Select site」 ドロップダウンからMatomoに登録したサイト名が選べるようになります。

3. トラッキングの有効化

接続しただけでは計測が始まらない場合があるため、以下の設定を確認します。

  1. トラッキングを有効化タブ をクリックします。
  2. トラッキングコードを追加: 「デフォルトのトラッキング」を選択します。これにより、WordPressの各ページに自動で計測タグが挿入されます。
  3. 変更を保存 をクリックして完了です。

よくあるトラブルと確認事項

別サーバー構成でうまく動かない場合は、以下をチェックしてください。

-URLの正確さ: Matomo URLが httphttps か、またサブディレクトリ(/matomo/ など)にインストールしていないか再確認してください。

  • ファイアウォール: WordPressサーバーからMatomoサーバーへの通信(ポート80または443)が許可されているか確認してください。
  • キャッシュ: WordPress側でキャッシュプラグインを使用している場合、設定反映後に一度キャッシュをクリアしてください。

自分がドハマリしたポイント

接続に失敗し、その接続ログが出てこない事態も発生しました。

その答えは、筆者が設定したapacheの.confファイルに入りました。

SSL設定を、筆者の標準の

SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2

から、以下の形で修正。

SSLProtocol -ALL +TLSv1.2 +TLSv1.3

これにより通信ができるようになりました。これには以下の理由があります。

1. 通信プロトコルの不一致(ハンドシェイクの失敗)

当初の設定: SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2 (意訳:TLS 1.3 のみを許可し、それ以外をすべて禁止する)

修正後の設定: SSLProtocol -ALL +TLSv1.2 +TLSv1.3 (意訳:TLS 1.2 と 1.3 の両方を許可する)

WordPressサーバーがMatomo APIにアクセスする際、内部ではPHPの cURL や openssl ライブラリが使用されます。もしWordPress側のOSやライブラリのバージョンが少し古い場合、あるいは最新であってもクライアント側の設定が TLS 1.2 までしか対応していない場合、Matomoサーバーが「TLS 1.3以外は受け付けない」という設定になっていると、共通の通信言語が見つからず、接続が即座に遮断されます。

2. PHP / cURL / OpenSSL の依存関係

WordPress(PHP)が外部サーバーと通信する際、バックグラウンドではシステム標準のOpenSSLライブラリが動いています。

TLS 1.3 を利用するには、OpenSSL 1.1.1以上が必要です。

もしWordPress側のサーバーOSが少し前の世代(例:Ubuntu 18.04やCentOS 7など)であったり、PHPのコンパイル環境が古い場合、TLS 1.3でのハンドシェイクを完遂できず、TLS 1.2での接続を試みようとします。

このとき、Matomo側が -TLSv1.2(1.2禁止)としていると、WordPress側は「接続できるプロトコルがない」と判断し、エラーを出します。

3. 「接続ログが出ない」という現象の理由

Apacheのアクセスログ(access.log)には、HTTPリクエストが成功または失敗した記録が残ります。しかし、SSL/TLSのハンドシェイク(暗号化の交渉)は、HTTPリクエストが送られる前段階で行われます。

TLS 1.2が禁止されている場合: 通信路を確立する前の「挨拶(ハンドシェイク)」の時点でサーバー側が「そのプロトコルは使えません」と接続を切断します。

こういうとき、以下のコマンドが有効です。

openssl s_client -connect analytics.example.com:443 -tls1_2

もし、このコマンドを実行して CONNECTED(00000003) と表示され、その後に証明書の情報がズラッと出てくれば、「インフラ(OS/ネットワーク/Apache)の土台は完璧である」という証明になります。

逆にここでエラーが出るなら、WordPressの設定(プラグイン)をいくらいじっても解決しないということが一瞬で分かります。

そのため、Apacheは「Webサイトへのアクセス」として認識する前に通信が終了してしまい、通常のアクセスログには一行も記録されないという事態が起こった話。

厳格な設定が裏目に出たという話でした。

WAFの「やりすぎ」と「見逃し」を飼い慣らす:ModSecurityチューニング実践

OSSでのWAFとして非常にメジャーなModSecurityとCRS(Core Rule Set)。

デフォルトでは非常に強力な保護が得られます。しかし、そのままではRedmineやNextcloudといった「複雑なリクエストを投げるアプリ」はまともに動きません。

今回は、筆者の例を元に、偽陽性(誤検知)を回避しつつ、偽陰性(すり抜け)を最小限に抑える設定術を解説します。

筆者環境

  • Ubuntu 24.04
  • Apache 2.4
  • Mod Security v2

動いているWebアプリ

  • Nextcloud
  • Redmine
  • BookStack

と、WAFが偽陽性を誘発するようなWebアプリ群です。

1. そもそも「偽陽性」「偽陰性」とは?

WAFを運用する上で避けて通れない2つの概念です。

用語状態影響対策
偽陽性 (False Positive)正常な通信を攻撃と判定ユーザーがログインできない、投稿が消えるルールの除外設定(Exclusion)を行う
偽陰性 (False Negative)攻撃的な通信を正常と判定脆弱性を突かれ、被害が出るシグネチャの更新、独自ルールの追加

「守りを固めれば不便になり、利便性を取れば危うくなる」。このジレンマを解決するのが、筆者が設定している個別除外ルールの設計です。

2. 確実だけど偽陰性を産むやり方「ID除外」

これは筆者が2025年9月まで実施していた例です。

たとえば、自分がRedmineで投稿した記事がエラーとなってしまった。そのエラーを

awk '
/ModSecurity/ {
  if (match($0, /\[client ([0-9\.]+):/, ip_arr) && match($0, /\[id "([0-9]+)"\]/, id_arr)) {
    print id_arr[1], ip_arr[1];
  }
}' /var/log/nextcloud_error.log | sort | uniq -c

等として調査。以下の結果が出てきたとします。

     36 911100 127.0.0.1
    267 911100 aaa.bbb.ccc.ddd
     65 920420 aaa.bbb.ccc.ddd
     36 949110 127.0.0.1
    267 949110 aaa.bbb.ccc.ddd
     36 980130 127.0.0.1
    267 980130 aaa.bbb.ccc.ddd
IDルール名(概要)挙動の説明
911100Method is not allowed by policy許可されていないHTTPメソッド(GET/POST以外など)を検知します。
920420Request content type is not allowed by policyContent-Typeヘッダーが許可リストにない場合に反応します。
949110Inbound Anomaly Score Exceeded重要: これは特定の攻撃を指すものではなく、他のルールの合計スコアが閾値を超えたため「ブロックした」という最終結果を示すIDです。
980130Inbound Anomaly Score Exceeded (Reporting)949110と同様に、リクエスト全体の異常スコアが高かったことを報告するログ用のIDです。

これらの偽陽性に引っかかったIDを割り出し、/etc/apache2/sites-available/example.confなどで

 ## 最初は検知モード

 SecRuleEngine DetectionOnly
+
+## 偽陽性と判断したID
+SecRuleRemoveById 911100
+SecRuleRemoveById 920420
+SecRuleRemoveById 949110
+SecRuleRemoveById 980130
+
 </VirtualHost>

を追加するのは確実に偽陽性“は”防ぐことができます。しかし、これでは「本当に上記の脆弱性を突いた攻撃」は素通しとなってしまいます。

特に、攻撃者は、クローリングスクリプトなどで内容を確認し、「この記事があればこのルールは無効化されているはず」と当たりをつけます。定番の防御ツール、ましてやOSSともなると、

  • 偽陽性になりやすい(取り除かれやすい)ルール
  • 一発アウトになりやすい文章

は極めて多いのです。

特に、技術ブログのように

  • コマンド羅列
  • SQLコマンドをベタ打ち
  • スクリプト文の紹介

などは、投稿した瞬間にエラーとなったため、渋々SecRuleRemoveIdで検知しないようにした方は極めて多いのではないでしょうか。

3. CRSの裏をかく「防御未満の攻撃」

また、CRSは「このラインまでだったら大丈夫だ」という「甘い判断基準」が悲しいことに存在します。

以下は、ある日のModSecurityエラーログの一部です(情報は無害化済み)。

# 1. Slowloris攻撃を疑わせる矛盾したConnectionヘッダーの検知
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?i)(?:keep-alive(?:,\\\\s*close|...)" at REQUEST_HEADERS:Connection. [id "10001"] [msg "[CUSTOM RULE] Contradictory Connection header, possible Slowloris probe."]

# 2. IPアドレスでの直接アクセスを検知
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Warning. Pattern match "(?:^([\\\\d.]+|...)" at REQUEST_HEADERS:Host. [id "920350"] [msg "Host header is a numeric IP address"]

# 3. アノマリスコアが閾値を超えたため遮断
[Wed Jan 14 12:00:00 2026] [security2:error] [client 192.0.2.100] ModSecurity: Access denied with code 403 (phase 2). Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 6)"]

ログから読み取れる攻撃者の意図

  • 矛盾したConnectionヘッダー:
  • Connection: keep-alive, close という通常ではありえないヘッダーが含まれていました。これは Slowloris などのDoS攻撃ツールに見られる特徴です。
  • IPアドレスでのホスト指定:
  • ドメイン名ではなくIPアドレス(例: 203.0.113.1)を指定してアクセスしています。これはボットによる無差別なスキャンの典型的な挙動です。

この、Mod_Securityのルールの外を狙った「じわじわとリソースを削っていく」攻撃こそ遮断する必要があります。

4. 「Webは守る」「投稿はスルーする」の「両方」をやる

この「偽陽性は防ぎつつ本来の防御を確立する」ために筆者が行っている手段は「例外ルールによるチューニング」です。

前提として:

こちらのリンク先のような形でCRSを設置。筆者記事:ModSecurityインストール

cd /usr/share/modsecurity-crs/coreruleset/rules && pwd

として、

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confファイルを作成します。

提示コンフィグ(ドメイン匿名化済み)

以下、筆者の例です。

  • 自身の環境に合わせてください。
  • 特に、正規表現としてドメイン名を利用しています。
  • 確実な正規表現の書き方は、あなたが今見ている機器で探してください。
#
# === CRS Exclusions - Before Rules Execution (Organized) ===
#

# ===================================================================
# 1. 共通ルール・汎用ルール (General/Common Rules)
# ===================================================================

# 1-1. 遅い通信(Slowloris)対策
# 矛盾するConnectionヘッダーを持つリクエストを遮断
SecRule REQUEST_HEADERS:Connection "@rx (?i)(?:keep-alive(?:,\sclose|,\skeep-alive)|close(?:,\skeep-alive|,\sclose))" \
    "id:10001,phase:1,t:none,block,msg:'[CUSTOM RULE] Contradictory Connection header, possible Slowloris probe.',tag:'application-attack',tag:'PROTOCOL_VIOLATION/INVALID_HREQ',setvar:'tx.inbound_anomaly_score_pl1=+%{tx.critical_anomaly_score}'"

# 1-2. WordPress スキャン対策
# 存在しないWPパスへのアクセスは、問答無用でスコアを加算して404へ飛ばす
# wordpressを設置している方は、このセクションを無効化してください
SecRule REQUEST_URI "@rx /(?:wordpress|wp-admin|wp-content|wp-includes|xmlrpc\\.php)" \
    "id:10002,phase:2,pass,nolog,capture,msg:'[CUSTOM] WordPress Probe Detected. Scored +5.',tag:'ATTACK_WP_PROBE',setvar:'tx.anomaly_score_pl1=+5',setvar:'tx.lfi_score=+5',setvar:'tx.wp_probe_detected=1'"

SecRule TX:wp_probe_detected "@eq 1" \
    "id:10003,phase:2,deny,nolog,msg:'[CUSTOM] Final action: Deny with 404 status.',status:404"

# 1-3. IPアドレス直打ちアクセス対策
# ホスト名ではなくIPで直接アクセスしてくる怪しい挙動を即座にマーク
SecRule REQUEST_HEADERS:Host "@rx ^[\d.]+$" \
    "id:10004,\
    phase:1,\
    deny,\
    status:403,\
    log,\
    msg:'[CUSTOM RULE] Host header is a numeric IP address. Blocked immediately.',\
    tag:'application-attack',\
    tag:'PROTOCOL_VIOLATION/INVALID_HREQ'"

# ===================================================================
# 2. アプリ別除外: BookStack (Knowledge Base)
# ===================================================================
SecRule SERVER_NAME "@streq bookstack.example.com" \
    "id:1001,phase:1,nolog,pass,skipAfter:END_BOOKSTACK_RULES_PRE"

# PUTメソッドの許可(下書き保存用)
SecRule REQUEST_URI "@rx ^/(ajax/page|books|pages)/" \
    "id:1003,phase:1,nolog,pass,setvar:'tx.allowed_methods=%{tx.allowed_methods} PUT',ctl:ruleRemoveById=911100"

# 記事投稿時のSQLi/XSS誤検知を除外
SecRule REQUEST_URI "@rx ^/(books|ajax/page|pages)/" \
    "id:1005,phase:2,nolog,pass, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-RCE, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-LFI, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-XSS, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-SQLI, \
    ctl:ruleRemoveById=921130, \
    ctl:ruleRemoveById=934130"

SecMarker END_BOOKSTACK_RULES_PRE

# ===================================================================
# 3. アプリ別除外: Nextcloud (Cloud Storage)
# ===================================================================
SecRule SERVER_NAME "@streq nextcloud.example.com" \
    "id:3001,phase:1,nolog,pass,skipAfter:END_NEXTCLOUD_RULES_PRE"

# WebDAV関連メソッド(PROPFIND等)を許可しないと同期が壊れるため除外
SecRule REQUEST_URI "@rx ^/remote\.php/" \
    "id:3002,phase:1,nolog,pass, \
    setvar:'tx.allowed_methods=%{tx.allowed_methods} PROPFIND OPTIONS REPORT PUT DELETE MKCOL', \
    ctl:ruleRemoveById=911100,ctl:ruleRemoveById=920420"

SecMarker END_NEXTCLOUD_RULES_PRE

# ===================================================================
# 4. アプリ別除外: Redmine (Project Management)
# ===================================================================
SecRule SERVER_NAME "@rx ^(redmine|projects)\.example\.com$" \
    "id:4001,phase:1,nolog,pass,skipAfter:END_REDMINE_RULES_PRE"

# PATCHメソッド(チケット更新)の許可
SecRule REQUEST_URI "@rx ^/(issues|projects)/" \
    "id:4002,phase:1,nolog,pass,setvar:'tx.allowed_methods=%{tx.allowed_methods} PATCH',ctl:ruleRemoveById=911100"

# チケット内容(コードブロック等)がSQLiやRCEと誤認されるのを防ぐ
SecRule REQUEST_URI "@rx ^/projects/[^/]+/(issues|knowledgebase/articles|news|issue_templates)|/issues|/journals|/questions|/issue_templates" \
    "id:4003,phase:2,nolog,pass, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-RCE, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-SQLI, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-LFI, \
    ctl:ruleRemoveByTag=OWASP_CRS/ATTACK-XSS, \
    ctl:ruleRemoveByTag=OWASP_CRS/PROTOCOL-ATTACK"

# View CustomizeプラグインでJS/CSSを編集する際の広範な除外
SecRule REQUEST_URI "@rx ^/view_customizes(?:/\d+)?$" "id:4006,phase:2,nolog,pass,t:none,chain"
  SecRule REQUEST_METHOD "@rx ^(POST|PUT|PATCH)$" \
    "ctl:ruleRemoveTargetByTag=OWASP_CRS/ATTACK-RCE;ARGS:view_customize[code],\
     ctl:ruleRemoveTargetByTag=OWASP_CRS/ATTACK-XSS;ARGS:view_customize[code]"

SecMarker END_REDMINE_RULES_PRE

なぜこれが必要なのか

  1. プロトコルの柔軟性: 標準のCRSは「GET/POST」以外のメソッド(PUT, PATCH, PROPFINDなど)を攻撃の兆候として厳しく制限します。しかし、モダンなWebアプリ(特にNextcloud)にはこれらが不可欠です。
  2. 偽陽性の温床「本文検査」: Redmineで「SQLの書き方」をチケットに書くと、WAFはそれを「SQLインジェクション攻撃」とみなしてブロックします。特定のパスに対してのみ、特定の検査(Tag)をオフにすることで、利便性を確保しています。
  3. 無駄なスキャンの排除: WordPressを使っていないサーバーへのWordPress用スキャンは、リソースの無駄です。これを早期に検知して「スコア加算+404応答」とすることで、後続の重い検査をスキップしつつ攻撃者をあしらいます。
  4. SecRuleRemoveByTag の威力: IDを1つずつ消すと、CRSがアップデートされた際に追加された「新しいSQLi検知ルール」に対応できません。今回のように OWASP_CRS/ATTACK-SQLI というタグ単位で除外することで、将来のアップデート後も「投稿機能だけは常に使える」状態を維持できます。
  5. 「404」で返す心理的効果: 攻撃者(のボット)は、403を返すと「あ、WAFがあるな」と判断して攻撃手法を変えてくることがありますが、404を返すと「このIPには何も存在しない」と判断してリストから外してくれる可能性が高まります。

設定のテストと反映

上記、設定を行ったら

  • 構文チェック
sudo apache2ctl configtest

apache 再起動

sudo systemctl restart apache2.service

apache 再起動確認

systemctl status apache2.service

active(running)を確認します。

偽陽性排除確認

実際にRedmine / Nextcloud等にアクセスして、投稿をしても偽陽性にならない(エラーにならない)を確認できれば成功です。

5. 賢いWAF運用のコツ

WAFは一度設定して終わりではありません。

ログを確認する:

/var/log/apache2/error.log や ModSecurityのアAuditログを監視し、id:xxxxx が出たら「それは本当に攻撃か?」を疑い、必要なら今回のコンフィグに除外ルールを追記します。

身内には優しく、外敵には厳しく:

特定のソースIPや、自社ドメイン宛の正常な操作(Redmineの更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。

この、例外ルールを正しく使うことで、スクリプトキディやクローラーに対して、このような大見得を切ってやりましょう。

『任務は遂行する』
…………
部下も守る
おまえごときに両方やるというのは
そうムズかしい事じゃあないな

Page 1 of 6

Powered by WordPress & Theme by Anders Norén