AWS WAFマネージドルールを全有効化したら本番が死んだ話と、1年半で辿り着いた設定戦略

「AWSが用意してるなら安全でしょ」と全部Blockにしたら決済APIが全滅した経験、ありませんか?1年半の試行錯誤で学んだCount/Block移行の現実的な進め方をまとめました。

WAFマネージドルールを全部有効化して本番が死んだ日

正直に言うと、最初にAWS WAF v2を導入したとき、やらかした。

SOC2審査の準備を進めていたチームから「WAFをちゃんとやれ」と言われて、マネージドルールグループを見たら「AWSManaged」が並んでるじゃないですか。「AWSが用意してるなら安全でしょ」って全部Blockモードで有効化したんですよ。その3時間後に決済APIが全滅しました。

アラートが飛んできて確認したら、AWSManagedRulesSQLiRuleSetがJSON bodyのネストしたSQLクエリを含むリクエストを全部ブロックしてた。うちのAPIはレポートクエリのfilter条件をJSONで受け取る設計だったので、正規のリクエストが全滅するのは当然だったんですが、その時は気づかなかった。

そこから1年半、試行錯誤しながら運用してきた知見を書く。以前WAF・DDoS対策の記事でも触れたけど、今回はマネージドルールの設定戦略にフォーカスした話。

2026年時点のWAF v2マネージドルールの全体像

2026年8月現在、AWS WAF v2のマネージドルールグループは大きく以下のカテゴリに整理されている。AWSが提供するものとAWS MarketplaceのISVが提供するものがあるが、うちのチームはまずAWS提供のものだけで固めた。

graph TB
    subgraph WAF_v2["AWS WAF v2 - マネージドルールグループ"]
        subgraph Core["Coreルールグループ"]
            CRS[AWSManagedRulesCommonRuleSet<br/>一般的な脆弱性 700 WCU]
            ADMIN[AWSManagedRulesAdminProtectionRuleSet<br/>管理パスへの不正アクセス 100 WCU]
            KBI[AWSManagedRulesKnownBadInputsRuleSet<br/>既知の悪意あるリクエスト 200 WCU]
        end
        subgraph UseCase["ユースケース別"]
            SQLI[AWSManagedRulesSQLiRuleSet<br/>SQLインジェクション 200 WCU]
            LINUX[AWSManagedRulesLinuxRuleSet<br/>Linux固有の脆弱性 200 WCU]
            PHP[AWSManagedRulesPHPRuleSet<br/>PHP固有の脆弱性 100 WCU]
            WINDOWS[AWSManagedRulesWindowsRuleSet<br/>Windows固有 200 WCU]
            POSIX[AWSManagedRulesPOSIXRuleSet<br/>POSIXシェル 100 WCU]
            WORDPRESS[AWSManagedRulesWordPressRuleSet<br/>WordPress固有 100 WCU]
        end
        subgraph Reputation["評判・Bot"]
            IPR[AWSManagedRulesAmazonIpReputationList<br/>Amazonの脅威インテリジェンス 25 WCU]
            ANON[AWSManagedRulesAnonymousIpList<br/>Tor/VPN/プロキシ 50 WCU]
            BOT[AWSManagedRulesBotControlRuleSet<br/>Bot検出 50-100 WCU]
            ATP[AWSManagedRulesATPRuleSet<br/>アカウント乗っ取り防止 50 WCU]
            ACFP[AWSManagedRulesACFPRuleSet<br/>アカウント作成詐欺防止 50 WCU]
        end
    end

WCU(WAF Capacity Unit)の合計が1500を超えるとWebACL自体が作れないので、全部入れようとするとすでに1525 WCU超える。これも気づかなかったポイントのひとつ。

各ルールグループのWCU消費量

xychart-beta
    title "WAFルールグループのWCU消費量"
    x-axis ["CommonRuleSet", "KBI", "SQLi", "Linux", "BotControl", "AdminProtect", "AnonymousIP", "IP Reputation"]
    y-axis "WCU" 0 --> 800
    bar [700, 200, 200, 200, 150, 100, 50, 25]

Bot ControlのWCUはTARGETEDモードにすると150 WCU程度になる。全部入れると合計1625 WCUで上限オーバーになるため、うちはWordPressRuleSet(使ってない)とPHPRuleSet(使ってない)は外してある。

2026年の変更点

2025年後半から2026年にかけて特に変化が大きかったのはBot Controlで、Targeted Inspectionという機能が強化された。シグネチャベースの検出から機械学習ベースの検出に切り替えるオプションで、WCUは増えるが誤検知が大幅に減る。あとATP(Account Takeover Protection)がJavaScript統合なしでも一定の検出ができるようになったのも地味に大きい変化だった。

本番で安全にマネージドルールを導入するための段階的アプローチ

失敗から学んで今うちのチームでやっている導入フローはこう。

flowchart TB
    A[マネージドルール選定] --> B[Count モードで全ルール有効化]
    B --> C[1〜2週間ログ分析]
    C --> D{誤検知の確認}
    D -->|誤検知多い| E[ルールアクションの上書き設定]
    D -->|誤検知なし| F[Block モードに切り替え]
    E --> G[除外ルールの設定]
    G --> H[再度ログ分析 1週間]
    H --> F
    F --> I[CloudWatch Metrics監視設定]
    I --> J[定期的なチューニング]

    style A fill:#FF9900,color:#fff
    style F fill:#28a745,color:#fff
    style E fill:#dc3545,color:#fff

最重要なのは必ずCountモードから始めること。これはWAFマネージドルール運用の先行記事でも触れているが、本当に大事すぎるので繰り返す。「AWSのマネージドルールだから大丈夫」という信頼は、うちの決済APIを3時間止めた。

Terraformでの設定例(2026年版)

resource "aws_wafv2_web_acl" "main" {
  name  = "production-web-acl"
  scope = "REGIONAL"

  default_action {
    allow {}
  }

  # Step 1: まずIPレピュテーションリストはいきなりBlock
  # 正規ユーザーがTorやVPNを使うケースは業務システムでは少ない
  rule {
    name     = "AWSManagedRulesAmazonIpReputationList"
    priority = 1

    override_action {
      none {}  # マネージドルールのデフォルトアクションを使用
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesAmazonIpReputationList"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "IpReputationList"
      sampled_requests_enabled   = true
    }
  }

  # Step 2: CommonRuleSetはCountモードで開始
  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 10

    override_action {
      count {}  # 最初はCountモード!
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"

        # 特定ルールのアクションを個別にCountに上書き
        # Body検査はAPIゲートウェイとの相性確認後に有効化
        rule_action_override {
          name = "SizeRestrictions_BODY"
          action_to_use {
            count {}
          }
        }

        rule_action_override {
          name = "NoUserAgent_HEADER"
          action_to_use {
            count {}
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "CommonRuleSet"
      sampled_requests_enabled   = true
    }
  }

  # Step 3: SQLインジェクションもCountから
  rule {
    name     = "AWSManagedRulesSQLiRuleSet"
    priority = 20

    override_action {
      count {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesSQLiRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "SQLiRuleSet"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "production-web-acl"
    sampled_requests_enabled   = true
  }
}

ここで地味に大事なのがSizeRestrictions_BODYNoUserAgent_HEADERの除外。うちの環境では最初にこれでハマった。Webhookのペイロードが8KBを超えることがあったし、一部の自動化ツールはUser-Agentを設定しないことがある。この2つはほぼ確実に誤検知を踏むので、最初から個別にCountに上書きしておくのが無難だと思う。

ログ分析クエリ(Athena)

Countモードで1週間動かした後、Athenaでこんなクエリを回して誤検知の候補を探す。

-- どのルールが何回引っかかっているか
SELECT
  rule_group_list.rule_match_details.rule_id AS matched_rule,
  COUNT(*) AS hit_count,
  COUNT(DISTINCT httprequest.clientip) AS unique_ips,
  MIN(timestamp) AS first_seen,
  MAX(timestamp) AS last_seen
FROM
  waf_logs
CROSS JOIN UNNEST(rule_group_list) AS t(rgl)
CROSS JOIN UNNEST(rgl.terminating_rule) AS t2(rule_match_details)
WHERE
  action = 'COUNT'
  AND from_iso8601_timestamp(timestamp) > NOW() - INTERVAL '7' DAY
GROUP BY
  rule_group_list.rule_match_details.rule_id
ORDER BY
  hit_count DESC
LIMIT 50;
-- 特定ルールで引っかかったリクエストのURLパターン確認
SELECT
  httprequest.uri,
  httprequest.httpmethod,
  COUNT(*) AS count
FROM
  waf_logs
WHERE
  action = 'COUNT'
  AND from_unixtime(timestamp/1000) > NOW() - INTERVAL '7' DAY
  -- 調査したいルール名を指定
  AND filter(rule_group_list, r -> r.terminating_rule.rule_id = 'SQLi_BODY') != ARRAY[]
GROUP BY
  httprequest.uri,
  httprequest.httpmethod
ORDER BY
  count DESC;

このクエリで「/api/v2/reports/queryが2000回ヒット」みたいな結果が出たら、そのパスは除外対象候補。正規の業務リクエストかどうかを確認して、除外するかAPIの実装を変えるかを判断する。個人的には「APIの設計を変えられるなら変えた方がいい」派だけど、現実的には既存APIを変えられないケースも多いので、その場合は後述のカスタムルールで除外する。

Bot Control Targeted Inspectionの活用(2026年の本番知見)

Bot Controlは正直、最初は懐疑的だった。WCUも食うし、追加料金もかかる。でも不正ログイン試行が週5000件規模になってきたあたりで本格導入に踏み切った。

2026年のBot Control設定

rule {
  name     = "AWSManagedRulesBotControlRuleSet"
  priority = 5

  override_action {
    none {}
  }

  statement {
    managed_rule_group_statement {
      name        = "AWSManagedRulesBotControlRuleSet"
      vendor_name = "AWS"

      managed_rule_group_config {
        aws_managed_rules_bot_control_rule_set {
          # 2026年から強化されたTargeted Inspectionを使用
          inspection_level        = "TARGETED"
          enable_machine_learning = true
        }
      }

      # 監視系ツール(Datadog、New Relic等)は除外
      scope_down_statement {
        not_statement {
          statement {
            regex_match_statement {
              field_to_match {
                single_header {
                  name = "user-agent"
                }
              }
              regex_string = "(Datadog|NewRelicPinger|UptimeRobot|StatusCake)"
              text_transformations {
                priority = 0
                type     = "LOWERCASE"
              }
            }
          }
        }
      }
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "BotControl"
    sampled_requests_enabled   = true
  }
}

enable_machine_learning = trueは2026年に入ってGAになったオプション。シグネチャだけでは検出できない高度なボットを機械学習で判定してくれる。導入後3ヶ月で不正ログイン試行が97%減少した。ただし誤検知も多少あって、API Gatewayからのヘルスチェックが最初は引っかかった。監視ツールの除外設定は必須と思っておいた方がいい。

本番環境のAWS構成図

うちの本番はこういう構成になっている。

graph TB
    subgraph Internet["Internet"]
        Users[正規ユーザー]
        Bots[Bot・攻撃者]
    end

    subgraph CloudFront["CloudFront Distribution"]
        CF[CloudFront]
        WAFCF[WAF WebACL<br/>CloudFront scope]
    end

    subgraph REGION["ap-northeast-1"]
        subgraph WAF_Layer["WAF Layer"]
            WAFR[WAF WebACL<br/>Regional scope]
        end

        subgraph ALB_Layer["ALB"]
            ALB[Application Load Balancer]
        end

        subgraph VPC["VPC 10.0.0.0/16"]
            subgraph PublicSubnet["Public Subnet"]
                NAT[NAT Gateway]
            end

            subgraph PrivateSubnet_A["Private Subnet AZ-a"]
                ECS_A[ECS Tasks<br/>API Servers]
            end

            subgraph PrivateSubnet_C["Private Subnet AZ-c"]
                ECS_C[ECS Tasks<br/>API Servers]
            end

            subgraph DataSubnet["Data Subnet"]
                RDS[(Aurora PostgreSQL)]
                REDIS[ElastiCache Redis]
            end
        end

        subgraph Monitoring["Monitoring"]
            CW[CloudWatch Logs<br/>WAF Full Logs]
            KF[Kinesis Firehose]
            S3L[S3 WAF Logs Bucket]
            ATHENA[Athena<br/>ログ分析]
        end

        subgraph Security["Security"]
            GD[GuardDuty]
            SM[Security Hub]
            SC[Shield Advanced]
        end
    end

    Users --> CF
    Bots --> CF
    CF --> WAFCF
    WAFCF -->|Block| Bots
    WAFCF -->|Allow| ALB
    ALB --> WAFR
    WAFR --> ECS_A
    WAFR --> ECS_C
    ECS_A --> RDS
    ECS_C --> RDS
    ECS_A --> REDIS
    ECS_C --> REDIS
    WAFR --> KF
    KF --> S3L
    S3L --> ATHENA
    GD --> SM
    SC --> ALB

CloudFront側とALB側の両方にWAFを配置しているのがポイント。CloudFront側はグローバルスコープで主にIPレピュテーションとBot Controlを担当して、ALB側はRegionalスコープでアプリケーション固有のルールを担当する。二重にしているのは、CloudFrontをバイパスして直接ALBを叩いてくる攻撃に備えるため。ALBのDNSをセキュリティグループで守り切れない場合のバックアップとして機能する。

ルールグループ選定の実務判断基準

1年半運用してわかった、どのルールを有効化すべきかの判断基準をまとめる。

ルールグループ推奨アクション注意点対象
AmazonIpReputationListBlock(即時OK)誤検知ほぼなし全環境
CommonRuleSetCount→段階的にBlockSizeRestrictions_BODYは要確認全環境
KnownBadInputsRuleSetBlock(比較的安全)Log4Shell等の既知CVEをブロック全環境
SQLiRuleSetCount→BlockJSON APIは誤検知多いDB接続あり
AdminProtectionRuleSetBlock/admin等のパスへのアクセス制御管理画面あり
AnonymousIpListCount→要判断VPN利用ユーザーへの影響を確認ケースバイケース
BotControlRuleSetCountのTargetedから監視ツールの除外設定必須Bot対策必要な場合
ATPRuleSetCount→BlockJavaScript統合で精度向上ログイン機能あり
LinuxRuleSetBlockLinuxサーバーなら基本入れるLinux環境
PHPRuleSetBlockPHPアプリのみPHP環境

正直、AnonymousIpListは難しくて、うちは結局Countのままにしている。在宅勤務でVPN使っているユーザーが普通にいるし、海外からアクセスするケースもある。完全にBlockしてサポートに問い合わせが来るより、ログで監視しておく方が現実的だと判断した。「セキュリティのために不便にする」のは限度があって、ビジネス要件とのバランスが大事だと思う。

誤検知との付き合い方と継続的チューニング

GuardDutyのFalse Positive問題と構造は似ていて、WAFも「ブロックすること」が目的じゃなく「正規トラフィックを通しながら攻撃を防ぐこと」が目的。この当たり前のことを忘れると詰む。最初の俺がまさにそれだった。

カスタムルールで誤検知をコントロールする

# 特定パスをマネージドルールの評価から除外
rule {
  name     = "ExcludeReportQueryPath"
  priority = 0  # マネージドルールより高い優先度

  action {
    allow {}
  }

  statement {
    and_statement {
      statements {
        # 特定パスのみ
        byte_match_statement {
          field_to_match {
            uri_path {}
          }
          positional_constraint = "STARTS_WITH"
          search_string         = "/api/v2/reports/query"
          text_transformations {
            priority = 0
            type     = "NONE"
          }
        }
      }
      statements {
        # 内部サービスのIPからのみ
        ip_set_reference_statement {
          arn = aws_wafv2_ip_set.internal_services.arn
        }
      }
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "ExcludeReportQueryPath"
    sampled_requests_enabled   = true
  }
}

重要なのは、除外を「全IP」で設定しないこと。上の例のように「特定パス」かつ「信頼できるIP」という条件を組み合わせることで、攻撃者が除外パスを悪用するリスクを下げられる。「このパスはWAFを全部スキップ」みたいな雑な設定をやってしまうと、せっかくのWAFが穴だらけになる。

ブロック率の推移

Countモード開始から段階的にBlockモードに移行していくにつれ、ブロック率がこんな感じで上がっていった。

xychart-beta
    title "WAF導入後のブロック率推移(月次)"
    x-axis ["2025-02", "2025-03", "2025-04", "2025-05", "2025-06", "2025-07", "2025-08"]
    y-axis "ブロック率(%)" 0 --> 30
    line [0, 3.2, 8.1, 11.4, 14.2, 15.8, 16.3]

16%台というのは「正規トラフィックの16%をブロックしている」ではなく「全リクエストの16%が攻撃と判定されてブロックされている」という意味。誤検知(正規トラフィックのブロック)は今は0.01%以下に収まっている。

コスト感と運用負荷の現実

WAF v2のコストはWebACL料金 + ルール料金 + リクエスト料金の合計。うちの規模(月1億リクエスト程度)での実績値を共有する。

コスト要素月額(目安)
WebACL(2個:Regional + CloudFront)$10
マネージドルールグループ(6個)$30
リクエスト処理料金(1億リクエスト)$60
Bot Control追加料金(1億リクエストの一部)$20
ログ出力(Firehose + S3)$15
合計約$135/月

月135ドルで本番サービスを守れていると思えば安いけど、最初は「WAFだけでこんなにかかるの」と思った。Shield Advancedは月3000ドルかかるのでまだ導入していない。DDoS対策はAWS Network Firewallとの組み合わせで対応している。

セキュリティ全般のコンプライアンス対応という観点ではSOC2対応記事も参考になる。WAFの設定は監査ログとして残すことが重要で、Full Loggingを必ず有効化しておくこと。

運用負荷について正直に言うと、最初の2〜3ヶ月はチューニングにかなり時間がかかった。週1回はAthenaでログを分析して、誤検知がないかチェックする作業が必要だった。今は月1回の定期レビューで済んでいる。慣れれば重くない作業だけど、「WAFを入れたら終わり」ではないのは覚悟しておいてほしい。

まとめ

1年半の本番運用でわかったWAF v2マネージドルール活用のポイントをまとめると:

  • 全有効化・即Blockは絶対NG。必ずCountモードから始めて、最低1〜2週間のログ分析後にBlockへ移行する
  • 誤検知ゼロは無理。どの誤検知を許容してどの攻撃を防ぐか、ビジネス要件に基づいて判断する。SizeRestrictions_BODYNoUserAgent_HEADERは要注意
  • IPレピュテーションリストだけは即Block。誤検知リスクが低く、効果が高い。これだけでもブロック数の半分以上を占める
  • Bot Control Targeted Inspectionは2026年時点でかなり実用的になった。不正ログイン試行の多いサービスには投資対効果が高い
  • ログ分析基盤(Athena + S3)の整備が運用継続の前提。ログがないとチューニングのしようがない

次のアクションとして、まだWAFを導入していないなら、まずIPレピュテーションリストだけBlockモードで設定するところから始めてみてほしい。5分でできて、即効性がある。皆さんのチームのWAF運用状況はどうですか?誤検知との戦い方、コメントで教えてもらえると嬉しい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事