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 | ルール名(概要) | 挙動の説明 |
|---|---|---|
| 911100 | Method is not allowed by policy | 許可されていないHTTPメソッド(GET/POST以外など)を検知します。 |
| 920420 | Request content type is not allowed by policy | Content-Typeヘッダーが許可リストにない場合に反応します。 |
| 949110 | Inbound Anomaly Score Exceeded | 重要: これは特定の攻撃を指すものではなく、他のルールの合計スコアが閾値を超えたため「ブロックした」という最終結果を示すIDです。 |
| 980130 | Inbound 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
なぜこれが必要なのか
- プロトコルの柔軟性: 標準のCRSは「GET/POST」以外のメソッド(PUT, PATCH, PROPFINDなど)を攻撃の兆候として厳しく制限します。しかし、モダンなWebアプリ(特にNextcloud)にはこれらが不可欠です。
- 偽陽性の温床「本文検査」: Redmineで「SQLの書き方」をチケットに書くと、WAFはそれを「SQLインジェクション攻撃」とみなしてブロックします。特定のパスに対してのみ、特定の検査(Tag)をオフにすることで、利便性を確保しています。
- 無駄なスキャンの排除: WordPressを使っていないサーバーへのWordPress用スキャンは、リソースの無駄です。これを早期に検知して「スコア加算+404応答」とすることで、後続の重い検査をスキップしつつ攻撃者をあしらいます。
- SecRuleRemoveByTag の威力: IDを1つずつ消すと、CRSがアップデートされた際に追加された「新しいSQLi検知ルール」に対応できません。今回のように OWASP_CRS/ATTACK-SQLI というタグ単位で除外することで、将来のアップデート後も「投稿機能だけは常に使える」状態を維持できます。
- 「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の更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。
この、例外ルールを正しく使うことで、スクリプトキディやクローラーに対して、このような大見得を切ってやりましょう。
『任務は遂行する』
…………
部下も守る
おまえごときに両方やるというのは
そうムズかしい事じゃあないな



