Skip to content
Published on

Ingress WAF - ModSecurity/Coraza と OWASP CRS の適用

Authors

はじめに

ある金曜の夕方、セキュリティチームから Slack メッセージが一通飛んできました。「外部スキャナーが当社のサービスドメインで SQL インジェクションの試行を一万件以上検知したのですが、ちゃんと遮断できていますか?」私はすぐにクラスタの Ingress 設定を開きました。そして気づいたのです。私たちは TLS 終端とルーティングは行っていましたが、アプリケーション層(L7)で入ってくるペイロードを検査する仕組みがまったくありませんでした。

当時の私たちのアーキテクチャはよくある形でした。クラウドロードバランサーの背後に ingress-nginx があり、その後ろに数十のマイクロサービスがぶら下がっていました。各サービスチームは自分のコードの入力検証には気を配っていましたが、全社的に一貫した攻撃遮断ポリシーはありませんでした。あるチームが守っても別のチームが破られれば、結局は同じクラスタ、同じ信頼境界の中で事故が起きます。

この記事では、Ingress 層に WAF(Web Application Firewall)を適用する話を扱います。レガシー標準であった ModSecurity とその後継エンジン Coraza の違い、ingress-nginx 統合設定、OWASP Core Rule Set(CRS)を適用して誤検知を整える実践プロセス、検知モードと遮断モードの運用戦略、ロギングとアラート、性能影響、そして専用 WAF との比較までを運用者の視点で解きほぐしていきます。

一つ先に触れておきたい点があります。2026年現在、Kubernetes の Ingress API は事実上凍結(frozen)されており、もはや新機能は追加されず、後継標準は Gateway API です。また ingress-nginx はメンテナンスモードに入り、その中に内蔵されていた ModSecurity サポートは実質的に廃止(deprecated)の流れをたどっています。したがって新規構築であれば Coraza ベースの経路を、そして中長期的には Gateway API への移行も併せて検討すべきです。この点は本文で繰り返し触れていきます。

なぜ Ingress 層に WAF が必要か

WAF は OSI 第7層、すなわち HTTP リクエストの内容そのものを検査するファイアウォールです。一般的なネットワークファイアウォールやセキュリティグループは IP とポート(L3/L4)を見ますが、WAF はリクエストボディ、ヘッダー、クエリパラメータ、Cookie といったアプリケーションデータを覗き込み、攻撃パターンを見つけ出します。

Kubernetes 環境において Ingress は、外部トラフィックがクラスタに入ってくる単一の関門です。まさにこの地点に WAF を配置すると、次のような利点があります。

  • 単一地点での適用: 数十のバックエンドサービスがそれぞれセキュリティロジックを実装しなくても、関門で一貫したポリシーを強制できます。
  • 仮想パッチ(virtual patching): アプリケーションコードをすぐに直せない状況で、既知の脆弱性(例: Log4Shell、特定の CVE)を狙うペイロードを WAF ルールで一時的に遮断できます。
  • 多層防御(defense in depth): アプリケーションの入力検証が完璧である保証はありません。WAF はその上にもう一枚かぶせる安全網です。
  • 可視性: どんな攻撃がどれだけ入ってくるかをログに残せるため、セキュリティ監視とインシデント対応の基盤になります。

ただし WAF は万能ではありません。ビジネスロジックの脆弱性(例: 権限バイパス、IDOR)はパターンマッチで捉えにくく、暗号化されたトラフィックは TLS 終端後にしか検査できず、誤ったチューニングは正常なトラフィックを遮断して障害を引き起こすこともあります。この限界は記事の後半で改めて扱います。

以下は WAF がどこに位置するかを示すトラフィックフローです。

            インターネット
              |
              v
   +----------------------+
   |  Cloud LoadBalancer  |  (L3/L4: IP, ポート)
   +----------------------+
              |
              v
   +----------------------+
   |   ingress-nginx Pod  |
   |  +----------------+  |
   |  |  WAF エンジン   |  |  (L7: HTTP ペイロード検査)
   |  | ModSec/Coraza  |  |  <-- OWASP CRS ルール評価
   |  +----------------+  |
   +----------------------+
              |
       (検査通過時)
              v
   +----------------------+
   |   Backend Service    |
   |   (マイクロサービス) |
   +----------------------+

ModSecurity と Coraza: 何がどう違うのか

WAF の「エンジン」と「ルール」は分けて理解する必要があります。エンジンはルールを評価する実行器であり、ルール(例: OWASP CRS)は何を遮断するかを定義するポリシーです。同じ CRS ルールを ModSecurity でも Coraza でも動かせます。

ModSecurity(レガシー)

ModSecurity は2002年から始まったオープンソースの WAF エンジンで、長らく事実上の標準でした。最初は Apache モジュールとして出発し、その後 Nginx と IIS をサポートする libmodsecurity(v3)へと発展しました。ingress-nginx にはこの ModSecurity がコンパイルされて組み込まれており、アノテーション一行で有効化できました。

問題は、ModSecurity の中核スポンサーであった Trustwave が2024年7月1日付で ModSecurity プロジェクトのサポートを終了すると発表したことから始まりました。その後プロジェクトは OWASP に移管されましたが、ingress-nginx の立場からは、C で書かれた重い依存関係を抱え続けるのは負担になっていました。実際 ingress-nginx のメンテナーは ModSecurity 統合を deprecated と表示し、今後の削除方向を明示しました。

Coraza(後継)

Coraza は OWASP が主管する Go で書かれた WAF エンジンです。核心的な特徴は、ModSecurity のルール文法(SecRule)とほぼ互換であることです。つまり、これまで使っていた SecLang ルールと OWASP CRS をほぼそのまま持ってきて使えます。Go で書かれているためメモリ安全性が高く、Envoy や Caddy、そして coraza-proxy-wasm の形でさまざまなプロキシに埋め込めるよう設計されています。

2026年現在、クラウドネイティブ環境で新規に L7 WAF を導入するなら、Coraza 系統が事実上の標準方向です。特に Envoy ベースのデータプレーン(Gateway API 実装の多くが Envoy を使用)では、coraza-proxy-wasm フィルターとして WAF を差し込むパターンが定着しています。

比較表

項目ModSecurity (libmodsecurity v3)Coraza
記述言語C / C++Go
リリース時期2002年(v3 は 2017年)2021年
ルール文法SecLang (SecRule)SecLang 互換
OWASP CRSサポートサポート
ingress-nginx 統合内蔵(廃止予定)外部/WASM または Gateway API 経路
Envoy 埋め込み限定的proxy-wasm でネイティブ対応
メンテナンス状態OWASP 移管、レガシー活発
メモリ安全性手動管理GC ベース

要点だけ覚えれば十分です。ルール(CRS)は同じく再利用され、エンジンだけが ModSecurity から Coraza に乗り換わるという図です。

ingress-nginx に ModSecurity を適用する(既存環境)

すでに ingress-nginx と内蔵 ModSecurity を使っている既存環境をまず扱います。新規構築であれば次のセクションの Coraza 経路をご覧ください。

ingress-nginx において ModSecurity は、ConfigMap のグローバル設定と Ingress アノテーションの二層で制御されます。まずコントローラのグローバル ConfigMap です。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  enable-modsecurity: "true"
  enable-owasp-modsecurity-crs: "true"
  # グローバル ModSecurity 設定スニペット
  modsecurity-snippet: |
    SecRuleEngine DetectionOnly
    SecRequestBodyAccess On
    SecAuditEngine RelevantOnly
    SecAuditLogParts ABIJDEFHZ
    SecAuditLog /var/log/modsec/audit.log
    SecAuditLogType Serial

上記の設定で enable-modsecurity はエンジン自体を有効化し、enable-owasp-modsecurity-crs はコントローライメージに内蔵された OWASP CRS をロードします。SecRuleEngine DetectionOnly は最初に導入するとき非常に重要です。実際には遮断せず検知だけしながらログを蓄積するモードです。

次は特定の Ingress にのみ ModSecurity を適用したり、詳細設定を上書きするアノテーションの例です。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress
  namespace: shop
  annotations:
    nginx.ingress.kubernetes.io/enable-modsecurity: "true"
    nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On
      SecRequestBodyLimit 13107200
      SecRule REQUEST_HEADERS:User-Agent "@contains badscanner" "id:1001,phase:1,deny,status:403,log,msg:'Blocked scanner'"
spec:
  ingressClassName: nginx
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: shop-svc
                port:
                  number: 80

ここでアノテーション層の SecRuleEngine On は、その Ingress に限って遮断モードを有効化する効果があります。つまりグローバルは DetectionOnly のままにして、検証が済んだサービスだけを一つずつ遮断モードに昇格させる段階的なロールアウトが可能です。

ingress-nginx に Coraza を適用する(推奨経路)

内蔵 ModSecurity が廃止の流れである以上、Coraza を外部から差し込む方式が現代的な代替案です。代表的に二つのパターンがあります。

一つ目は、ingress-nginx の server-snippet または http-snippet を通じて OpenResty ベースの Coraza Lua 連携(coraza-nginx)を呼び出す方式。二つ目は、データプレーン自体を Envoy ベース(例: Gateway API 実装)に変えて coraza-proxy-wasm フィルターを付ける方式です。

以下は Envoy ベースのデータプレーンで coraza-proxy-wasm フィルターを構成する概念的な例です。EnvoyFilter やゲートウェイ実装ごとの拡張メカニズムを通じて適用します。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: coraza-waf
  namespace: istio-system
spec:
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.wasm
          typed_config:
            "@type": type.googleapis.com/udpa.type.v1.TypedStruct
            type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
            value:
              config:
                name: coraza-filter
                vm_config:
                  runtime: envoy.wasm.runtime.v8
                  code:
                    local:
                      filename: /etc/envoy/coraza-proxy-wasm.wasm
                configuration:
                  "@type": type.googleapis.com/google.protobuf.StringValue
                  value: |
                    {
                      "directives_map": {
                        "default": [
                          "SecRuleEngine On",
                          "Include @owasp_crs/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"
                        ]
                      },
                      "rules": ["default"]
                    }

Coraza のディレクティブ文法が ModSecurity と同一である点に注目してください。SecRuleEngine OnInclude 指示子もすべてそのままです。移行時にルール自体はほとんど手を加えなくて済みます。

Coraza を独立デーモンとして置き、nginx の auth_request やサイドカーで検査を委譲するパターンもありますが、運用の複雑さと遅延(latency)を考えると、データプレーンに直接埋め込む WASM 方式の方がすっきりしています。

OWASP Core Rule Set の適用とチューニング

エンジンを有効化したら、次はどのルールで検査するかが核心です。OWASP CRS は最も広く使われる汎用ルールセットで、SQL インジェクション、XSS、RCE、LFI/RFI、プロトコル違反など OWASP Top 10 に対応するルールをカテゴリ別にまとめて提供します。

異常スコアモード(Anomaly Scoring)

CRS の動作方式を理解することがチューニングの出発点です。CRS は基本的に「異常スコア(anomaly scoring)」モデルで動作します。各ルールがマッチするたびにリクエストにスコアを加え、その累積スコアが閾値(threshold)を超えると遮断します。ルール一つが引っかかったらすぐ遮断するのではなく、疑わしいシグナルが累積して初めて遮断される構造です。

ルールには深刻度に応じたスコアが付与されます。

深刻度デフォルトスコア
CRITICAL5
ERROR4
WARNING3
NOTICE2

デフォルトのインバウンド閾値は通常5です。つまり CRITICAL ルール一つが引っかかるだけで閾値に到達します。この閾値とスコア付与方式は CRS の設定ファイルで調整します。

# crs-setup.conf 抜粋(概念例)
SecAction "id:900110,phase:1,pass,nolog,\
  setvar:tx.inbound_anomaly_score_threshold=5,\
  setvar:tx.outbound_anomaly_score_threshold=4"

# パラノイアレベル(Paranoia Level)設定
SecAction "id:900000,phase:1,pass,nolog,\
  setvar:tx.paranoia_level=1"

パラノイアレベル(Paranoia Level)

CRS には PL1 から PL4 までの「パラノイアレベル」があります。レベルが高いほどより攻撃的で厳格なルールが有効化され、検知率は上がりますが、その分だけ誤検知(false positive)も急増します。

レベル性格推奨用途
PL1基本、誤検知ほぼなしほとんどの本番の開始点
PL2やや厳格セキュリティ重視サービス、チューニング後
PL3非常に厳格金融など高セキュリティ、十分なチューニング必須
PL4極めて厳格特殊環境、誤検知が非常に多い

実践的な推奨は明確です。**PL1 から始めて DetectionOnly で十分にログを蓄積し、誤検知を整理したうえで遮断モードに上げ、必要なら PL をゆっくり高めます。**最初から PL3 で遮断モードを有効にすると、十中八九正常なサービスが遮断されて障害になります。

誤検知(False Positive)の管理

CRS を導入すると必ず誤検知に出会います。例えば JSON ボディに SQL に似た文字列が入ったり、リッチテキストエディタが HTML タグをそのまま送信すると XSS ルールに引っかかります。正常なトラフィックを遮断しないためには例外ルールを書く必要があります。

誤検知を扱う標準的な方法は、特定のルール ID を特定のパスやパラメータに対してのみ無効化することです。

# 特定のルールを特定のパスでのみ無効化
SecRule REQUEST_URI "@beginsWith /api/articles" \
  "id:10001,phase:1,pass,nolog,\
   ctl:ruleRemoveById=942100"

# 特定のパラメータに対してのみ特定のルールを無効化
SecRule REQUEST_URI "@beginsWith /admin/editor" \
  "id:10002,phase:1,pass,nolog,\
   ctl:ruleRemoveTargetById=941100;ARGS:content"

ここで ruleRemoveById はルール全体を無効化し、ruleRemoveTargetById は特定の入力フィールド(ここでは content パラメータ)に対してのみルール検査を除外します。可能なら全体を無効化するより対象だけを絞って除外する方が安全です。むやみにルール ID をすべて無効化すると WAF が実質的に無力化されます。

誤検知を見つける流れは次のとおりです。

  [1] DetectionOnly モードで運用
        |
        v
  [2] audit ログから遮断されたであろうリクエストを収集
        |
        v
  [3] 正常なトラフィックなのにマッチしたルール ID を特定
        |
        v
  [4] パス/パラメータ単位の例外ルールを作成
        |
        v
  [5] 誤検知が十分に減ったら遮断(On)モードに昇格

検知モード vs 遮断モード

WAF 運用で最も重要な意思決定の一つが「今遮断するか、見るだけにするか」です。

モードSecRuleEngine 値動作用途
無効Offルール評価しない緊急無効化
検知DetectionOnlyマッチのログのみ、通過させる導入初期、チューニング段階
遮断On閾値超過時に遮断十分に検証された本番

原則は常に DetectionOnly で始めることです。新規サービスにいきなり遮断モードを有効にすると、そのサービスの正常なトラフィックパターンを知らない状態なので、誤検知による障害リスクが大きくなります。最低でも数日から数週間検知モードで運用しながらログを分析し、誤検知の例外を整理したうえで、サービス単位で一つずつ遮断モードに上げるのが安全です。

緊急時に WAF が正常なトラフィックを遮断していると判断したら、すぐに DetectionOnly に戻せるようロールバック手順をあらかじめ文書化しておくのがよいでしょう。アノテーション一行の変更で即座に適用されるため、迅速な緩和が可能です。

ロギングとアラート

WAF の価値は半分が可視性から生まれます。何を遮断し何を見たかの記録がなければ、遮断ポリシーを整えることも、インシデントを追跡することもできません。

ModSecurity/Coraza は audit ログを残します。上で見た SecAuditEngine RelevantOnly は、異常スコアが閾値を超えたか、明示的にロギング対象になったリクエストだけを記録するという意味です。すべてのリクエストを記録すると(On)ログ量が爆発するため、運用では RelevantOnly が一般的です。

audit ログは JSON 形式で残すと後続の収集と分析が楽になります。

SecAuditLogFormat JSON
SecAuditLogType Serial
SecAuditLog /var/log/modsec/audit.log

このように残したログは、サイドカーやノード単位のログコレクタ(Fluent Bit、Vector など)で吸い上げ、中央ログシステム(Loki、Elasticsearch、OpenSearch)へ送ります。そこでマッチしたルール ID、クライアント IP、攻撃カテゴリ別にダッシュボードを作り、特定の閾値(例: 分あたり遮断件数の急増)にアラートを掛けるのが一般的な構成です。

アラート設計で注意すべき点は、インターネットに公開されたサービスは常時スキャナートラフィックを受けるため、「遮断1件ごとにアラート」はすぐにノイズになることです。傾向(平常比での急増)や特定の発信元からの集中攻撃、遮断モード移行直後の誤検知急増のような意味のあるシグナルにアラートを掛けるべきです。

性能影響

WAF はタダではありません。すべてのリクエストのボディとヘッダーを正規表現ベースのルール数百個で評価するため、CPU と遅延にコストが発生します。

  • CPU: CRS の全ルールを PL1 で動かすと、ingress コントローラの CPU 使用量が目に見えて増加します。ルール評価は CPU バウンドな作業です。
  • 遅延(latency): リクエストごとに追加される遅延は通常数ミリ秒程度ですが、大きなリクエストボディを検査するときやパラノイアレベルが高いときはより大きくなることがあります。
  • リクエストボディのバッファリング: ボディ検査のために WAF がリクエストボディをバッファリングするため、大容量アップロードのパスでは SecRequestBodyLimit とボディ検査ポリシーを慎重に設定する必要があります。

実務的な推奨事項は次のとおりです。第一に、WAF を有効化したら必ず負荷テストで遅延とスループットの変化を測定します。第二に、ingress コントローラのリソース要求/制限とレプリカ数を再算定します。第三に、大容量ファイルアップロードのようにボディ検査が不要または危険なパスは別処理(例: そのパスだけボディ検査を除外)を検討します。

# 大容量アップロードパスのボディ検査制限例(アノテーションスニペット)
nginx.ingress.kubernetes.io/modsecurity-snippet: |
  SecRule REQUEST_URI "@beginsWith /upload" \
    "id:20001,phase:1,pass,nolog,ctl:requestBodyAccess=Off"

専用 WAF との比較

Ingress 内蔵 WAF がすべての状況の正解ではありません。クラウドマネージド WAF(AWS WAF、Cloud Armor、Azure Front Door WAF など)やアプライアンス/CDN 型 WAF(Cloudflare、Akamai など)と比較して長所短所を見極める必要があります。

項目Ingress 内蔵(ModSec/Coraza)クラウドマネージド WAFCDN/アプライアンス型
位置クラスタ内部クラウドエッジ/LB 前段グローバルエッジ
コストモデルコンピュート資源のみリクエスト/ルール単位課金トラフィック/プラン課金
運用主体自前運用マネージドマネージド
DDoS 防御なし(別途必要)一部含む強力
ルールカスタマイズ非常に柔軟(SecLang)コンソール/限定的プロバイダーごとに異なる
遅延の位置バックエンド付近エッジエッジ
クラスタ内部トラフィック保護可能外部トラフィックのみ外部トラフィックのみ

大きな絵はこうです。大規模 DDoS とグローバルエッジ防御が重要なら CDN/クラウド WAF に強みがあり、細かいルール制御やクラスタ内部の east-west トラフィックまで検査したい、あるいはベンダーロックインを避けたいなら Ingress 内蔵(Coraza)が有利です。実務では両者を重ねて使う多層構成がよくあります。エッジで大量攻撃やボットをふるい落とし、Ingress で精緻なアプリケーションルールを適用するという形です。

限界と注意点

WAF を導入するとき必ず認識すべき限界です。

  • ビジネスロジック攻撃は防げない: 権限バイパス、IDOR、価格操作のようなロジックの脆弱性は正常な形式のリクエストなので、パターンマッチでは捉えられません。WAF は認証/認可の設計を代替しません。
  • 回避テクニックが存在する: エンコーディング、チャンク分割、パラメータ汚染などでシグネチャを回避しようとする試みが絶えずあります。CRS は正規化(normalization)変換で対応しますが完璧ではありません。
  • TLS 終端への依存: 暗号化されたトラフィックは復号後にしか検査できません。終端位置の設計が重要です。
  • 誤検知による障害: 誤ったチューニングはそれ自体がサービス障害です。遮断モードへの移行は慎重であるべきです。
  • 性能負担: 上で扱ったとおり CPU と遅延のコストがあります。
  • メンテナンス: ルール(CRS)もエンジンも継続的に更新する必要があります。放置された WAF は新しい攻撃を防げません。

そして繰り返し強調すべき運用コンテキストがあります。2026年現在、ingress-nginx はメンテナンスモードで、内蔵 ModSecurity は廃止の方向です。新しく構築するなら Coraza ベースの経路と Gateway API への移行を併せて設計することで長期的に安全になります。Gateway API 実装の多くが Envoy をデータプレーンとして使うため、coraza-proxy-wasm を付ける図と自然にかみ合います。

導入チェックリスト

実務で Ingress WAF を導入するときに点検する項目をまとめます。

  • エンジン選択: 新規は Coraza、既存 ModSecurity は移行ロードマップの策定
  • DetectionOnly モードでまずデプロイしたか
  • OWASP CRS は PL1 から始めたか
  • audit ログを中央ログシステムへ収集しているか
  • 誤検知を特定する分析フローがあるか(パス/パラメータ単位の例外)
  • 遮断モード昇格はサービス単位の段階的ロールアウトか
  • 緊急ロールバック手順(即座に DetectionOnly 復帰)が文書化されているか
  • 負荷テストで性能影響を測定したか
  • ingress コントローラのリソース/レプリカを再算定したか
  • 大容量アップロードなどボディ検査の例外パスを定義したか
  • アラートは単件ではなく意味のある傾向に掛けているか
  • CRS とエンジンの更新サイクルを定めたか
  • 専用 WAF(エッジ)との役割分担を定義したか
  • Gateway API 移行時の WAF 適用方式を検討したか

おわりに

最初に Slack メッセージを受け取ったあの金曜の夕方に戻ってみると、実は私たちに必要だったのは一つの魔法のような遮断装置ではなく手順でした。Ingress 層に WAF を置くことは確かに強力な防御線ですが、それを有効にした瞬間に終わる仕事ではなく、そこから始まる運用です。

要点を整理するとこうです。エンジンは ModSecurity から Coraza へ移る流れであり、ルールは OWASP CRS を PL1 から慎重に適用し、運用は DetectionOnly で始めて誤検知を整理したうえで段階的に遮断へ昇格させることです。そして WAF は多層防御の一枚にすぎず、アプリケーションセキュリティと認証/認可の設計を代替しません。

最後に、ingress-nginx がメンテナンスモードに入り Gateway API が標準になっていく2026年の流れの中で、WAF 戦略もその移行と併せて設計することをお勧めします。エンジンとデータプレーンは変わっても、「検知から始めて慎重に遮断へ」という運用原則は変わりません。

参考資料