Ubuntu

Ubuntu 24.04 ApacheでModSecurity

Ubuntu

変なアクセスがあったのでGeminiさんと相談していたら、WAF(Web Application Firewall)を紹介された。
要求に応答してからFail2banで止めるのではなく、要求を受けた段階で止めるべきものもあるよ、と。

敷居が高そうだけれど、トライしてみることにした。



ここから広告
広告
広告ここまで


概要

オープンソースのWAFでModSecurityを導入したら?という提案。

Apacheのモジュールとして追加することで、不正なアクセスに403で応答するようになるそうだ。
OWASP Foundationがメンテナンスしてくれており、OWASP CRS(Core Rule Set)を導入することで機能する。

導入

Apacheのモジュール mod-security2 をインストール。

$ sudo apt install libapache2-mod-security2

インストールしただけで、モジュールが有効化され、OWASP CRSも導入されていた。

まずは試運転。この設定をコピーすると、「検知はするがエラーを戻さない」というモードで起動する。

$ sudo cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
$ sudo systemctl reload apache2

テスト。

$ curl https://hogeserver.hogeddns.jp/?test=/etc/passwd

これで、以下のようなログが出力される。

/var/log/apache2/modsec_audit.log

--f7cdcc2c-A--
[13/Aug/2026:14:05:18.697992 +0900] sssssssssssssssssssssssssss 192.168.110.10 60422 192.168.110.40 443
--f7cdcc2c-B--
GET /?test=/etc/passwd HTTP/2.0
User-Agent: curl/8.5.0
Accept: */*
Host: hogeserver.hogeddns.jp
<省略>

インターネット側にサービスを公開していれば、それなりに攻撃されていると思われる。
うちも攻撃を受けているので、ログはどんどん書き込まれていく。

OWASP CRSの最新化

modsecurityは、OWASP CRS(Open Worldwide Application Security Project - core rule set)に沿って問題のあるアクセスを検知している。

mod-securityをインストールしたときに、同時にmodsecurity-crsがインストールされている。
ただし、Ubuntuの24.04がリリースされた段階でバージョンが固定されている。

そこで、最新のライブラリを使えないものか、と考えた。

CRSはここでメンテナンスされているとのこと。
https://github.com/coreruleset/coreruleset/

切り替え

インストール済みのCRSを削除する。

$ sudo apt purge modsecurity-crs

本家のリポジトリを/etc/modsecurity/crs-gitにダウンロードしてくる。

$ sudo git clone https://github.com/coreruleset/coreruleset.git /etc/modsecurity/crs-git

mainブランチは開発用として利用されているので、最新リリースのタグに合わせる。
この日はv4.28.0だった。

$ sudo git -C /etc/modsecurity/crs-git/ checkout v4.28.0

crs-setup.confをサンプルからコピーして作る。

$ sudo cp /etc/modsecurity/crs-git/crs-setup.conf.example /etc/modsecurity/crs-git/crs-setup.conf

起動時にCRSが読み込まれるように、ファイルを読み込むための設定ファイルを作る。

/etc/modsecurity/owasp-crs.conf ※新規作成

Include /etc/modsecurity/crs-git/crs-setup.conf
IncludeOptional /etc/modsecurity/crs-git/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
Include /etc/modsecurity/crs-git/rules/*.conf
IncludeOptional /etc/modsecurity/crs-git/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

confファイルが2つあるが、アルファベット順に読み込まれるとのこと。

設定をチェックして、Apacheに反映させる。

$ sudo apachectl configtest
$ sudo systemctl reload apache2

アップデート

以下で最新ファイルを持ってきてタグを切り替える。

$ sudo git -C /etc/modsecurity/crs-git/ pull
$ sudo git -C /etc/modsecurity/crs-git/ checkout v4.28.0

crs-setup.confを何も変更していないときは、そのままコピー。
※何らかの編集していた場合は、編集内容を確かめて反映させる。

$ sudo cp /etc/modsecurity/crs-git/crs-setup.conf.example /etc/modsecurity/crs-git/crs-setup.conf

チェックして、問題なければ反映。

$ sudo apachectl configtest
$ sudo systemctl reload apache2

システムの理解

modsecurity + OWASP CRS の場合、誤検知があるからチューンナップする必要がある、というのがGeminiさんの説明だった。
だけど、それは誤検知ではなく、正当に検知してくれているのであって、たまたまウチで利用しているシステムが、危ないっぽい通信を使っているというだけのこと。

ウチで使っているシステムが止まらないように、システムの考え方に沿って調整をしていくことになる。
そのために、システムの構造を理解しておく。

ファイル構成

通常、動かしたら様子を見て調整…という話になると思うのだけれど、調整する前に、どう調整すべきなのかの整理。

CRSをgitから持ってくるようにしたこともあって、ファイル構造が少し変わっている。

/etc/apache2/mods-available/
├── security2.conf -> IncludeOptional /etc/modsecurity/*.conf が書かれている
├── security2.load


/etc/modsecurity/
├── modsecurity.conf -> WAFエンジン本体の動作設定
├── owasp-crs.conf -> ルールセットの読み込み(先程作成したもの、読み込み順序を制御)
├── crs-git
│   ├── crs-setup.conf -> 全体の変数・ポリシー定義
│   ├── REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf -> メイン判定前のユーザールール ※まだ作っていない
│   ├── rules -> 攻撃検知ルール本体
│  │   ├── REQUEST-901-INITIALIZATION.conf -> コアルールの初期化と準備(変更不可)
│   │   ├── REQUEST-905-COMMON-EXCEPTIONS.conf -> コアルール(変更不可)
│   │   ︙
│   └── RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf -> メイン判定後のユーザールール ※まだ作っていない
└── unicode.mapping

/var/log/apache2/
├── modsec_audit.log -> 評価結果のログ
├── error.log -> アラート・速報がエラーログに出力される

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confは、ルール本体の前に評価される。
本体の評価をスキップさせるような処理に向いている。

RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.confは、ルール評価やレスポンス検証の後に評価される。
出力側の誤検知除外や、最終的なアノマリースコアの補正など、事後処理に必要な命令を記述する。

modsecurity.conf

WAFエンジン本体の動作設定。

マニュアルはこちら。Ubuntu 24.04の場合はVersion 2を見る。
Github / owasp-modsecurity / ModSecurity / Wiki

項目設定値備考
SecRuleEngineDetectionOnly / On / Off最初は検知だけ、問題ないところまで調整できたらOn。
SecResponseBodyAccessOn / Offレスポンスの情報漏洩を調べたい場合はOn。
そのような要件がなければOff。
負荷が高いのと、WordPressの管理画面にすら入れない程度には厳しいルール。
SecAuditLogPartsABDEFHIJZ別表にて説明。ABFHIJZあたりか。

他にもたくさんの設定値があるけれど、最初はこの辺りを見ておけば良いのではないかと。

ログパーツの方は、以下の説明だった。

パーツ説明 ※Geminiによる翻訳
A監査ログのヘッダー(必須)
Bリクエストヘッダー
Cリクエストボディ
リクエストボディが存在し、かつ ModSecurity がそれを捕捉するよう設定されている場合のみ出力。
SecRequestBodyAccess を On に設定する必要がある。
D中間レスポンスヘッダーのための予約領域、未実装
E中間レスポンスボディ
レスポンスボディを捕捉するよう設定されており、かつ監査ログエンジンがそれを記録するよう設定されている場合のみ出力。
SecResponseBodyAccess を有効にする必要がある。
F最終レスポンスヘッダー
コンテンツ配信の最終段階で Apache によって常に追加される Date および Server ヘッダーを除く。
Gレスポンスボディのための予約領域、未実装
H監査ログのトレーラー
(個人メモ)これがないと、マッチしたルールの詳細やエラーメッセージが分からない
IパートCの代用
multipart/form-data 形式が使われている場合を除き、すべてのケースでパート C と同じデータを記録する。
multipart/form-data の場合は、ファイルデータそのものを除外し、パラメータに関する情報のみを含む擬似的な application/x-www-form-urlencoded ボディを記録する。
これは、(サイズが大きくなりがちな)ファイルデータを監査ログに保存したくない場合に有用。
Jmultipart/form-data 形式でアップロードされたファイルに関する情報
Kマッチしたすべてのルールの完全なリストが、マッチした順に(1行に1ルールずつ)出力される。
ルールは完全修飾形式で出力されるため、引き継がれたアクションやデフォルトの演算子も表示される。
Z境界線
ログエントリの終了を示す(必須)

crs-setup.conf

動作モードの切り替えや、ログの制御ができる。

また、900000番代のルールが多数コメント化された状態で書かれている。
必要に応じてコメントを解除すれば、動作を変更することができる。

動作モードのデフォルトは Anomaly Scoring mode で、以下の設定。

SecDefaultAction "phase:1,log,auditlog,pass"
SecDefaultAction "phase:2,log,auditlog,pass"
SecDefaultAction "phase:3,log,auditlog,pass"
SecDefaultAction "phase:4,log,auditlog,pass"
SecDefaultAction "phase:5,log,auditlog,pass"

ログを出さない場合のサンプル。

# SecDefaultAction "phase:1,nolog,auditlog,pass"
# SecDefaultAction "phase:2,nolog,auditlog,pass"
# SecDefaultAction "phase:3,nolog,auditlog,pass"
# SecDefaultAction "phase:4,nolog,auditlog,pass"
# SecDefaultAction "phase:5,nolog,auditlog,pass"

Self-contained mode については、以下のようなサンプルが書かれていた。

# SecDefaultAction "phase:1,log,auditlog,deny,status:403"
# SecDefaultAction "phase:2,log,auditlog,deny,status:403"

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.exampleにかかれている情報によると、

  • 環境固有のローカルな例外を設定するのに、REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf、
    RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.confを使う。
  • 例えば、固定的にルールを無効化する SecRuleRemoveById 等を使う場合、そのルールが事前に読み込まれている必要がある。
    この場合、RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.confで最後に読み込ませる。
  • ルールの評価中に状態を変更する ruleRemoveById 等を使う場合、対象のルールで評価する前に実行される必要がある。
    この場合、REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confで最初に読み込ませる。

ということのようだ。

何かをしたくなったとき、どちらにどう書くか、これを頭において考えれば良さそうだ。

ログ(modsec_audit.log)

詳細解析用のログで、ログはパーツ単位で出力される。
パーツの詳細は前述のmodsecurity.conf参照。

閾値に達した例

これは、Geminiさんによれば、

  • X-Forwarded-For: 2a06:98c0:3600::103(Cloudflare Workers)をプロキシーとして悪用したアクセス。
    Reverse Proxy / Forward Proxy を JavaScript数行で記述できるらしい。
  • /.git/HEADを要求。
  • /.git/HEADを取得できた場合、git-dumper等のツールでリポジトリ全体をダウンロードし、各種情報を取り出そうとしている。

という攻撃だそう。

--d483674d-A--
[14/Aug/2026:02:48:08.266998 +0900] an4DWJY7hWD15KVF_hLy1AAAEgE 104.23.221.168 11848 192.168.110.40 443
--d483674d-B--
GET /.git/HEAD HTTP/2.0
X-Forwarded-For: 2a06:98c0:3600::103
Cf-Ray: a2a98c76f93ec124-ARN
Cf-Ew-Via: 15
Cdn-Loop: cloudflare; loops=1
Upgrade-Insecure-Requests: 1
Sec-Fetch-User: ?1
Accept-Language: en-US,en;q=0.9
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0
Pragma: no-cache
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Cf-Worker: jx1akqrgxx5exg.workers.dev
Cf-Visitor: {"scheme":"https"}
X-Forwarded-Proto: https
Accept-Encoding: gzip
Host: hogeserver.hogeddns.jp

--d483674d-F--
HTTP/1.1 404 Not Found
X-Powered-By: PHP/8.0.29
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private
Link: <https://hogeserver.hogeddns.jp/wp-json/>; rel="https://api.w.org/"
Referrer-Policy: no-referrer-when-downgrade
Connection: close
Content-Type: text/html; charset=UTF-8
Date: Thu, 13 Aug 2026 17:48:08 GMT
Server: Apache/2.4.58 (Ubuntu)

--d483674d-H--
Message: Warning. Matched phrase ".git/" at REQUEST_FILENAME. [file "/etc/modsecurity/crs-git/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .git/ found within REQUEST_FILENAME: /.git/HEAD"] [severity "CRITICAL"] [ver "OWASP_CRS/4.29.0-dev"] [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"]
Message: Warning. Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/etc/modsecurity/crs-git/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.29.0-dev"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"]
Message: Warning. Unconditional match in SecAction. [file "/etc/modsecurity/crs-git/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [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.29.0-dev"] [tag "reporting"] [tag "OWASP_CRS"]
Apache-Error: [file "apache2_util.c"] [line 275] [level 3] [client 104.23.221.168] ModSecurity: Warning. Matched phrase ".git/" at REQUEST_FILENAME. [file "/etc/modsecurity/crs-git/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .git/ found within REQUEST_FILENAME: /.git/HEAD"] [severity "CRITICAL"] [ver "OWASP_CRS/4.29.0-dev"] [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 "hogeserver.hogeddns.jp"] [uri "/.git/HEAD"] [unique_id "an4DWJY7hWD15KVF_hLy1AAAEgE"]
Apache-Error: [file "apache2_util.c"] [line 275] [level 3] [client 104.23.221.168] ModSecurity: Warning. Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/etc/modsecurity/crs-git/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.29.0-dev"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "hogeserver.hogeddns.jp"] [uri "/.git/HEAD"] [unique_id "an4DWJY7hWD15KVF_hLy1AAAEgE"]
Apache-Error: [file "apache2_util.c"] [line 275] [level 3] [client 104.23.221.168] ModSecurity: Warning. Unconditional match in SecAction. [file "/etc/modsecurity/crs-git/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [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.29.0-dev"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "hogeserver.hogeddns.jp"] [uri "/index.php"] [unique_id "an4DWJY7hWD15KVF_hLy1AAAEgE"]
Apache-Handler: proxy:fcgi://127.0.0.1:9000
Stopwatch: 1786643288166215 100875 (- - -)
Stopwatch2: 1786643288166215 100875; combined=3304, p1=1846, p2=1210, p3=0, p4=0, p5=247, sr=0, sw=1, l=0, gc=0
Producer: ModSecurity for Apache/2.9.7 (http://www.modsecurity.org/); OWASP_CRS/4.28.0.
Server: Apache/2.4.58 (Ubuntu)
Engine-Mode: "DETECTION_ONLY"

--d483674d-Z--

パーツAで表示されている an4DWJY7hWD15KVF_hLy1AAAEgE は、このセッションで使われているユニークID。
Apacheが割り当てているとのこと。

パーツHでどう判定したのかがわかるとのことで、Geminiさんに確認してみた。

  • 1行目 Message、930130のルールでクリティカル(5点)と判定。
    2行目 Message、949110のルールは、直前までの判定で加算されたスコアを取り出して(5点)、超過と判定。
    3行目 Message、InboundとOutboundをあわせた判定結果詳細。
  • 4行目~6行目 Apache-Error、この判定結果をApache標準のエラーログに出力したことを示す。
  • 7行目 Apache-Handler、Apacheがリクエストを処理する際に委任したハンドラー。
  • 8行目 Stopwatch、トランザクション処理時間(マイクロ秒、旧仕様)
    9行目 Stopwatch2、トランザクション処理時間(マイクロ秒、WAF評価全体で3ミリ秒(combined)、それぞれのフェーズの処理時間)、レスポンス全体に101ミリ秒かかっているけれど、そのうちWAFで3ミリ秒を使ったことを表す。
  • 10行目 Producer、エンジンバージョン2.9.7、OWASP CRSバージョン 4.28.0
    11行目 Server、modsecurity2を実行しているサーバー。
  • 12行目 Engine-Mode、ここでは検知だけしていることを表している。いずれ本番運用する。

実際のアクセスログはこれ。

アクセスログ

104.23.221.168 2a06:98c0:3600::103 - - [14/Aug/2026:02:48:08 +0900] "GET /.git/config HTTP/2.0" 404 45078 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0" "-"

modsecurityはエラーログにも情報を出力してくれている。

エラーログ

[Fri Aug 14 02:48:08.167702 2026] [security2:error] [pid 67448:tid 129306002974400] [remote 104.23.221.168:11848] [client 104.23.221.168] ModSecurity: Warning. Matched phrase ".git/" at REQUEST_FILENAME. [file "/etc/modsecurity/crs-git/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "150"] [id "930130"] [msg "Restricted File Access Attempt"] [data "Matched Data: .git/ found within REQUEST_FILENAME: /.git/HEAD"] [severity "CRITICAL"] [ver "OWASP_CRS/4.29.0-dev"] [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 "hogeserver.hogeddns.jp"] [uri "/.git/HEAD"] [unique_id "an4DWJY7hWD15KVF_hLy1AAAEgE"]
[Fri Aug 14 02:48:08.170086 2026] [security2:error] [pid 67448:tid 129306002974400] [remote 104.23.221.168:11848] [client 104.23.221.168] ModSecurity: Warning. Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/etc/modsecurity/crs-git/rules/REQUEST-949-BLOCKING-EVALUATION.conf"] [line "233"] [id "949110"] [msg "Inbound Anomaly Score Exceeded (Total Score: 5)"] [ver "OWASP_CRS/4.29.0-dev"] [tag "anomaly-evaluation"] [tag "OWASP_CRS"] [hostname "hogeserver.hogeddns.jp"] [uri "/.git/HEAD"] [unique_id "an4DWJY7hWD15KVF_hLy1AAAEgE"]
[Fri Aug 14 02:48:08.266933 2026] [security2:error] [pid 67448:tid 129305510090432] [client 104.23.221.168:11848] [client 104.23.221.168] ModSecurity: Warning. Unconditional match in SecAction. [file "/etc/modsecurity/crs-git/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [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.29.0-dev"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "hogeserver.hogeddns.jp"] [uri "/index.php"] [unique_id "an4DWJY7hWD15KVF_hLy1AAAEgE"]

閾値に満たない例

これはセキュリティ調査のアクセスのようだ。
これはフィルターに引っかかったわけではなく、403を返したからログに出した、ということだった。

--85dc954f-A--
[13/Aug/2026:14:13:24.000682 +0900] sssssssssssssssssssssssssss 45.205.1.240 41540 192.168.110.40 443
--85dc954f-B--
GET /favicon.ico HTTP/1.1
user-agent: Mozilla/5.0 (compatible; FlowIQLabsBot/1.0; +https://flowiq-labs.com/scanning-info)
accept: */*
connection: close
host: nnn.nnn.nnn.nnn

--85dc954f-F--
HTTP/1.1 403 Forbidden
Content-Length: 338
Connection: close
Content-Type: text/html; charset=iso-8859-1

--85dc954f-E--
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.Reason: Cannot perform Post-Handshake Authentication.<br /></p>
<hr>
<address>Apache/2.4.58 (Ubuntu) Server at nnn.nnn.nnn.nnn Port 443</address>
</body></html>

--85dc954f-H--
Apache-Error: [file "ssl_engine_kernel.c"] [line 1013] [level 3] AH10158: cannot perform post-handshake authentication
Apache-Handler: proxy-server
Stopwatch: 1786598003999460 1314 (- - -)
Stopwatch2: 1786598003999460 1314; combined=859, p1=510, p2=0, p3=51, p4=150, p5=147, sr=78, sw=1, l=0, gc=0
Response-Body-Transformed: Dechunked
Producer: ModSecurity for Apache/2.9.7 (http://www.modsecurity.org/); OWASP_CRS/3.3.5.
Server: Apache/2.4.58 (Ubuntu)
Engine-Mode: "DETECTION_ONLY"

--85dc954f-Z--

実際のアクセスログとエラーログはこれ。

アクセスログ

45.205.1.240 - - [14/Aug/2026:02:22:57 +0900] "GET / HTTP/1.1" 403 2289 "-" "Mozilla/5.0 (compatible; FlowIQLabsBot/1.0; +https://flowiq-labs.com/scanning-info)"

エラーログ

[Fri Aug 14 02:22:57.370613 2026] [ssl:error] [pid 67448:tid 129305510090432] [client 45.205.1.240:34510] AH10158: cannot perform post-handshake authentication
[Fri Aug 14 02:22:57.370666 2026] [ssl:error] [pid 67448:tid 129305510090432] SSL Library Error: error:0A000117:SSL routines::extension not received
[Fri Aug 14 02:22:57.371201 2026] [security2:error] [pid 67448:tid 129305510090432] [client 45.205.1.240:34510] [client 45.205.1.240] ModSecurity: Warning. Unconditional match in SecAction. [file "/etc/modsecurity/crs-git/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [id "980170"] [msg "Anomaly Scores: (Inbound Scores: blocking=3, detection=3, per_pl=3-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=0, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=3)"] [ver "OWASP_CRS/4.29.0-dev"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "nnn.nnn.nnn.nnn"] [uri "/"] [unique_id "an39cZY7hWD15KVF_hLywwAAABI"]

実際、このサイトは証明書がないと入れない形にしているので、エラーログが出力されている。
そこに合わせてmodsecurityがログを出してくれているので、ユニークIDで検索すると当該箇所がすぐに見つかる、という仕掛け。

ログ(Apache)

ルールの中でlogアクションが定義されている場合、エラーログにも情報が出力される。
VirtualHostでエラーログ出力先を分けたりしているのだが、その場合は分けた先に出力される。

運用する上では、これが調査の起点になる。
Fail2banと連携する場合にも、このログが監視対象。

具体的に出力されるのは、modsec_audit.logのHセクションでMessageとして表示されているもののうち、ルールにlogアクションが定義されているもの。
これだけでも何が起きているのか大体分かる。

リクエストの全貌を見ないと何が起きているのか分からない、といった場合にmodsec_audit.logを見に行く運用になる。

調整

構造が大体わかったところで、必要な通信が遮断と判断されていないか確認する。

SecResponseBodyAccess

modsecurity.confで設定できるこの項目、OnのままだとWordPressの サイトネットワーク管理-ダッシュボード に入れなかった。
それ以外の箇所でもはじかれる。

原因はBodyのチェックにより、must be a validという文字を検知したため。

閾値は4がデフォルト値であり、割とすぐに閾値オーバーとなり403が返される。

情報漏洩が許されない環境ではOn。
それ程の強度は必要なく、外部からの不正アクセスを止めたいという程度であればOffで良いと思われる。

遮断と判断した通信の調査

Inbound遮断(ID 949110)、Outbound遮断(ID 959100)が発動したログを取り出す。

$ grep -E '\[id "(949110|959100)"\]' /var/log/apache2/error.log

取り出した中から、例えばこのログ。

[Fri Aug 14 16:43:23.292388 2026] [security2:error] [pid 154953:tid 129305518483136] [client nnn.nnn.nnn.nnn.64:42987] [client nnn.nnn.nnn.nnn] ModSecurity: Warning. Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/etc/modsecurity/crs-git/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 "hogeserver.hogeddns.jp"] [uri "/remote.php/dav/files/NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN/"] [unique_id "an7HG-dNAoC9zIfVO3qQ_wAAAA4"]

NextcloudのAndroidアプリからのアクセスだった。

nnn.nnn.nnn.nnn - - [14/Aug/2026:16:43:23 +0900] "PROPFIND /remote.php/dav/files/NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN/ HTTP/1.1" 207 1493 "-" "Mozilla/5.0 (Android) Nextcloud-android/34.1.0"

ユニークIDが an7HG-dNAoC9zIfVO3qQ_wAAAA4 なので、これでもう一度Grepして、そのセッションのログを取り出す。

[Fri Aug 14 16:43:23.283613 2026] [security2:error] [pid 154953:tid 129305518483136] [client nnn.nnn.nnn.nnn:42987] [client nnn.nnn.nnn.nnn] ModSecurity: Warning. Match of "within %{tx.allowed_methods}" against "REQUEST_METHOD" required. [file "/etc/modsecurity/crs-git/rules/REQUEST-911-METHOD-ENFORCEMENT.conf"] [line "44"]
[id "911100"] [msg "Method is not allowed by policy"] [data "PROPFIND"] [severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-generic"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/METHOD-ENFORCEMENT"] [tag "capec/1000/210/272/220/274"] [hostname "hogeserver.hogeddns.jp"] [uri "/remote.php/dav/files/NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN/"] [unique_id "an7HG-dNAoC9zIfVO3qQ_wAAAA4"]
[Fri Aug 14 16:43:23.292388 2026] [security2:error] [pid 154953:tid 129305518483136] [client nnn.nnn.nnn.nnn:42987] [client nnn.nnn.nnn.nnn] ModSecurity: Warning. Operator GE matched 5 at TX:blocking_inbound_anomaly_score. [file "/etc/modsecurity/crs-git/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 "hogeserver.hogeddns.jp"] [uri "/remote.php/dav/files/NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN/"] [unique_id "an7HG-dNAoC9zIfVO3qQ_wAAAA4"]
[Fri Aug 14 16:43:23.357575 2026] [security2:error] [pid 154953:tid 129305518483136] [client nnn.nnn.nnn.nnn:42987] [client nnn.nnn.nnn.nnn] ModSecurity: Warning. Unconditional match in SecAction. [file "/etc/modsecurity/crs-git/rules/RESPONSE-980-CORRELATION.conf"] [line "99"] [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=0, RCE=0, PHPI=0, HTTP=0, SESS=0, COMBINED_SCORE=5)"] [ver "OWASP_CRS/4.28.0"] [tag "reporting"] [tag "OWASP_CRS"] [hostname "hogeserver.hogeddns.jp"] [uri "/remote.php/dav/files/NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN/"] [unique_id "an7HG-dNAoC9zIfVO3qQ_wAAAA4"]

※改行して先頭にIDを持ってきたが、実際には1行で出力されている。

ルール(ID 911100)に抵触したことがわかった。

多くの場合は、これで事足りるということなのだけれど、詳しく色々見たい場合には、
/var/log/apache2/modsec_audit.log
を開き、ユニークIDで検索すればOK。

遮断と判断した通信の調査 複数

1つだけなら手作業でもよいが、実際はたくさん出ているので、上記作業を自動で行うスクリプトをGeminiさんに作ってもらった。

modsec-analyze

#!/bin/bash

# 引数チェック: ファイルが指定されていない場合は使い方を表示して終了
if [ $# -eq 0 ]; then
echo "エラー: ログファイルを指定してください。"
echo "使用例: $0 /var/log/apache2/*error*.log"
exit 1
fi

# スクリプトに渡されたすべての引数(ログファイルパス群)を配列に格納
LOG_FILES=("$@")

# 1. 閾値超過 (949110 or 959100) が発生した unique_id を抽出
# ※ -E オプションを追加し、sort -u で重複を除外
UNIQUE_IDS=$(zgrep -E '\[id "(949110|959100)"\]' "${LOG_FILES[@]}" 2>/dev/null | zgrep -oP '\[unique_id "\K[^"]+' | sort -u)

# 該当する unique_id が見つからなかった場合の判定
if [ -z "$UNIQUE_IDS" ]; then
echo "遮断イベント (949110/959100) は検出されませんでした。"
exit 0
fi

# 2. その unique_id を持つログから、実際に点数を加算させた個別ルールとメッセージを表示
for id in $UNIQUE_IDS; do
echo "=== Unique ID: $id ==="
zgrep "$id" "${LOG_FILES[@]}" 2>/dev/null | zgrep -E -v '\[id "(949110|959100|980170)"\]'
done

結果をゆっくり見ていけば、ルールに抵触した要求がどんなものだったかがわかる。

プラグインの利用

Nextcloudでバンバンエラーが出ていたのでGeminiさんに相談したところ、Nextcloud用のプラグインがあると教えてくれた。
Github / coreruleset / nextcloud-rule-exclusions-plugin

ダウンロード

早速ダウンロードしてくる。

$ sudo git clone \
https://github.com/coreruleset/nextcloud-rule-exclusions-plugin.git \
/etc/modsecurity/crs-plugin/nextcloud

プラグインの読み込み

プラグインを決められた順序どおりに読み込むようにする。
WordPress用のプラグインも追加するつもりなので、ワイルドカードを使っている。

/etc/modsecurity/owasp-crs.conf

Include /etc/modsecurity/crs-git/crs-setup.conf

IncludeOptional /etc/modsecurity/crs-plugin/*/plugins/*-config.conf
IncludeOptional /etc/modsecurity/crs-plugin/*/plugins/*-before.conf


IncludeOptional /etc/modsecurity/crs-git/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
Include /etc/modsecurity/crs-git/rules/*.conf
IncludeOptional /etc/modsecurity/crs-git/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

IncludeOptional /etc/modsecurity/crs-plugin/*/plugins/*-after.conf

プラグインの設定

設定ファイルを開けてみたところ、ルールを2つ有効化する必要があった。

  • iOSのユーザーがいるので、iOSの場合の特別判定。
  • Collaboraが発する通信を許容。

/etc/modsecurity/crs-plugin/nextcloud/plugins/nextcloud-rule-exclusions-config.conf

SecAction \
"id:9508011,\
phase:1,\
pass,\
nolog,\
ver:'nextcloud-rule-exclusions-plugin/1.6.0',\
setvar:'tx.nextcloud-rule-exclusions-plugin_ios_workaround_enabled=1'"

SecRule REMOTE_ADDR "@ipMatch 172.30.0.0/16,fdbb:bbbb:bbbb::/64" \
"id:9508012,\
phase:1,\
pass,\
nolog,\
ver:'nextcloud-rule-exclusions-plugin/1.6.0',\
setvar:'tx.nextcloud-rule-exclusions-plugin_server_crawler_rules_enabled=1'"

Geminiさんによれば、

  • IPアドレス指定は、カンマ区切りで、複数登録できる。
  • Nextcloud発の通信を許容するので、コンテナーのIPアドレスをサブネットで指定。

とのことであった。

有効化設定

プラグインは読み込まれた段階で有効になっていた。

ただ、ウチではいくつかのサービスを提供しているので、そのサービスだけで有効化させたいと考えた。
OWASP / CRS Documentation / Plugins

コメントに書かれていた無効化の指定を参考に、赤文字部分に手を入れた。

/etc/modsecurity/crs-plugin/nextcloud/plugins/nextcloud-rule-exclusions-config.conf

SecRule &TX:nextcloud-rule-exclusions-plugin_enabled "@eq 0" \
"id:9508010,\
phase:1,\
pass,\
nolog,\
ver:'nextcloud-rule-exclusions-plugin/1.6.0',\
chain"
SecRule WebAppID "!@streq nextcloud" \
"t:none,\
setvar:'tx.nextcloud-rule-exclusions-plugin_enabled=0'"

最初の行は、値ではなくnextcloud-rule-exclusions-plugin_enabledが定義されているかどうかを確認している。
chainとしたことで、次の条件がANDで評価される。

2つ目の評価で、WebAppIDがnextcloudではない、が評価されて、ルールを無効化する。

ということで、VirtualHostにWebAppIDを追加する。

/etc/apache2/sites-available/<service>.conf

<VirtualHost *:443>
ServerName hogeserver.hogeddns.jp
...
SecWebAppId "nextcloud"
</VirtualHost>

ここまで設定したら、Apacheをreloadすればプラグインが働き出す。

ルールの作成

プラグインがない場合、あるいは、プラグインがあっても必要に応じて除外ルールを作る場合がある。
当サイトの場合、スラッグにlocalhostという単語を含んでいたりするので、正当なアクセスを攻撃と見なす場合がある。

例えば、以下のようなログが出ていた。見やすいようにテキトーに改行を入れている。

--06f04534-H--
Message: Warning. Matched phrase "localhost/" at REQUEST_FILENAME. [file "/etc/modsecurity/crs-git/rules/REQUEST-934-APPLICATION-ATTACK-GENERIC.conf"] [line "138"]
[id "934190"] [msg "Possible Server Side Request Forgery (SSRF) Attack: Scheme-less localhost or internal hostname detected"]
[data "Matched Data: localhost/ found within REQUEST_FILENAME: /ubuntu18-04-the-hostname-of-syslog-is-set-to-localhost/"]
[severity "CRITICAL"] [ver "OWASP_CRS/4.28.0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-ssrf"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-GENERIC"] [tag "capec/1000/225/664"]

抵触したルールはこれ。

SecRule REQUEST_COOKIES|REQUEST_COOKIES_NAMES|REQUEST_FILENAME|ARGS_NAMES|ARGS|XML:/*|XML://@* "@pmFromFile ssrf-no-scheme.data" \
"id:934190,\
phase:2,\
...

リクエストの色々な情報を見ているルールなので、URLだけをチェックの対象から除外してみた。

SecRule REQUEST_HEADERS:Host "@streq rohhie.net" \
"id:100003,\
phase:1,\
pass,\
nolog,\
ctl:ruleRemoveTargetById=934190;REQUEST_FILENAME"

最初の行で、gitea.rohhie.netはまた別にルールを作るとして、rohhie.netに限定。
ruleRemoveTargetByIdで、ルールの中でもREQUEST_FILENAMEだけを除外。

ものによってはTargetが使えず、ruleRemoveByIdでルール自体を除外する場合もある。

といったようなやり方で、正当なアクセスを除外しないように調整をしていく。

本番稼働

動作モードの変更

プラグインのおかげで設定作業は手軽、かつ、安全に行うことができた。

とはいえ、攻撃をそれなりに受けているので、正当なアクセスを見分けてルールを追加するのは大変。
Geminiさんに相談しながらなんとか大丈夫そうなところまでやってきた。

いよいよ本当に遮断する。

/etc/modsecurity/modsecurity.conf

SecRuleEngine On

Apacheに反映させる。

$ sudo apachectl configtest
$ sudo systemctl reload apache2

Fail2banとの連携

Fail2banと連携させることで、「悪意のあるアクセス」を見分けてBanすることができるようになる。

/etc/fail2ban/jail.local

[apache-modsecurity]
enabled = true

※findtime, maxretry, bantime はお好みで。

通常の接続では、[client <HOST>:<PORT>] [client <HOST>]がログに出力される。
X-Forwarded-For等が付いている場合は、[remote <HOST>:<PORT>] [client <HOST>]がログに出力される。

[Sat Aug 22 12:48:46.180144 2026] [security2:error] [pid 34563:tid 133621732062912]
[remote 192.168.110.40:40106]
[client 192.168.110.30] ModSecurity:...続く

※適当に改行しているが、これが1行で出る。

デフォルトの設定ではこのログを見られなかったので、以下を追加してみた。

/etc/fail2ban/filter.d/apache-modsecurity.local

[Definition]
failregex = %(known/failregex)s
\[client <HOST>\]\s+ModSecurity:\s+(?:\[(?:\w+ \"[^\"]*\"|[^\]]*)\]\s*)*Access denied with code [45]\d\d

もともとの定義を包含しているような気もするけれど、少なくとも見落としはなくなった。

事前の調整を慎重に実施しておかないと、正常なアクセスをBanする場合があるから注意が必要。
でも、上手く調整できた後なら、相当高度な防御の仕組みになると思われる。

起きたこと

色々と思い出しながら書いてみる。

  • Limit Login Attemptsを卒業することにした。
    • WordPressのログイン失敗は200が返り、成功すれば302(リダイレクト)が返る。
    • WordPressがどんな応答をするのかがポイントなので、ルールはphase:3で定義。
      ログイン画面でユーザー情報とパスワードを入力した後、200で応答しようとしたら、これを403に変更して妨害。
    • Fail2banでerror.logを検知してBan。
    • なお、WordPressのサブサイトでログインする場合にルールが発動しなかった。
      そこで、ApacheのVirtualHostに以下を追加設定した。
      RewriteRule ^/([_0-9a-zA-Z-]+/)?(wp-login.php)$ /wp-login.php [PT,L]
  • WordPressのスラッグにlocalhostが書かれていたために403となった。
    • 検知したルールを調べ、リクエストのファイル名のみをチェック対象から外した。
    • 存在しないファイルを繰り返し要求されたらBan。
  • ルールを除外しすぎた。
    • ルールを除外する(ruleRemoveById)のではなく、ルールの中の一部だけを除外する(ruleRemoveTargetById)方向で。
    • ただ、できるものとできないものがあるので、元のルールをよく調べる。
  • Giteaへの正当なアクセスをBanした。
    • Giteaで公開しているファイルへのアクセスそのものが危険と判断される。
    • クローン操作で403が発生した。
    • 今もすべてがちゃんと動いているのかどうか…分からない。
  • ちょっと過大にルールを緩めて運用を開始してしまうと、最適化が難しくなる。
    • 家族や親戚だけしか使っていないとはいえ、「工事しますよ」宣言や計画的なテストと修正が必要になる。

さいごに

GeminiさんにWAFを勧められて導入したけれど、これはとても難しいテーマだった。

自分としては、

  • まずは使ってみたい。
  • 動き出したら、全体の構造を知りたい。
  • いくつか試してみて、細かいところが気になったら、そこを深掘りしつつ横にも広げていく。

と思うので、Geminiさんに聞きながら記事を整理していった。

だけど、これを一人でやろうとすると時間がかかる、物凄く時間がかかる。
Geminiさんがいてくれて良かった。

今回WAFを導入してみて、想像以上に調査(という名の攻撃)が行われていることを実感した。
次から次へと「調査」されているので、サンプルがどんどん集まる、見切れないほどに。
なるほど、Geminiさんが統計という言葉をチラホラ出す訳だ。

ただ、ここまできて気付いたことが。

一番多いのは、IPアドレス直打ちでの「調査」だった。
敵の常套手段がこれなら、IPアドレスでのアクセスを遮断して、サービスへのログイン試行を制限すれば、WAFがなくても「調査」の8割方を防げるのでは?

高級感が出たので、ヨシッ!

ここから広告
広告
広告ここまで

コメントはこちらから お気軽にどうぞ ~ 投稿に関するご意見・感想・他