タグ: apache Page 1 of 5

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の更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。

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

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

Nextcloud高性能バックエンドサーバ (Signaling Server) 構築メモ。

概要:

Nextcloudアップデート後に出るようになった

高性能バックエンドが構成されていません - 高性能バックエンドなしでNextcloud Talkを実行すると、非常に小規模な通話 (最大2~3人の参加者)でのみ利用できます。複数の参加者との通話がシームレスに機能するようにするためには高性能バックエンドを設定してください。

このメッセージを対処するため、「高性能バックエンドサーバ」とやらをインストールすることにします。

当初はこれは考慮していませんでした(個人用のファイルサーバとして使っていたため)が、

「自分の信条を曲げてまでDockerを入れてしまった以上、こいつもDockerで入れる」

と“それはそれ、これはこれ/That's another matter entirely, chaps."の精神でインストールしていきます。

これの導入により、何が変わるのか?

接続の安定化・高速化です。

これまでPHP(Nextcloud本体)が行っていた「誰と誰をつなぐか」という重い処理(シグナリング)を、専用のGo言語プログラム(高性能バックエンド)が肩代わりします。これにより、通話開始までの時間が短縮され、サーバー全体の負荷が劇的に下がります。

環境

  • Nextcloud 32.0.3
  • PHP 8.3
    • PHP-FPM 8.3
  • MySQL 8.0
  • Apache 2.4

さっくりとした手順

  1. 【コマンドライン】(オプション)docker-composeをインストールします。
  2. 【コマンドライン】レット文字列を生成します。
  3. 【コマンドライン】Dockerファイルを作成します。
  4. 【コマンドライン】コンテナを起動します。
  5. 【コマンドライン】Apache設定ファイルを編集します。
  6. 【Webブラウザ】動作を確認します。
  7. 【Webブラウザ】Nextcloud管理画面を設定します。

(オプション)docker-composeのインストール

sudo aptitude install docker-compose

筆者の好みでaptitudeを用いています。

シークレット文字列を生成します。

openssl rand -hex 16

4accc25d95464f00a9537dc956bd5414といった文字列が出ます。これを以下「共有シークレット」と呼びます。

Docker設定ファイルを作成します。

任意のコンテナ設定ファイル群に以下を作成します。

mkdir -p /path/to/container/directory/nextcloud-signaling
cd /path/to/container/directory/nextcloud-signaling && pwd

このディレクトリに入り、ファイルを作っていきます。

※ドメイン名などは自分の環境に合わせましょう。※

  • server.conf
[http]
listen = 0.0.0.0:8080

[app]

debug = false

[sessions]

# 手順1で生成した文字列を使用 hashkey = [共有シークレット] blockkey = [共有シークレット]

[backend]

# Nextcloud本体のURL backend = https://hoge.example.com # Docker内部からホストへのSSL検証をスキップ (接続エラー回避のため必須) allowall = true # 手順1で生成した文字列を使用 secret = [共有シークレット]

[nats]

url = nats://nats:4222

[mcu]

# 現時点ではJanus(MCU)は使用しないため無効化 # type = janus # url = ws://127.0.0.1:8188

※ これらシークレット文字列は、別個にしておいた方がより安全です。

-docker-compose.yml

version: '3.6'

services:
  nats:
    image: nats:2.9
    restart: always

  signaling:
    image: strukturag/nextcloud-spreed-signaling:latest
    restart: always
    ports:
      - "127.0.0.1:8080:8080" # ホストのApacheからのみアクセス許可
    volumes:
      - ./server.conf:/config/server.conf
    depends_on:
      - nats
    extra_hosts:
      # Docker内部から hoge.example.com を解決するためにホストIPを指定
      - "hoge.example.com:172.17.0.1"

※重要: extra_hosts には ip addr show docker0 で確認したホストIP (例: 172.17.0.1) を記述します。

Dockerファイルを起動します。

cd /path/to/container/directory/nextcloud-signaling && pwd

念のため、先ほど作成したディレクトリにいることを確認してください。

  • コンテナ起動
sudo docker-compose up -d
  • コンテナ起動確認
sudo docker ps

STATUS が UPになっていれば問題なく起動できています。

Nextcloudのバーチャルサイトの編集

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

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

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

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

  • ファイル編集

/etc/apache2/sites-available/nextcloud.conf を、任意の方法で編集します。(エディタという宗教問題が絡むため)

他のリダイレクト設定やセキュリティ設定(RewriteRuleなど)に干渉されないよう、<VirtualHost *:443> ブロック内のなるべく上の方に記述するのがコツです。

<VirtualHost *:443>
    # (その他既存の設定...)

    # ====================================================
    # Signaling Server 設定
    # ====================================================

    # 1. バックエンド(Docker)に正しいホスト名とプロトコルを伝える
    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"

    # 2. プロキシ設定
    # "upgrade=websocket" オプションにより、http/ws を自動判別してヘッダーを渡す
    ProxyPass "/standalone-signaling/" "http://127.0.0.1:8080/" upgrade=websocket
    ProxyPassReverse "/standalone-signaling/" "http://127.0.0.1:8080/"

    # ====================================================

    # ... (これ以降にDocumentRootやセキュリティ設定が続く) ...
  • 編集後の差分確認
diff -u /path/to/backup/directory/nextcloud.conf.$(date +%Y%m%d) /etc/apache2/sites-available/nextcloud.conf

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

+# ====================================================
+    # Signaling Server 設定
+    # ====================================================
+    # 1. バックエンド(Docker)に正しいホスト名とプロトコルを伝える
+    ProxyPreserveHost On
+    RequestHeader set X-Forwarded-Proto "https"
+
+    # 2. プロキシ設定
+    # "upgrade=websocket" オプションにより、http/ws を自動判別してヘッダーを渡す
+    ProxyPass "/standalone-signaling/" "http://127.0.0.1:8080/" upgrade=websocket
+    ProxyPassReverse "/standalone-signaling/" "http://127.0.0.1:8080/"
+    # ====================================================
+

設定反映

  • 整合性確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • apache再開前確認
systemctl status apache2.service

active(running)を確認します

  • apache再開
sudo systemctl restart apache2.service
  • apache再開確認
systemctl status apache2.service

active(runnning)を確認します。

動作確認

ブラウザで、

https://hoge.example.com/standalone-signaling/api/v1/welcome

にアクセスし、{"nextcloud-spreed-signaling":"Welcome", ...}の表示が出ていればOKです。

Nextcoloud管理画面設定

Nextcloudに管理者ログインし、[管理設定] > [Talk] > [高性能バックエンド] を設定します。

  • 高性能バックエンドURL: https://hoge.example.com/standalone-signaling/
  • 共有シークレット: 手順1で生成した文字列
  • SSL証明書を検証する: チェックを入れる

設定後、「保存」ボタンをクリックします。 「OK: 実行中のバージョン: x.x.x~docker」 と表示されれば完了です。

FAQ

Q. Dockerを入れることでサーバそのものが高負荷になるということは?

A. むしろ逆で、サーバー全体の負荷は「劇的に下がります」。

  • これまで(高性能バックエンドなし):
    • Nextcloudの画面を開いている間、ブラウザは数秒おきに「着信はありますか?」「新しいチャットはありますか?」とサーバー(Apache/PHP)に聞きに行きます(ポーリング方式)。 そのたびに Apacheが動き、PHPが起動し、データベースに接続し… という重い処理が走っていました。これがサーバーを高負荷にする原因です。
  • これから(高性能バックエンドあり):
    • Docker(Go言語)が、ブラウザと「1本のパイプ(WebSocket)」を繋ぎっぱなしにします。 情報はパイプを通ってスッと届くため、「着信確認」のための無駄な処理がゼロになります。

Q. メモリは食いますか?

A. メモリは食いますが、CPUは休まります。

  • Dockerコンテナが常駐するため、約50MB〜100MB程度のメモリ を常に確保(占有)します。これは増えます。
  • しかし、上記の「無駄な確認作業」がなくなるため、CPUの利用率はガクンと下がります。 サーバーにとって一番きついのは「メモリを確保すること」ではなく「CPUをブン回すこと」なので、トータルではサーバーが楽になります。

もっとさっくり言うと:

  • 1. 以前(高性能バックエンドなし)
    • 動作: ブラウザが「ねえ、着信ある?」「ねえ、メッセージ来た?」と、数秒おきにApacheを叩き起こしていました。
    • 負荷: そのたびに、数十MBのメモリを使うPHPプロセス が起動し、データベースに接続し、確認して終了する…という「重い開け閉め」を繰り返していました。
  • 2. 現在(高性能バックエンドあり)
    • 動作: 今回導入した signaling コンテナ(7MB/筆者環境)が、ブラウザと細い糸電話(WebSocket)を繋ぎっぱなしにします。
    • 負荷: 何もなければただ待っているだけ(CPU 0.02%/筆者環境)。着信があった時だけ、「来たよ!」と一瞬で伝えます。

重たいApacheやPHPは、本当に必要な画面表示の時だけ働けば良くなったので、サーバー全体が静かになり、反応速度(レスポンス)が向上します。

『ONE OUTS』システム(Apache/Mod_Security/テキストファイル連携によるWeb防御)解説。 1 OUT

概要

これは、筆者が自分のサーバに組み込んでいるWebセキュリティシステム(と言ってもスクリプトと設定の組み合わせ) 『ONE OUTS』について述べたものです。

  1. OSSで動くこと
  2. シンプルな仕組みであること
  3. メンテナンス性と再現性が高いこと

を目標に構築しました。

メンテナンスが高いとは言え、少々複雑な流れを含むため、いくつかに分けて解説します。

名前の由来

甲斐谷忍先生による同名の野球漫画『ONE OUTS』から来ています。

  • 持ち玉はストレートのみ
  • パワーよりも心理戦で打者を翻弄
  • ルールの裏をかきながらもルールに従う姿勢

などをイメージしながら構築しました。

環境

以下の環境で動いています。

  • Ubuntu 24.04
  • Apache 2.4
    • モジュール
      • mod_rewrite
      • mod_ssl
      • mod_header
      • mod_alias
      • mod_security2
  • シェルスクリプト

次のページから、実際のファイル群を示します。

Webサーバーの基盤設定:ディレクトリ変更とVirtualHostの目的(ローカル環境編)

Apacheの初期設定とWebコンテンツのルートディレクトリを変更する理由と、たとえサイトが一つでもVirtualHostを設定する必要性についてのメモです。

大前提「これはローカル環境での手順」です。

なぜなら、これはインターネット上サーバでの絶対的なルールが抜けています。

  1. https通信の対応
  2. ログ設定の未考慮

この2つは別の項でも解説しますが、これらを考慮しないと

  • 「管理が甘いサーバ」として攻撃者の格好の的になる
  • 「トラブル発生時の証跡をつかめない」

の2点が重くのしかかります。

理由1.なぜホームディレクトリ(DocumentRoot)を変更するのか?

標準でapacheを入れると、Webサーバーのコンテンツディレクトリとして使われるのは /var/www です。

しかし、本稿では/home/www-data へとホームディレクトリを変更します。これには、以下の明白な理由があります。

理由詳細
セキュリティ上の分離 (Preference)/var ディレクトリは、ログ(/var/log)や一時ファイル(/var/tmp)など、システムが頻繁に書き換える揮発性のデータを格納するために設計されています。Webコンテンツを /home に置くことで、システムデータとユーザーデータ(コンテンツ)を明確に分離し、万が一のシステム障害や冗長化の際に管理が容易になります。
ユーザープロファイルの整合性 (Practicality)www-data ユーザーは通常シェルログインできませんが、Ruby on RailsやNode.jsなどのモダンなアプリケーションbundle install やパッケージのインストールを行う際や、gitを利用する際に環境変数として自身のホームディレクトリを参照することがあります。これを /home/www-data に設定することで、アプリケーションがスムーズに動作し、予期せぬエラーを防ぐことができます。

理由2. なぜ単一サイトでもVirtualHost(バーチャルサイト)を仕込むのか?

VPS/ローカルサーバでで公開するサイトが一つだけであっても、デフォルトの設定ファイル (000-default.conf など) ではなく、個別のVirtualHost設定ファイルを作成して運用すべきです。

理由詳細
セキュリティ(デフォルト設定の無効化)デフォルト設定 (000-default.conf) は通常、/var/www/html を DocumentRoot としています。これを無効化し、カスタムVirtualHostのみを有効にすることで、意図しないディレクトリが公開されるリスクや、デフォルト設定のバージョン情報などが露出するリスクを防ぎます。
スケーラビリティと将来性将来、サブドメイン(例: blog.example.com)や別のドメインを追加する際に、既存の設定をコピーして修正するだけで済みます。最初からVirtualHostの構造を採用することで、設定の拡張が容易になり、メンテナンス性が向上します。
HTTPS(SSL)対応の必須要件HTTP(ポート80)からHTTPS(ポート443)への強制リダイレクトや、独自のSSL証明書の設定は、VirtualHostブロック (<VirtualHost *:443>) ごとに行うのが一般的です。VirtualHostが基本となることで、この必須のセキュリティ対策をスムーズに組み込めます。

さっくりとした手順

  1. ホームディレクトリの作成と設定
  2. /etc/passwdの書き換え(要注意)
  3. VirutalHostの設定
  4. 設定の有効化と確認

1. 新たなホームディレクトリの作成と設定

VPSのコンテンツ保存場所を /home/www-data に統一します。

  • ディレクトリ作成
sudo mkdir -p /home/www-data
  • ディレクトリの所有者変更
sudo chown -R www-data:www-data /home/www-data
  • 設定確認
ls -ld /home/www-data

 → 所有者がwww-dataになっていることを確認

2. /etc/passwd ファイルの書き換え

www-data ユーザーのホームディレクトリの定義を /var/www から変更します。

** 注意:** /etc/passwd はシステムアカウントを制御する重要ファイルです。必ずバックアップを取り、変更は一行のみであることを確認してください。

  • バックアップ作成
sudo cp -pi /etc/passwd /path/to/backup/directory/passwd.$(date +%Y%m%d)

→ 任意のバックアップディレクトリを指定します。また、$(date +%Y%m%d)を仕込むことで、「いつ」このバックアップを取ったかが明白になります。

  • バックアップ作成確認
sudo diff -u /path/to/backup/directory/passwd.$(date +%Y%m%d) /etc/passwd

→ 面倒でもこの作業は必須です。というのも、差分を取ることで「オリジナルとバックアップの両方が作成されている」というのを、コマンドという第三者の絶対的な目で見ることができるからです。

→ また、先ほどのcpと逆になっていることにも注意してください。これは、変更後に「どの行が惹かれ、どの行が足されたか」を明確化するためです。

  • sedによるファイル書き換え
sudo sed -i 's|/var/www|/home/www-data|' /etc/passwd

→ 特に注意が必要です。これをミスるとサーバ全体のロックアウトなども容易に発生します。

  • 書き換え確認
diff -u /path/to/backup/directory/passwd.$(date +%Y%m%d) /etc/passwd

差分が以下のみであることを確認します。

-www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
+www-data:x:33:33:www-data:/home/www-data:/usr/sbin/nologin

→ ここで、「なぜ、逆にして差分を取るのか」を明確化。

diff -u /etc/passwd /path/to/backup/directory/passwd.$(date +%Y%m%d)

とした場合、得られる情報は「まるで逆」になります。

+www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
-www-data:x:33:33:www-data:/home/www-data:/usr/sbin/nologin

となり、「バックアップの方が正しい修正」となってしまうからです。

3. VirtualHost 設定の準備(Redmineを例にする)

Redmine(動的アプリケーションの例)を /home/www-data 内のディレクトリで公開するためのベース設定を作成します。

cat <<- __EOF__ | sudo tee -a /etc/apache2/sites-available/redmine.conf
<VirtualHost *:80>
    ServerName 【hoge.example.com】
    # ServerNameは自身が設定したドメイン名に置き換えてください。

    DocumentRoot /home/www-data/redmine/public

    <Directory /home/www-data/redmine/public>
        # .htaccessによる設定変更を許可
        AllowOverride All
        # 複数Viewを無効化(アプリケーションのルーティングを優先させるため)
        Options -MultiViews
        # 全てのアクセスを許可
        Require all granted
    </Directory>
</VirtualHost>
__EOF__

4. 設定の有効化と確認

作成したVirtualHostを有効化し、デフォルトサイトを無効化します。

  • 既存のデフォルトサイトを無効化
sudo a2dissite 000-default.conf
  • 新しいサイト設定を有効化
sudo a2ensite redmine.conf
  • 構文チェック
sudo apache2ctl configtest

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

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

active(running)を確認します。

これで、Apacheの基盤がセキュリティと運用効率を考慮したカスタム設定で動作するようになりました。

改訂・再編集『Ubuntu24.04にApacheをインストールするための具体的手順』

この記事を今のトレンドに合わせた上でのステップバイステップとした記事です。

概要

Ubuntu24.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 /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

Growiのフォントをサーバ内のフリーフォントに変更。

Growiの表示フォントを、サーバー内に配置したカスタムフォント(kiloji)に変更するメモを記します。

BookStackと同じような方法でいけるかと思いましたが、Apacheの追加設定が必要でした。

環境(作業の前提条件)

  • Growi v7.3.0
    • Growiの実行ユーザはroot
  • Ubuntu 24.04
  • Apache 2.4によるリバースプロキシ
    • 結果的に、これが解決策でした。

さっくりとした手順

  1. フォントの配置(フォントはこちらを参考に)
  2. Webサーバの設定変更
  3. カスタムCSSの追加
  4. フォント変更確認

フォントファイルの配置

Webブラウザからアクセスできる公開ディレクトリに、使用したいフォントファイルを設置します。上記リンク先に示した「kiloji」を使います。

  • ディレクトリ移動
cd /path/to/growi/packages/preset-themes/public

/path/to/growiは自分の環境に合わせます。(筆者環境/home/www-data/growi)

  • フォント格納ディレクトリ作成
sudo mkdir -p ./fonts/kiloji
  • フォント格納

上記ディレクトリ(筆者例では/home/www-data/growi/fonts/kiloji)

にフォント一式を格納します。

Webサーバー(Apache)の設定ファイルを変更

この設定がハマった部分です。というのも、Growiの使用上、

https://【growi】/fonts/kiloji.wof

などにアクセスしても、アプリはURLとして解釈してしまうからです。

そのため、リバースプロキシで運用しているApacheのconfファイル/etc/apache2/sites-available/growi.confなどの修正を行います。

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

→ 差分がなければ(エラーが表示されなければ)バックアップ成功です。

  • ファイル編集

※編集前

FileETag None 

<FilesMatch "\.(js|css|png|jpg|gif|svg|woff2?)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

# リバースプロキシー設定
RewriteEngine on
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteRule /(.*) ws://localhost:3000/$1 [P,L]

ProxyPass        / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/

※編集後

これは、リバーズプロキシ設定の直情に記載します。

# (1) 静的ファイル(フォントなど)のキャッシュ設定
FileETag None
<FilesMatch "\.(js|css|png|jpg|gif|svg|woff2?)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>

# (2) /fonts/ ディレクトリの場所をApacheに教える【重要:この部分を追加してください】
Alias "/fonts/" "/home/www-data/growi/packages/preset-themes/public/fonts/"
<Directory "/home/www-data/growi/packages/preset-themes/public/fonts/">
    Require all granted
</Directory>

# (3) /fonts/ へのアクセスはプロキシの対象から除外する
ProxyPass "/fonts/" "!"

※差分(以下のコマンドを発行して差分を確認)

-FileETag None 
-
+# (1) 静的ファイル(フォントなど)のキャッシュ設定
+FileETag None
 <FilesMatch "\.(js|css|png|jpg|gif|svg|woff2?)$">
     Header set Cache-Control "public, max-age=31536000, immutable"
 </FilesMatch>

+
+# (2) /fonts/ ディレクトリの場所をApacheに教える【重要:この部分を追加してください】
+Alias "/fonts/" "/home/www-data/growi/packages/preset-themes/public/fonts/"
+<Directory "/home/www-data/growi/packages/preset-themes/public/fonts/">
+    Require all granted
+</Directory>
+
+
+# (3) /fonts/ へのアクセスはプロキシの対象から除外する
+ProxyPass "/fonts/" "!"
+
 # リバースプロキシー設定
 RewriteEngine on
 RewriteCond %{HTTP:Upgrade} websocket [NC]
diff -u /path/to/backup/directory/growi.conf.$(date +%Y%m%d) /etc/apache2/sites-available/growi.conf

設定反映を確認します。

  • 構文確認
sudo apache2ctl configtest

Syntax OKを確認します。

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

active (running)を確認します。

GrowiのカスタムCSS設定

  1. Growiに管理者としてログインします。
  2. 「管理」>「カスタマイズ」を開きます。
  3. 「カスタムCSS」のテキストボックスに、以下のCSSコードを貼り付ける。 /* --- フォントの定義 --- */ @font-face { font-family: 'kiloji'; font-style: normal; font-weight: normal; src: url('/fonts/kiloji/kiloji.woff2') format('woff2'), url('/fonts/kiloji/kiloji.woff') format('woff'), url('/fonts/kiloji/kiloji.ttf') format('truetype'); } @font-face { font-family: 'kiloji'; font-style: normal; font-weight: bold; src: url('/fonts/kiloji/kiloji_b.woff2') format('woff2'), url('/fonts/kiloji/kiloji_b.woff') format('woff'), url('/fonts/kiloji/kiloji_b.ttf') format('truetype'); } /* --- フォントの適用 (アイコンを壊さない修正版) --- */ body, .growi, .page-main, .wiki, .sidebar-content, h1, h2, h3, h4, h5, h6, p, li, .btn, a, input, textarea { font-family: 'kiloji', sans-serif !important; } /* 基本の文字サイズを改めて指定 */ body { font-size: 16px; }
  4. ページ下部の更新ボタンを押し、設定を保存します。
  5. ブラウザでGrowiのページを開き、キャッシュをクリアして再読み込み(Ctrl + Shift + R)し、フォントが変わっていることを確認できれば作業完了です。

まとめ

今回、この手順がうまくいったのはひとえに「Apacheによるリバースプロキシ化」でした。

http://【growiサイト】:3000

のように、直接(Growiのような)nodeアプリに接続していたら、フォントファイルを透過させるかのようにアクセスさせることは難しかったでしょう。

Apache & PHP-FPM 管理スクリプト (引数対応版)。

Webサーバを運用する上で役立ている

  1. Apacheで有効になっているサイトの設定情報を表示
  2. 構文を確認
  3. 問題が無ければApache再起動
  4. 再起動後にステータスを表示する

スクリプト。非常に便利なものでしたが、VPS新調に際してPHP-FPMを導入。

そこで、

  1. PHP-FPMも構文チェックを行う
  2. 他、引数による処理

を改訂しました。(元ネタ:ChatGPT、リファクタリングはdeepseekとGemini)

スクリプト内容

  • apache2-check.sh
#!/bin/bash

#================================================================
# Apache & PHP-FPM 管理スクリプト (引数対応版)
#================================================================
#
# 機能:
# 1. Apacheで有効になっているサイトの設定情報を表示します。
# 2. ApacheとPHP-FPMの設定ファイルの構文をチェックします。
# 3. ApacheとPHP-FPMの再起動を個別に確認し、実行します。
# 4. 再起動後に各サービスのステータスを表示します。
#
# 引数:
#  -y : 確認プロンプトをすべてスキップし、自動で'yes'と応答します。
#  -a : Apacheのみを対象とします。
#  -p : PHP-FPMのみを対象とします。
#  -h : このヘルプを表示します。
#
#================================================================

# --- 設定項目 ---

# Apacheのサイト設定が格納されているディレクトリ
SITES_DIR="/etc/apache2/sites-enabled"

# 操作対象のPHPバージョン (例: "8.3", "8.2", "7.4")
PHP_VERSION="8.3"

# --- 設定ここまで ---


# --- スクリプトの動作を制御するフラグ ---
AUTO_YES=false
RESTART_APACHE=true  # デフォルトでは両方実行
RESTART_PHP=true     # デフォルトでは両方実行
EXCLUSIVE_MODE=false # -a または -p が指定されたかを判定するフラグ

# --- ヘルプ表示関数 ---
usage() {
    echo "Usage: $(basename "$0") [-y] [-a] [-p] [-h]"
    echo "  -y : 確認プロンプトをすべてスキップし、自動で'yes'と応答します。"
    echo "  -a : Apacheのみを対象とします。"
    echo "  -p : PHP-FPMのみを対象とします。"
    echo "  -h : ヘルプを表示します。"
    echo "引数なしの場合は、ApacheとPHP-FPMの両方が対象となります。"
    exit 0
}

# --- 引数解析 ---
while getopts "yaph" opt; do
  case $opt in
    y)
      AUTO_YES=true
      ;;
    a)
      if ! $EXCLUSIVE_MODE; then
          # -a or -p が初めて指定された場合、排他モードにしてデフォルトをリセット
          RESTART_APACHE=false
          RESTART_PHP=false
          EXCLUSIVE_MODE=true
      fi
      RESTART_APACHE=true
      ;;
    p)
      if ! $EXCLUSIVE_MODE; then
          # -a or -p が初めて指定された場合、排他モードにしてデフォルトをリセット
          RESTART_APACHE=false
          RESTART_PHP=false
          EXCLUSIVE_MODE=true
      fi
      RESTART_PHP=true
      ;;
    h)
      usage
      ;;
    \?)
      # 不正なオプション
      usage
      ;;
  esac
done

# PHP-FPMのサービス名とコマンド名を変数から生成
PHP_FPM_SERVICE="php${PHP_VERSION}-fpm"
PHP_FPM_COMMAND="php-fpm${PHP_VERSION}"

#
# サービスを再起動し、ステータスを表示する関数
#
restart_and_check_service() {
    local service_name="$1"
    local service_label="$2" # 表示用の名前
    local confirm_action="n"

    if [ "$AUTO_YES" = true ]; then
        confirm_action="y"
        echo "${service_label} を再起動します... (-yオプションにより自動確認)"
    else
        read -p "${service_label} を再起動しますか? (y/n): " confirm_action
    fi

    if [[ "$confirm_action" =~ ^[Yy]$ ]]; then
        if [ "$AUTO_YES" = false ]; then
          echo "--- ${service_label} を再起動します... ---"
        fi
        if ! systemctl restart "$service_name"; then
            echo "エラー: ${service_label} の再起動に失敗しました。"
        else
            echo "${service_label} が正常に再起動されました。"
            echo
            echo "==== ${service_label} ステータス ===="
            systemctl status "$service_name" --no-pager
            echo "=================================="
        fi
    else
        echo "${service_label} の再起動はキャンセルされました。"
    fi
    echo
}

# スクリプトを root ユーザーで実行しているかチェック
if [ "$EUID" -ne 0 ]; then
    echo "エラー: このスクリプトは root 権限で実行する必要があります。"
    exit 1
fi

# 1. 有効なサイト設定とドメインを表示
# このセクションは引数に関わらず常に表示される情報として実行します
echo "==== 有効なサイト設定ファイル ===="
if [ -z "$(ls -A "$SITES_DIR" 2>/dev/null)" ]; then
    echo "サイト設定が存在しません。"
else
    shopt -s nullglob
    for site in "$SITES_DIR"/*; do
        echo "設定ファイル: $(basename "$site")"
        entries=$(grep -hi -E "^\s*(ServerName|ServerAlias)\s+" "$site" | sed -E 's/^[[:blank:]]+//;s/[[:blank:]]*#.*//' | awk '{
            original_directive = $1
            directive = tolower(original_directive)
            proper_directive = (directive == "servername") ? "ServerName" : \
                               (directive == "serveralias") ? "ServerAlias" : original_directive
            for (i=2; i<=NF; i++) {
                domain = tolower($i)
                sub(/[;,]*$/, "", domain)
                gsub(/^[[:blank:]]+|[[:blank:]]+$/, "", domain)
                if (domain) {
                    printf "%s %s\n", proper_directive, domain
                }
            }
        }' | sort -u)
        if [ -z "$entries" ]; then
            echo "  ※ ServerName/ServerAliasが定義されていません"
        else
            echo "$entries" | sed 's/^/  /'
        fi
        echo
    done
    shopt -u nullglob
fi
echo "=================================="
echo

# 2. 構文チェック
echo "--- 構文チェック ---"
SYNTAX_OK=true

# Apache構文チェック
if [ "$RESTART_APACHE" = true ]; then
    echo "Apache の構文をチェックしています..."
    if ! apachectl configtest 2>&1 | grep -q "Syntax OK"; then
        echo "エラー: Apacheの構文エラーが検出されました。"
        apachectl configtest # エラー詳細を再表示
        SYNTAX_OK=false
    else
        echo "Apacheの構文は正常です。"
    fi
    echo
fi

# PHP-FPMの存在確認と構文チェック
PHP_FPM_ENABLED=false
if [ "$RESTART_PHP" = true ]; then
    if systemctl list-units --type=service --all | grep -q "${PHP_FPM_SERVICE}.service"; then
        if command -v "$PHP_FPM_COMMAND" &>/dev/null; then
            PHP_FPM_ENABLED=true
            echo "${PHP_FPM_SERVICE} の構文をチェックしています..."
            if ! "$PHP_FPM_COMMAND" -t 2>&1 | grep -q "test is successful"; then
                echo "エラー: ${PHP_FPM_SERVICE} の構文エラーが検出されました。"
                "$PHP_FPM_COMMAND" -t # エラー詳細を再表示
                SYNTAX_OK=false
            else
                echo "${PHP_FPM_SERVICE} の構文は正常です。"
            fi
        else
            echo "警告: ${PHP_FPM_SERVICE} サービスは存在しますが、${PHP_FPM_COMMAND} コマンドが見つかりませ
ん。PHP-FPMのチェックはスキップします。"
            RESTART_PHP=false # 実行フラグをオフにする
        fi
    else
        echo "情報: ${PHP_FPM_SERVICE} サービスが見つかりません。PHP-FPM関連の処理はスキップします。"
        RESTART_PHP=false # 実行フラグをオフにする
    fi
    echo
fi

# 構文エラーがあれば処理を中断
if [ "$SYNTAX_OK" = false ]; then
    echo "構文エラーが検出されたため、処理を中断します。"
    exit 1
fi
echo "--------------------"
echo

# 3. サービスの再起動
if [ "$RESTART_APACHE" = false ] && [ "$RESTART_PHP" = false ]; then
    echo "再起動対象のサービスがありません。"
    exit 0
fi

# Apacheの再起動
if [ "$RESTART_APACHE" = true ]; then
    restart_and_check_service "apache2" "Apache"
fi

# PHP-FPMの再起動
if [ "$RESTART_PHP" = true ] && [ "$PHP_FPM_ENABLED" = true ]; then
    restart_and_check_service "$PHP_FPM_SERVICE" "$PHP_FPM_SERVICE"
fi

スクリプトの動き

  1. sudo bash apache2-check.shとすることで
  2. A@acheを起動するか、PHP-FPMを起動するかの2択が生まれます。
  3. -yオプションの場合は全てyにしてプロンプトを省略します。
  4. -aでApacheのみの再起動-pはPHP-FPMのみの再起動

と、柔軟かつやりやすいスタイルへと仕上がりました。

Mod_Securityチューニングのケーススタディ:「PCRE limits exceeded」エラーへの対処

概要

先日の記事でgitを用いたmod-securrity core rule setのアップデートを行いました。

このアップデートにより新たに検知されたルールの対処を行います。

環境

  • Ubuntu 24.04
  • Apache 2.4
  • Mod_Security 2系
  • OWASP Core Rule Set v4.1.5
  • ApacheのバーチャルファイルごとにModSecurityを制御

事象

OWASP Core Rule Set (CRS) を導入したModSecurityをブロックモード(SecRuleEngine On)に切り替え後、

Apacheのエラーログに、これまで見られなかった Execution error - PCRE limits exceeded (-47) というエラーが大量に記録されていることを発見しました。

  • エラー例(IPアドレスやホスト名は改変済み)
[Sat Jun 14 11:09:05.195039 2025] [security2:error] [pid 28306:tid 28306] [client AAA.BBB.CCC.DDD:59314] [client AAA.BBB.CCC.DDD] ModSecurity: Rule 73f4b9603e90 [id "951190"][file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf"][line "246"] - Execution error - PCRE limits exceeded (-47): (null). [hostname "hoge.example.com"] [uri "/path/to/page.html"] [unique_id "aE_NnHj4jZdbb2PH1-4O0QAAABc"]
[Sat Jun 14 11:09:05.195564 2025] [security2:error] [pid 28306:tid 28306] [client AAA.BBB.CCC.DDD:59314] [client AAA.BBB.CCC.DDD] ModSecurity: Rule 73f4b95f9e98 [id "951210"][file "/usr/share/modsecurity-crs/coreruleset/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf"][line "288"] - Execution error - PCRE limits exceeded (-47): (null). [hostname "hoge.example.com"] [uri "/path/to/page.html"] [unique_id "aE_NnHj4jZdbb2PH1-4O0QAAABc"]

このログから、SQLインジェクション系の情報漏洩を検知するRESPONSE-951-DATA-LEAKAGES-SQL.confの内容に沿ったものと特定。

cat /var/log/apache2/hoge/error.log |grep 951190 |wc -l

としたところ、数時間で2万件近いログを検知。

PCEリミットとは?

複雑な正規表現を、非常に大きなデータ(Webページなど)に対して実行すると、サーバーのCPUやメモリを大量に消費し、サービス拒否(DoS)攻撃に繋がる危険性があります。これを防ぐため、ModSecurityには処理の複雑さや再帰の深さに上限(リミット)が設けられています。

エラーの原因

今回のエラーは、サーバーが返信するHTMLページ(レスポンスボディ)の内容を、情報漏洩がないか確認するルールがスキャンしようとした際に、そのWebページの内容が非常に大きい、または複雑であったため、PCREの処理上限値を超えてしまったことが原因です。(筆者のサイトの長文が災いしています)

対処内容

この、処理上限値を超えてしまったことでエラーが発生したのですからmodsecurity.confSecPcreMatchLimitの値を大きくすると言うのが考えられる対処ではありますが、

運用しているサイトのvpsのリソースを鑑みて、より簡便な「原因となっている特定のルールのみの無効化」を行いました。

さっくりとした手順

  1. Apacheのバーチャルサイト設定(.confファイル)のバックアップを取ります。
  2. .confファイルの修正を行います。
  3. Apache再起動で修正を反映させます。
  4. ログを確認し、設定反映を確認します。

設定ファイルの修正

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

設定ファイルやバックアップディレクトリは自分の環境に合わせます。

  • バックアップ確認
diff -u /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) /etc/apache2/sites-available/hoge.conf

エラー無く、差分も表示されていなければバックアップは成功です。

  • ファイル修正

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

# PCREリミット超過エラーを起こす、レスポンスボディスキャン系のルール群を無効化
SecRuleRemoveById 951190
SecRuleRemoveById 951210
SecRuleRemoveById 951220
SecRuleRemoveById 951250
  • ファイル修正確認
diff -u /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) /etc/apache2/sites-available/hoge.conf
  • 差分例
+ # PCREリミット超過エラーを起こす、レスポンスボディスキャン系のルール群を無効化
+ SecRuleRemoveById 951190
+ SecRuleRemoveById 951210
+ SecRuleRemoveById 951220
+ SecRuleRemoveById 951250

設定ファイルの設定反映

  • 構文確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • 設定反映
sudo systemctl restart apache2.service
  • Apache稼働確認
systemctl status apache2.service

active(running)を確認します。

動作確認

対処後、ターミナルで

tail -f /var/log/apache2/hoge/error.log |grep "PCRE limits"

を実行し、しばらく観察します。

このエラーが表示されないことを確認し、処置が完了です。

gitによるmod-securrity core rule setのアップデート。

概要

こちらの方法で導入したMod-Security(WAF)。

この防御ルールであるOWASP Core Rule Setの更新を行います。

環境

  • Ubuntu 24.04
  • Apache 2.4
    • バーチャルサイトで個別設定
    • aptでインストールしているため
    • 設定ファイルは/etc/apache2/sites-available配下の.confファイル
    • サービス実行ユーザはwww-data
  • Mod Security ver.2系

注意点

アップデートにより

  • 追加されたルール
  • 削除されたルール
  • 変更された挙動

などがあるため、今までと同じ作業をしていたのにWAFがブロックした(あるいは今までブロックされていたアクセスがスルーした)などは可能性としてあります。

なので、

  1. ModSecurityを検知モードにして作業を実施する
  2. しばらく様子を見て有効化に戻す

という作業を取ります。そのため、WAFの防御が一度無効化される時間が発生します。

環境や予算が許すなら、インターネットに公開された別環境で試してからの方が良いでしょう。

→ テストが済んでいる状態であれば、本番環境で「ModSecurityをオフにする」の作業は不要になります。

さっくりとした手順

  1. 【事前準備】ModSecurityを検知モードにします。
  2. 【事前準備】ルールセットディレクトリのバックアップを行います。
  3. gitで最新リリースを確認します。
  4. OWASP Core Rule Setのアップデートを行います。
  5. Webサービスの再起動でアップデートを反映させます。
  6. 【事後作業】ModSecurityを有効化します。

【事前準備】ModSecurityを検知モードにします。

  • ディレクトリ移動
cd /etc/apache2/sites-available && pwd
  • .confファイルのバックアップ
sudo cp -pi hoge.conf /path/to/backup/directory/hoge.conf.yyyymmdd
  1. 設定ファイル(.conf)は自分の環境に合わせます。
  2. 任意のバックアップディレクトリを指定します。
  • .confファイルのバックアップ確認
diff -u /path/to/backup/directory/hoge.conf.yyyymmdd hoge.conf

差分が無いことを確認します。

通常、筆者は$(date +%Y%m%d)変数を用いてバックアップを行いますが、「検知モードでの試験」は日をまたぐこともあるため、この形式にしています。

  • .confファイルの編集

hoge.confファイルを管理者権限で修正。

SecRuleEngine On

から

SecRuleEngine DetectionOnly

に変更し、保存します。

  • ファイル修正確認
diff -u /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) hoge.conf

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

- SecRuleEngine On
+ SecRuleEngine DetectionOnly
  • 設定ファイル構文確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • Webサービス再起動前確認
systemctl status apache2.service

active(running)を確認します。

  • Webサービス再起動
sudo systemctl restart apache2.service

※設定ファイルの修正のみのためreloadで十分ですが、念を入れてrestartにしています。(以下同じ)

  • Webサービス再起動後確認
systemctl status apache2.service

active(running)を確認します。

【事前準備】ルールセットディレクトリのバックアップを行います。

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

これは、上記のリンク先で示した通り、ルールセットを/usr/share/modsecurity-crs/corerulesetに配置していた場合のディレクトリです。

必要に応じて自分の環境に合わせます。

  • ルールセットのディレクトリコピー
sudo cp -pir coreruleset /path/to/backup/directory/coreruleset.$(date +%Y%m%d)

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

  • ディレクトリコピー確認
ls -l /path/to/backup/directory/coreruleset.$(date +%Y%m%d)

ディレクトリ一式があることを確認します。

OWASP Core Rule Setのアップデート

  • ディレクトリ移動
cd /usr/share/modsecurity-crs/coreruleset && pwd
  • リモートリポジトリの確認
sudo git fetch origin
  • 最新バージョンとの差分確認
sudo git status

筆者環境で、以下のように表示されました。

ブランチ main

このブランチは 'origin/main' に比べて172コミット遅れています。fast-forwardすることができます。

  (use "git pull" to update your local branch)


Changes not staged for commit:

  (use "git add/rm <file>..." to update what will be committed)

  (use "git restore <file>..." to discard changes in working directory)

        deleted:    crs-setup.conf.example


no changes added to commit (use "git add" and/or "git commit -a") 

コミット内容が大幅に変わっているので、上述した「検知モードで様子見にする」が活きてきます。

  • gitによるアップデート
sudo git pull

これで、OWASP Core Rule Setが最新版に変更されました。

Webサービスの再起動でアップデートを反映させます。

  • Webサービス再起動前確認
systemctl status apache2.service

active(running)を確認します。

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

active(running)を確認します。

その後、既存のWebシステムに問題が無いかを確認します。

tail -f /var/log/hoge/hoge_error.log

等として、通常の作業を行い

  1. 既存のサービスに異常は無いか?
  2. 新たな偽陽性は発生していないか?

等を確認し、それに応じて新たな偽陽性の除外などを行っていきます。

【事後作業】ModSecurityを有効化します。

アップデートの影響で既存サービスに問題が無いことが判明したら、再びModSecurityを有効化します。この例では、バックアップから.confファイルを切り戻します。

  • バーチャルサイト(.conf)ファイルのディレクトリに移動
cd /etc/apache2/sites-available && pwd
  • バックアップから切り戻し
sudo cp -pi /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) hoge.conf
  • Webサービス再起動前確認
systemctl status apache2.service

active(running)を確認します。

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

active(running)を確認します。

その後、既存のWebシステムに問題が無いかを確認します。

有効化前に既存.confファイルを修正した場合

新たに偽陽性を追加した、既存のファイルを削除した、またはそれに伴う設定変更を行ったことにより、バックアップ前と.confファイルが異なるケースはあります。

その場合は、.confファイルの

SecRuleEngine DetectionOnly

SecRuleEngine On

と修正した後で以下の作業を行います。

  • 設定ファイル構文確認
sudo apache2ctl configtest

Syntax OKを確認します。

  • Webサービス再起動前確認
systemctl status apache2.service

active(running)を確認します。

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

active(running)を確認します。

アクセスログを圧迫するクローラーへの対処。(Apache 2.4と外部ファイル連携によるクローラーのアクセス遮断とログ軽減)

はじめに

Webサイトを外部に公開していると、非常に厄介なクローラーがアクセスログに蓄積。これらは

  • 超高速
  • 超高頻度

で行われるため、本当に必要なアクセスログ(どの検索サイトから来たか、攻撃の兆候となる不審な点はないか)がつかみにくくなっています。

これを解決するため、以下の措置を執りました。

  1. クローラーや悪質なボットのアクセス拒否
  2. それらをアクセスログにすら残さず、きちんとしたアクセスログを追いやすくする

(※こちらの記事を更に強力に推し進めたものです)

環境

  • Ubuntu 24.04
  • Apache 2.4
    • setenvifモジュール
    • authz_core モジュール
    • (特段の理由がない限り、上記2つは標準で組み込まれていることが多いです。)
    • ない場合はsudo a2enmod setenvif等とした後、sudo systemctl restart apache2.serviceとしてモジュールを組み込みます。
  • また、バーチャルサイトによる複数ドメインにも対応しています。

さっくりとした手順

  1. クローラーのリストファイルを作成します。
  2. Apacheバーチャルサイトの.confファイルを修正します。
  3. 設定を反映後、状況を確認します。

クローラーのリストファイルを作成

  • /etc/apache2/sites-enabled/il-vento-d-oro.txt

としてファイルを作成。(ファイル名や格納場所は自分の環境に合わせてください)

SetEnvIfNoCase User-Agent "facebookexternalhit/1\.1" bad_bot dontlog
SetEnvIfNoCase User-Agent "SemrushBot/7~bl" bad_bot dontlog
SetEnvIfNoCase User-Agent "AhrefsBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "MJ12bot" bad_bot dontlog
SetEnvIfNoCase User-Agent "DotBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "Baiduspider" bad_bot dontlog
SetEnvIfNoCase User-Agent "YandexBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "Sogou web spider" bad_bot dontlog
SetEnvIfNoCase User-Agent "Exabot" bad_bot dontlog
SetEnvIfNoCase User-Agent "MegaIndex\.ru" bad_bot dontlog
SetEnvIfNoCase User-Agent "SeznamBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "BLEXBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "Bytespider" bad_bot dontlog
SetEnvIfNoCase User-Agent "DataForSeoBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "serpstatbot" bad_bot dontlog
SetEnvIfNoCase User-Agent "SeekportBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "index\.community crawler" bad_bot dontlog
SetEnvIfNoCase User-Agent "PetalBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "BacklinksExtendedBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "meta-externalagent/1\.1" bad_bot dontlog
SetEnvIfNoCase User-Agent "ICC-Crawler" bad_bot dontlog
SetEnvIfNoCase User-Agent "bingbot" bad_bot dontlog
SetEnvIfNoCase User-Agent "msnbot" bad_bot dontlog
SetEnvIfNoCase User-Agent "Applebot" bad_bot dontlog
SetEnvIfNoCase User-Agent "DuckDuckBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "Majestic-SEO" bad_bot dontlog
SetEnvIfNoCase User-Agent "cognitiveSEO" bad_bot dontlog
SetEnvIfNoCase User-Agent "archive\.org_bot" bad_bot dontlog
SetEnvIfNoCase User-Agent "CCBot" bad_bot dontlog
SetEnvIfNoCase User-Agent "^$" bad_bot dontlog
SetEnvIfNoCase User-Agent "Custom-AsyncHttpClient" bad_bot dontlog
SetEnvIfNoCase User-Agent "ImagesiftBot" bad_bot dontlog

上記は、サイトを運営していく中で

  • robots.txtを無視し
  • あらゆるページにアクセスし
  • 下手をすれば帯域を阻害する

ものを集めています。"^$"は、エージェントを偽装するスクレイピングへの措置です。

この、

  • SetEnvIfNoCase User-Agent "エージェント名"
  • bad_bot
  • dontlog

と、続けて書くことで、厄介なボットへの対処とアクセスログへの無視を決めます。

もちろん、これらのbotが必要というのであれば省いていきます。

apacheの設定ファイルを編集

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

設定ファイルは自分の環境に合わせます。
任意のバックアップディレクトリを指定します。

  • diffによるバックアップ確認
diff -u /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) etc/apache2/sites-available/hoge.conf

差分(エラー)が無いことを確認します。

  • ファイルの修正

以下のように修正していきます。(要管理者権限)

例)

<VirtualHost *:443>
    # ServerName、ErrorLogは自分の環境に合わせます
    ServerName hoge.example.com
    ErrorLog /var/log/apache2/hoge/error.log
    # 1)Robots.txtをすり抜ける悪質なbotのリスト(先ほど作成したリストファイルのパスをフルパスで指定) 
    Include /etc/apache2/sites-enabled/il-vento-d-oro.txt
    # dontlogを付与したエージェントはアクセスログに記録させません
    # CustomLogは自分の環境に合わせます
    CustomLog /var/log/apache2/hoge/access.log combined env=!dontlog

# DocumentRootは自分の環境に合わせます。
DocumentRoot /var/www/html/hoge

# Directoryディレクティブは自分の環境に合わせます。
    <Directory /var/www/html/hoge>
        Options -MultiViews
        AllowOverride All
     <RequireAll>
      # 上記リストに従い、"bad_bot"変数がセットされているクローラーのアクセスを拒否します
      Require not env bad_bot
      # それ以外は許可
      Require all granted
     </RequireAll>
    </Directory>

# Require notでForbiddenにすると、クローラーは「拒否されただけで本体はある」と判断し、Webアクセスを繰り返すパターンがあるため、403に404を返す設定を付け加えます
# Directory
ErrorDocument 403 "Not Found"
  • 差分確認
diff -u /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) etc/apache2/sites-available/hoge.conf
  • 上記、加えた設定が+になっていること
  • ディレクティブの重複や閉じ忘れがないこと

を改めて確認します。

設定反映と確認

  • 別のセッションでアクセスログを確認
tail -f /var/log/apache2/hoge/access.log

※この段階ではクローラーからのアクセスが多々来ています

  • 構文確認
sudo apache2ctl configtest

Syntax Okを確認します

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

active(running)を確認します

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

active(running)を確認します

この段階で、上記、

tail -f /var/log/apache2/hoge/access.log

が止まっていれば設定は成功です。

  • 今までと同じようにアクセスができること
  • 既存のWebサイトに異常が無いこと
  • その場合のアクセスログが表示されること

を確認し、作業は成功です。

作業を切り戻す場合

何か不具合が起きた場合は、バックアップしていた.confを切り戻します。

  • 差分確認
sudo cp -pi /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) etc/apache2/sites-available/hoge.conf

作業時に指定したバックアップディレクトリです。

  • 差分確認
diff -u /path/to/backup/directory/hoge.conf.$(date +%Y%m%d) etc/apache2/sites-available/hoge.conf

差分が ない ことを確認します。

  • 構文確認
sudo apache2ctl configtest

Syntax Okを確認します

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

active(running)を確認します

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

active(running)を確認します

設定前になっていることを確認します。

Page 1 of 5

Powered by WordPress & Theme by Anders Norén