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の更新など)は、今回のようにパスやメソッドで丁寧に除外を作るのが、運用を長続きさせる秘訣です。
この、例外ルールを正しく使うことで、スクリプトキディやクローラーに対して、このような大見得を切ってやりましょう。
『任務は遂行する』 ………… 部下も守る おまえごときに両方やるというのは そうムズかしい事じゃあないな