Amazon Web Services ブログ
Amazon CloudFront と AWS WAF によりエッジで MCP エンドポイントを保護する
本記事は 2026 年 10 月 5 日に公開された Jaiganesh Girinathan、Harith Gaddamanugu、Kaustubh Phatak、Ted Middleton による “Protect MCP Endpoints at the Edge with Amazon CloudFront and AWS WAF” を翻訳したものです。
AWS WAF を使うと、 Model Context Protocol (MCP) エンドポイントをエッジで実用的に保護し、可視化できます。MCP は、 AI エージェントがツールやデータとやり取りするための標準的な方法として急速に普及しました。もともと MCP は、長時間のセッション、永続的な接続、クライアントとサーバーインスタンスの紐付けを前提としたステートフルなプロトコルでした。 2026 年 7 月 28 日の改定 では、 MCP がステートレスな HTTP リクエスト/レスポンス型のプロトコルとして再設計されました。これにより、すべての MCP リクエストは自己完結した POST リクエストとなり、ネットワーク上ではほかの REST API 呼び出しと見分けがつかない形になりました。その結果、ほかの HTTP API の前段ですでに利用しているのと同じレイヤー 7 の検査、変換、最適化を、 MCP サーバーのコードを変更することなく CDN や AWS WAF のレイヤーで適用できるようになります。
本記事では、Amazon CloudFront または Application Load Balancer (ALB) の背後にデプロイした MCP エンドポイントを、 AWS WAF で保護し、可視化する方法を紹介します。新しい仕様によって簡単に適用できる、次の 9 つのユースケースを順に説明します。
- すべてのリクエストで、許可された
MCP-Protocol-Versionを必須にする - AWS WAF Bot Control で呼び出し元を分類し、相手がどのエージェントなのかを把握する
- ボディサイズのプリフライトチェックを追加する
- エージェントが呼び出せるツールの許可リストを作成する
- query_database ツールで破壊的な SQL をブロックする
- ツールの引数に隠されたサーバーサイドリクエストフォージェリ ( SSRF )をブロックする
- クエリ形式の入力に含まれる SQL インジェクションを検出する
- ツールごとにレート制限を設け、単一の高コストな操作によってバックエンドのリソースが枯渇しないようにする
- オプションとして、ツールごとの制限の下に、全体の上限となるグローバルなレート制限を設ける
MCP をエッジで検査できるようになった理由
2026 年 7 月 28 日の改定より前の仕様では、 MCP セッションにはハンドシェイクが必要でした。クライアントが initialize を送信すると、サーバーが Mcp-Session-Id を返し、以降のすべてのリクエストはその識別子を付けて同じサーバーインスタンスに送られていました。リモート環境へのデプロイでは、接続の固定 ( ピニング ) 、レプリカ間でのセッション状態の共有、ストリームを維持するためのプロキシの運用が必要でした。プロトコルがステートフルで、リクエストストリームの中身も不透明であったため、エッジでの検査は現実的ではありませんでした。
新しい仕様では、セッションが完全に廃止されました。 MCP リクエストは、どのサーバーインスタンスでも処理できる単一の自己完結した HTTP POST リクエストになっています。
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: query_database
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "query_database",
"arguments": { "filter": "id = 42" }
}
}
エッジでポリシーを適用するうえで重要になるのが、 Mcp-Method と Mcp-Name の 2 つのヘッダーです。どちらも Streamable HTTP Transport では必須とされています ( SEP-2243 ) 。これらのヘッダーは、ロードバランサー、ゲートウェイ、ファイアウォールがボディを解析せずに、操作の内容に基づいてルーティングできるようにするために用意されています。ヘッダーとボディの内容が一致しないリクエストをサーバーは拒否するため、これらのヘッダーは意図を示す信頼できるシグナルになります。
AWS WAF にとってこれは、MCP リクエストに関するもっとも重要な 2 つの情報 ( どの操作が実行されるのか、どのツールが呼び出されるのか ) を、ヘッダーだけで把握できることを意味します。より深い検査が必要な場合は、 JSON ボディの $.params.arguments パスが対象になります。すべての MCP ツール呼び出しは、ツールごとの引数をこのパスに格納します。上記のサンプルリクエストであれば、 query_database に渡される filter の値が該当します。 JSON ペイロードのそれ以外の部分は、プロトコルの定型構造です。
ポリシーを適用する場所は、 MCP クライアントの内部 ( たとえば、エージェントが呼び出せるツールの制限 ) 、ネットワーク境界 ( たとえば、承認されていない MCP メソッドをブロックするファイアウォールルール ) 、あるいはサーバーの前段にある CDN レイヤーのいずれかです。本記事では CDN レイヤー ( エッジ ) に焦点を当てます。 CDN レイヤーは、利用者自身が完全にコントロールできる唯一の適用ポイントです。フレームワークやネットワーク経路に関係なくすべての呼び出し元をカバーでき、 MCP サーバーのコードを変更する必要もありません。
AWS WAF による MCP エンドポイント保護のリファレンスアーキテクチャ
アーキテクチャは、一般的な REST API の前段で運用しているものと同じです。 Amazon CloudFront ディストリビューション ( または Application Load Balancer ) を MCP サーバーの前段に配置し、 AWS WAF のウェブ ACL をそのディストリビューションに関連付けます。エージェントから届いたリクエストを AWS WAF がインラインで評価し、すべてのルールを通過したリクエストだけがオリジンに到達します。追加のホップやプロキシ層は不要で、 MCP サーバーのコードを変更する必要もありません。図 1 にリクエストの経路を示します。
図 1. AWS WAF のウェブ ACL を関連付けた Amazon CloudFront ( または ALB ) が、各 MCP リクエストをインラインで評価する構成。ブロックルールに一致したリクエストには 403 が返され、通過したリクエストは MCP サーバーに転送されます。
検査の境界を理解する:エッジとオリジン
MCP-Protocol-Version 、 Mcp-Method 、 Mcp-Name 、 Authorization ヘッダーの 4 つです。 AWS WAF は最初の 3 つに対して直接マッチングやレート制限を行えます。一方、ベアラートークンの検証やデコードはできないため、トークンの検証はオリジンで行います。
AWS WAF のルールスタック
Priority は、評価順序を制御する AWS WAF のプロパティです。たとえば、軽量なヘッダーチェックを最初に、対象を絞った JSON ボディの検査を次に、レート制限を最後に配置します。本記事で使用している優先度の値は、あくまで一例です。重要なのは順序であり、具体的な数値ではありません。番号の間隔をあえて空けているのは、あとから新しいルールを追加する際に、ウェブ ACL 全体の番号を振り直さなくて済むようにするためです。
ユースケース 1:有効な MCP プロトコルバージョンを持たないリクエストを拒否する
Mcp-Method と Mcp-Name を意図を示すシグナルとして信頼します。この信頼が正当化できるのは、ヘッダーとボディの整合性検証が必須となるプロトコルバージョンをリクエストが宣言している場合に限られます。 2026 年 7 月 28 日の仕様改訂 でも、このことが明記されています。ミラーリングされたヘッダーに基づいてポリシーを適用する中間装置は、検証されていないヘッダー値を信頼するのではなく、 MCP-Protocol-Version が古いまたは存在しないリクエストを拒否すべきとされています。
MCP-Protocol-Version を持たない /mcp へのリクエストをすべてブロックします。単一のヘッダーをマッチングするだけでボディの検査を行わないため、スタックの中でもっとも軽量なルールであり、先頭に配置します。
/mcp で始まり、かつ mcp-protocol-version ヘッダーが ^2026-07-28$ に一致しない場合にブロック
{
"Name": "RequireMCPProtocolVersion",
"Priority": 1,
"Action": {
"Block": {}
},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "/mcp",
"FieldToMatch": {
"UriPath": {}
},
"PositionalConstraint": "STARTS_WITH",
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
},
{
"NotStatement": {
"Statement": {
"RegexMatchStatement": {
"RegexString": "^2026-07-28$",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-protocol-version"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
}
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "RequireMCPProtocolVersion"
}
}
正規表現にはアンカーを付けているため、わずかに異なる値 ( たとえば 2026-07-28-preview ) も拒否されます。新しいリビジョンがリリースされた場合は、 ^(2026-07-28|2026-11-15)$ のようにマッチする候補を追加してください。対象を /mcp に限定しているため、同じディストリビューション上のほかのコンテンツには影響しません。
ユースケース 2:AWS WAF Bot Control で各呼び出し元のアイデンティティにラベルを付ける
| 呼び出し元の種類 | ラベル | 備考 |
| 検証済みクローラー | awswaf:managed:aws:bot-control:bot:name:<name>awswaf:managed:aws:bot-control:bot:verified |
検証済みボットに付与されるラベル |
| Web Bot Auth を使用するエージェント | awswaf:managed:aws:bot-control:bot:web_bot_auth:verified |
署名の検証に失敗した場合は :invalid、:expired、:unknown_bot |
| 識別可能なベンダー | awswaf:managed:aws:bot-control:bot:vendor:<vendor_name> |
現在は Amazon Bedrock AgentCore でホストされているエージェントに付与される |
| クラウドでホストされている呼び出し元 | awswaf:managed:aws:bot-control:signal:cloud_service_provider:<csp> |
サフィックスは aws、gcp、azure、oracle のいずれか |
| ヘッドレスブラウザ | awswaf:managed:aws:bot-control:signal:automated_browser |
MCP エンドポイントでは不審なものとして扱う |
- SignalNonBrowserUserAgent は、 User-Agent が実際のブラウザではない呼び出し元をブロックします。そのため、標準的な HTTP クライアント (python-requests 、Go の net/http 、 Node の fetch) を使用するエージェントフレームワークはすべて、このルールに引っかかります。
- CategoryHttpLibrary は、既知の HTTP ライブラリを直接ブロックします。 curl や wget に加え、ほとんどの MCP クライアントのコードベースが依存している SDK も対象になります。
- SignalAutomatedBrowser は、ヘッドレス Chrome 、Selenium 、Playwright をブロックします。これらは、エージェントフレームワークが内部でよく利用しているものです。
- CategoryAI は、 AI エージェントのトラフィックであることを自ら名乗るトラフィックをブロックします。正当な MCP クライアントのほとんどは、このカテゴリに該当します。
- SignalKnownBotDataCenter は、既知のクラウドデータセンターの IP 範囲からの呼び出し元をブロックします。 MCP エンドポイントへのエージェントのトラフィックの大半は、一般家庭の IP ではなくデータセンターから送信されます。そのため、このルールをそのまま有効にすると、正当な呼び出し元のほとんどがブロックされてしまいます。
TGT_VolumetricSession 、 TGT_VolumetricIpTokenAbsent ) 、スクレイパーや SEO カテゴリのブロック、そして後続のアイデンティティとツールを組み合わせたルールの入力となる Web Bot Auth ( WBA ) の判定です。グループ全体に対してウェブ ACL レベルで OverrideAction: Count を設定するのではなく、マネージドルールグループのステートメントで RuleActionOverrides を使用してください。前者の方法では、ルールグループ全体が Count になり、Bot Control の利点が失われてしまうからです。
SignalNonBrowserUserAgent 、 CategoryHttpLibrary 、 SignalAutomatedBrowser 、 CategoryAI 、 SignalKnownBotDataCenter の各ルールを Count に設定し、ブロックせずにラベルだけを付与
{
"Name": "BotControlCommon",
"Priority": 2,
"OverrideAction": {
"None": {}
},
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesBotControlRuleSet",
"ManagedRuleGroupConfigs": [
{
"AWSManagedRulesBotControlRuleSet": {
"InspectionLevel": "COMMON"
}
}
],
"RuleActionOverrides": [
{
"Name": "SignalNonBrowserUserAgent",
"ActionToUse": {
"Count": {}
}
},
{
"Name": "CategoryHttpLibrary",
"ActionToUse": {
"Count": {}
}
},
{
"Name": "SignalAutomatedBrowser",
"ActionToUse": {
"Count": {}
}
},
{
"Name": "CategoryAI",
"ActionToUse": {
"Count": {}
}
},
{
"Name": "SignalKnownBotDataCenter",
"ActionToUse": {
"Count": {}
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BotControlCommon"
}
}
signal:non_browser_user_agent 、 bot:category:http_library 、 signal:automated_browser 、 bot:category:ai 、 signal:known_bot_data_center ) を付与します。そのため、後続のルールでこれらのラベルを引き続き利用できます。 Block によって評価が終了しなくなるだけです。
これらのラベルを利用できるような状態であると、後続のルールでアイデンティティの条件と Mcp-Name のマッチングを組み合わせられます。たとえば「呼び出し元の Web Bot Auth 署名が無効で、かつ高コストなツールを呼び出している場合はブロックする」といったルールを、独自のフィンガープリンティングのロジックなしで記述できます。次の例がそのルールです。tools/call に一致し、ツールが expensive-tools パターンセットに含まれ、かつ Web Bot Auth のラベルが invalid または unknown_botである場合にブロックします。
mcp-method ヘッダーが tools/call と等しく、かつ mcp-name が expensive-tools パターンセットに一致し、かつ呼び出し元の Web Bot Auth ラベルが invalid または unknown_bot である場合にブロック
{
"Name": "BlockUnverifiedBotOnHighCostTool",
"Priority": 4,
"Action": {
"Block": {}
},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "tools/call",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-method"
}
},
"PositionalConstraint": "EXACTLY",
"TextTransformations": [
{
"Priority": 0,
"Type": "LOWERCASE"
}
]
}
},
{
"RegexPatternSetReferenceStatement": {
"ARN": "arn:aws:wafv2:us-east-1:123456789012:global/regexpatternset/mcp-expensive-tools/def456",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-name"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
},
{
"OrStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:bot-control:bot:web_bot_auth:invalid"
}
},
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:bot-control:bot:web_bot_auth:unknown_bot"
}
}
]
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockUnverifiedBotOnHighCostTool"
}
}
BlockUnverifiedBotOnHighCostTool は、まず Count モードでデプロイしてください。そして一定の観察期間を設けて、呼び出し元に付与されるラベルの分布を確認してから、この組み合わせルールを Block に切り替えます。Bot Control の料金は、サブスクリプション料金とリクエストごとの評価料金で構成されます。有効にする場合は、その分の予算を確保してください。最新の料金は、 AWS WAF の料金と Bot Control の料金セクション を参照してください。
ユースケース 3:検査の前にサイズの大きすぎるリクエストボディをブロックする
AssociationConfig を使用すると、この上限を 64 KB まで引き上げられます。本記事では上限を 32 KB に引き上げています。これは、一般的な MCP ツールの引数には十分なサイズでありつつ、過大なペイロードを招かない値です。
OversizeHandling の設定に従って処理されます。ボディを検査するすべてのルールで OversizeHandling: MATCH を設定すると安全ではありますが、ノイズが多くなります。正当なサイズの大きいボディが、すべてのルールに引っかかってしまうためです。代わりに、最初の段階で一度だけフェイルクローズする方針を取ります。具体的には、 32 KB を超えるボディを単一のサイズ制約ルールで拒否します。この設定があれば、サイズの大きすぎるボディは後続のルールに到達しないため、後続のすべてのルールで OversizeHandling: NO_MATCH を安全に使用できます。
OversizeHandling: MATCH が発動し、誤ったブロックが発生してしまいます。ウェブ ACL の作成時に上限を引き上げてください。
{
"Name": "mcp-web-acl",
"Scope": "CLOUDFRONT",
"AssociationConfig": {
"RequestBody": {
"CLOUDFRONT": {
"DefaultSizeInspectionLimit": "KB_32"
}
}
},
"DefaultAction": {
"Allow": {}
},
"Rules": [ /* ... */ ]
}
プリフライトチェックのルールは次のとおりです。
{
"Name": "MCPBodySizePreflight",
"Priority": 5,
"Action": {
"Block": {}
},
"Statement": {
"SizeConstraintStatement": {
"FieldToMatch": {
"Body": {
"OversizeHandling": "MATCH"
}
},
"ComparisonOperator": "GT",
"Size": 32768,
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "MCPBodySizePreflight"
}
}
ユースケース 4:呼び出し可能なツールを許可リストで制限する
execute_shell や delete_all_records を呼び出そうとしても、リクエストはエッジで 403 として止められ、サーバーには到達しません。 Mcp-Name はファーストクラスのヘッダーであるため、このルールはボディを解析しない、純粋なヘッダールールになります。
${awswaf:request_id:} というプレースホルダーは評価時に実際のリクエスト ID に置き換わります。これにより、呼び出し元はサポートに伝えるべき単一のフィールドを把握でき、オンコール担当者はログ内を直接 grepで検索できます。
mcp-method が tools/call と等しく、かつ mcp-name が許可されたツールの正規表現パターンセットに一致しない場合にブロック
{
"Name": "BlockUnapprovedMCPTools",
"Priority": 10,
"Action": {
"Block": {
"CustomResponse": {
"ResponseCode": 403,
"CustomResponseBodyKey": "McpBlockedTool",
"ResponseHeaders": [
{
"Name": "x-mcp-waf-request-id",
"Value": "${awswaf:request_id:}"
}
]
}
}
},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "tools/call",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-method"
}
},
"PositionalConstraint": "EXACTLY",
"TextTransformations": [
{
"Priority": 0,
"Type": "LOWERCASE"
}
]
}
},
{
"NotStatement": {
"Statement": {
"RegexPatternSetReferenceStatement": {
"ARN": "arn:aws:wafv2:us-east-1:123456789012:global/regexpatternset/mcp-allowed-tools/abc123",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-name"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
}
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockUnapprovedMCPTools"
}
}
参照先のレスポンスボディはウェブ ACL レベルで一度だけ定義するため、ほかの Block アクションでも再利用できます。
{
"CustomResponseBodies": {
"McpBlockedTool": {
"ContentType": "APPLICATION_JSON",
"Content": "{\"error\":\"tool_not_allowed\",\"waf_request_id\":\"${awswaf:request_id:}\",\"client_ip\":\"${awswaf:ip:}\"}"
}
}
}
正規表現パターンセットは、サーバーの tools/list レスポンスから生成するため、マニフェストと常に同期した状態を保てます。初期の内容は、ツール名をアンカー付きの選択肢として連結したものです:^(query_database|get_document|send_notification)$
ユースケース 5:query_database ツールで破壊的な SQL をブロックする
query 引数には、 LLM が組み立てた生の SQL が含まれます。プロンプトインジェクションによって、 SELECT が DROP TABLE に書き換えられてしまう可能性があります。 AWS WAF を使えば、リクエストがサーバーに到達する前に、破壊的なパターンをエッジで拒否できます。
AWSManagedRulesSQLiRuleSet とは異なります。このマネージドルールグループは、本来 SQL が含まれるべきではないフィールド向けに作られています。そのため構文パーサーを使用して、整った形式の SQL を見つけるとすべて検出してしまいます。そこには、ツールが本来実行すべき正当な SELECT クエリも含まれます。ここで説明するルールはより限定的です。 query 引数は SQL を扱うことが目的なので SQL 自体は許可しつつ、読み取り専用のツールが発行する必要のないサブセット ( DROP 、 TRUNCATE 、 ALTER 、 GRANT 、 DELETE FROM 、 UPDATE…SET ) をブロックします。
Mcp-Name ヘッダーが一致した場合に mcp:tool:query_database というラベルを付与します。ブロックルールは、このラベルが存在し、かつ query 引数に破壊的なキーワードが含まれている場合にのみ発動します。
mcp-name が query_database と等しい場合に mcp:tool:query_database のラベルを付与する。次に、そのラベルが付与されていて、かつ /params/arguments/query が破壊的な SQL キーワードに一致する場合にブロック
{
"Name": "LabelToolQueryDatabase",
"Priority": 15,
"Action": {
"Count": {}
},
"RuleLabels": [
{
"Name": "mcp:tool:query_database"
}
],
"Statement": {
"ByteMatchStatement": {
"SearchString": "query_database",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-name"
}
},
"PositionalConstraint": "EXACTLY",
"TextTransformations": [
{
"Priority": 0,
"Type": "LOWERCASE"
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "LabelToolQueryDatabase"
}
}
{
"Name": "BlockDestructiveSQL",
"Priority": 20,
"Action": {
"Block": {}
},
"Statement": {
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "mcp:tool:query_database"
}
},
{
"RegexMatchStatement": {
"RegexString": "\\b(DROP|TRUNCATE|ALTER|GRANT|REVOKE|CREATE|DELETE\\s+FROM|INSERT\\s+INTO|UPDATE\\s+.+\\s+SET)\\b",
"FieldToMatch": {
"JsonBody": {
"MatchPattern": {
"IncludedPaths": [
"/params/arguments/query"
]
},
"MatchScope": "VALUE",
"InvalidFallbackBehavior": "MATCH",
"OversizeHandling": "NO_MATCH"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "COMPRESS_WHITE_SPACE"
},
{
"Priority": 1,
"Type": "UPPERCASE"
}
]
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockDestructiveSQL"
}
}
query 引数に書き込みや削除のキーワードが含まれているのであれば、上流のどこかで何かが改ざんされています。もっとも可能性が高いのは、プロンプトインジェクションです。 COMPRESS_WHITE_SPACE によって、「D R O P」のような回避の試みも検出できます。 InvalidFallbackBehavior: MATCH を指定すると、 query 引数自体が含まれていないリクエストもブロックされます。欠落したフィールドを検査なしで通過させるのではなく、ポリシー違反として扱うためです。
ユースケース 6:ツールの引数に隠された SSRF を検出してブロックする
169.254.169.254) 、 IPv4 のプライベートアドレス範囲、 IPv6 のループバックおよびユニークローカルのプレフィックス、クラスター内部のサービスに到達するための DNS 名です。マッチングの前に URL_DECODE を適用して、 1 回分のパーセントエンコーディングを展開します。
mcp-method が tools/call と等しく、かつ /params/arguments 内のいずれかの値がプライベート IP 、IMDS エンドポイント、内部 DNS 名に一致する場合にブロック
{
"Name": "BlockMCPSSRF",
"Priority": 30,
"Action": {
"Block": {}
},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "tools/call",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-method"
}
},
"PositionalConstraint": "EXACTLY",
"TextTransformations": [
{
"Priority": 0,
"Type": "LOWERCASE"
}
]
}
},
{
"OrStatement": {
"Statements": [
{
"RegexMatchStatement": {
"RegexString": "(169\\.254\\.169\\.254|metadata\\.google\\.internal|kubernetes\\.default(\\.svc)?|\\.(local|internal)([^a-z0-9.-]|$))",
"FieldToMatch": {
"JsonBody": {
"MatchPattern": {
"IncludedPaths": [
"/params/arguments"
]
},
"MatchScope": "VALUE",
"InvalidFallbackBehavior": "NO_MATCH",
"OversizeHandling": "NO_MATCH"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "URL_DECODE"
},
{
"Priority": 1,
"Type": "LOWERCASE"
}
]
}
},
{
"RegexMatchStatement": {
"RegexString": "\\b(10|127|169\\.254|172\\.(1[6-9]|2[0-9]|3[01])|192\\.168)\\.\\d{1,3}\\.\\d{1,3}\\b",
"FieldToMatch": {
"JsonBody": {
"MatchPattern": {
"IncludedPaths": [
"/params/arguments"
]
},
"MatchScope": "VALUE",
"InvalidFallbackBehavior": "NO_MATCH",
"OversizeHandling": "NO_MATCH"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "URL_DECODE"
}
]
}
},
{
"RegexMatchStatement": {
"RegexString": "(::1|::ffff:|f[cd][0-9a-f]{2}:|fe80:)",
"FieldToMatch": {
"JsonBody": {
"MatchPattern": {
"IncludedPaths": [
"/params/arguments"
]
},
"MatchScope": "VALUE",
"InvalidFallbackBehavior": "NO_MATCH",
"OversizeHandling": "NO_MATCH"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "URL_DECODE"
},
{
"Priority": 1,
"Type": "LOWERCASE"
}
]
}
}
]
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockMCPSSRF"
}
}
.svc.cluster.local への到達を許可する」と定義する方が、判断が容易になります。
ユースケース 7:クエリ形式の入力に含まれる SQL インジェクションを検出する
AWSManagedRulesSQLiRuleSet を使用する方法と、組み込みのプリミティブである SqliMatchStatement を使用する方法です。マネージドルールグループは生のボディをルールセットと照合して検査するため、HTML フォームや URL エンコードされた API に対しては適切なデフォルトの選択肢です。しかし MCP の場合、ペイロードは JSON であり、脆弱性のあるフィールド ( sql 、 query 、 filter など ) は $.params.arguments 内の文字列です。JSON エンコードされても有効なままの典型的なインジェクションペイロードは、生のボディの検査をすり抜ける可能性があります。たとえば、値として埋め込まれた 1 UNION SELECT username, password FROM users は周囲の JSON 構造によって偽装されてしまいます。
arguments フィールドに直接向けることです。 JsonBody に対して SqliMatchStatement を使用すると、まず指定した JSON パスにある文字列の値を抽出し、次にそれぞれの値に対して SQL の構文パーサーを実行します。JSON のラッパーではなく、実際の SQL の内容を検査するということです。ボディを検査するほかのルールと同様に、 tools/call にスコープを限定し、 SQL を正当に受け付けるツールをラベルチェーンで除外します。
mcp-method が tools/call と等しく、かつ mcp:tool:execute_sql のラベルが付与されておらず、かつ /params/arguments が感度 LOW の SQL インジェクションパーサーに検出された場合にブロック
{
"Name": "MCPSQLiProtection",
"Priority": 40,
"Action": {
"Block": {}
},
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "tools/call",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-method"
}
},
"PositionalConstraint": "EXACTLY",
"TextTransformations": [
{
"Priority": 0,
"Type": "LOWERCASE"
}
]
}
},
{
"NotStatement": {
"Statement": {
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "mcp:tool:execute_sql"
}
}
}
},
{
"SqliMatchStatement": {
"FieldToMatch": {
"JsonBody": {
"MatchPattern": {
"IncludedPaths": [
"/params/arguments"
]
},
"MatchScope": "VALUE",
"InvalidFallbackBehavior": "NO_MATCH",
"OversizeHandling": "NO_MATCH"
}
},
"SensitivityLevel": "LOW",
"TextTransformations": [
{
"Priority": 0,
"Type": "URL_DECODE"
},
{
"Priority": 1,
"Type": "HTML_ENTITY_DECODE"
},
{
"Priority": 2,
"Type": "LOWERCASE"
}
]
}
}
]
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "MCPSQLiProtection"
}
}
SensitivityLevel は慎重に設定する必要があります。HIGH の場合、AWS WAF の SQL パーサーは整った形式の SQL をすべてフラグ付けします。単純な SELECT id FROM users LIMIT 10 であっても、SQL であるというだけで問題として扱われます。これは、呼び出し元が SQL を一切送信すべきではないエンドポイントには適していますが、フィルター式を受け付けるツールには適していません。MCP において実用的な設定は LOW です。LOW では、典型的なインジェクションマーカー ( OR 1=1 、 UNION SELECT 、 ; による複数ステートメントの連結、 EXEC xp_cmdshell ) を検出しつつ、通常のフィルター値にはフラグを立てません。
SELECT 」と「意図しない SELECT 」を区別できません。完全な SQL ステートメントは構文的に SQL であり、パーサーは SQL として扱わざるを得ません。 MCP ツールは、完全な SQL ステートメントではなく、構造化された引数 ( テーブル名、フィルター式、列のリスト ) を受け取るように設計してください。生の SQL を扱うツールは、管理者向けの execute_sql のように、限定的かつ除外されたパスに限定します。リファレンス実装では、 query_database は filter 引数 ( WHERE 句形式の式 ) を受け取り、 execute_sql はラベルによって除外された生の SQL 用のツールになっています。これが私たちの推奨するパターンです。
ユースケース 8:高コストなツールにレート制限を設ける
Mcp-Name を使えば、ツールごとのレート制限を簡単に実装できます。 expensive-tools のパターンセットに含まれるツール ( データベースクエリ、有料 API、大量の計算処理 ) には控えめな制限を設定し、低コストなツールにはより緩い制限を設定します。AWS WAF の RateBasedStatement は、デフォルトでは IP ごとに集計します。MCP の場合は、 カスタム集約キー を使用してツールのヘッダーで集計し、特定の操作に上限を設けます。 EvaluationWindowSec には 60、120、300、600 秒のいずれかを指定できます。この例では 300 ( 5 分 ) を使用しています。
ルールのロジック:
mcp-name が expensive-tools のパターンセットに一致し、かつ同じ mcp-name と送信元 IP の組み合わせによるリクエストが 300 秒間に 200 件を超えた場合にブロック
{
"Name": "MCPExpensiveToolRateLimit",
"Priority": 50,
"Action": {
"Block": {}
},
"Statement": {
"RateBasedStatement": {
"Limit": 200,
"EvaluationWindowSec": 300,
"AggregateKeyType": "CUSTOM_KEYS",
"CustomKeys": [
{
"Header": {
"Name": "mcp-name",
"TextTransformations": [
{
"Priority": 0,
"Type": "LOWERCASE"
}
]
}
},
{
"IP": {}
}
],
"ScopeDownStatement": {
"RegexPatternSetReferenceStatement": {
"ARN": "arn:aws:wafv2:us-east-1:123456789012:global/regexpatternset/mcp-expensive-tools/def456",
"FieldToMatch": {
"SingleHeader": {
"Name": "mcp-name"
}
},
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
}
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "MCPExpensiveToolRateLimit"
}
}
Mcp-Name と送信元 IP の組み合わせで集計しているのは、単一の呼び出し元がツール全体の割り当て予算を使い切ってしまうことを防ぎつつ、多数の正当な呼び出し元がそれぞれ自分の割り当て分を利用できるようにするためです。これは、OAuth トークンをデコードせずに AWS WAF で実現できる「呼び出し元ごと・ツールごと」の制限にもっとも近い方法であり、エッジにおける妥当なデフォルトです。ただし、IP はアイデンティティの代わりとしては大まかな指標にすぎません。エージェントのトラフィックは少数のデータセンターの送信元 IP 範囲から送られる傾向があり、何千もの異なるユーザーがわずかな数の IP の背後にまとまってしまうことがあります。真のユーザーごとのレート制限を行うべきレイヤーはオリジンであり、ベアラートークンを検証してユーザーのアイデンティティを確立された後に行うべきです。
ユースケース 9 ( オプション ):IP ごとのグローバルなレート制限
ツールごとの制限に加えて、 /mcp パスを対象に IP 単位で集計するグローバルな RateBasedStatement を設定すると、全体の上限を設けられます。これにより、検証済みかどうかにかかわらず、単一の送信元が評価ウィンドウ内に N 件を超えるリクエストを送信できなくなります。 AWS WAF では EvaluationWindowSec の値として 60、120、300、600 をサポートしているため、トラフィックに対する考え方に合ったウィンドウを選択できます。一般的な設定は、IP ごとに 5 分間のウィンドウで 5,000 リクエストです。この設定であれば、正当なエージェントのワークロードを妨げることなく、暴走したループや明らかに悪意のある大量のリクエストを検出できます。ウィンドウを短く ( 60 秒 ) すると、バーストへの反応は速くなりますが、通常のトラフィックの変動に対する許容度は低くなります。
可観測性 (Observability)
Mcp-Method と Mcp-Name は httpRequest.headers に含まれているため、各ログエントリを見れば、どのツールが呼び出され、それがどう処理されたのかがわかります。
ツール名の抽出: AWS WAF のログでは、
httpRequest.headers は {name, value} オブジェクトの配列です。次の Amazon CloudWatch Logs Insights のクエリは、ツールごとに抽出・集計します。
次のクエリは、5 種類を超えるツールを呼び出している IP を抽出します。
振る舞いのベースライン:エージェントのアイデンティティ × ツールのフィンガープリント:Bot Control の CategoryAI ルールは、エンドポイントを呼び出している検証済みの AI ボットを検出します。通常の運用では、検証済みのエージェントと、そのエージェントが呼び出すツールの関係は安定しています。未検証またはなりすましのエージェントには bot:name ラベルが付与されないため、この方法ではそれらを検出できません。それでも、昨日まで存在しなかった新しい ( エージェント、ツール ) ペアが出現した場合、上流で何かが変化したことを示すため、有用なベースラインになります。次のクエリは、この対応関係を作成します。
AWS WAF コンソールで無料で利用できる AI トラフィック分析ダッシュボード を使用して、 MCP エンドポイントへの検証済みと未検証のエージェントのトラフィックを分析することもできます。
図 5. リクエスト数とカテゴリ別の上位のボットクローラー。MCP エンドポイントにアクセスしている Bedrockbot、Claudebot、GPTBot などの検証済み AI エージェントを識別しています。
異常検出:ラベル付けルールのメトリクスに対して Amazon CloudWatch 異常検出を使用し、過去の範囲からの逸脱を検知します。アーカイブしたログに対して Amazon Athena の日次クエリを実行し、「ツールの種類数」のシグナルを確認します。1 日のうちに呼び出すツールの種類数が 3 から 15 に急増したエージェントは、振る舞いの形状が変化しています。これは、通常のリクエスト量よりも強力なプロンプトインジェクションの兆候です。ブロックされた mcp-name の値について、上位コントリビューターを継続的に把握するには Amazon CloudWatch Contributor Insights を使用します。
アラート:ページング ( 呼び出し ) に値するアラートは、明示的なブロックに紐づけられています:
- SSRF のブロック:即座にページング。SSRF のブロックは、内部のインフラストラクチャへの到達が実際に試みられていることを示します。
- 未承認ツールのブロックの急増:プロンプトインジェクションの可能性があるイベントとして調査します。ブロックされたツール呼び出しが急増している場合、エージェントが操作されている可能性があります。
- レート制限への継続的な到達:人によるレビューにエスカレーションします。グローバルなレート上限に継続的に達している呼び出し元は、手動で調査すべきほど異常な動作をしています。
安全にデプロイする
SampledRequestsEnabled は true のままにしておきます。 AWS WAF は、ルールごとに一致したリクエストのデータを 3 時間保持します。
まとめ
2026 年 7 月 28 日の改定により、 MCP はヘッダーに十分な構造化メタデータを持つ HTTP プロトコルとなり、エッジでのセキュリティ対策が現実的な選択肢になりました。適切な順序で構成した AWS WAF のスタックを使えば、 MCP サーバーのコードを変更することなく、堅牢な境界防御を実現できます。
AWS WAF のログとラベルをもとに構築した可観測性のレイヤーにより、その境界全体でエージェントが何をしているのかを把握できます。ユーザーごとのレート制限は、意図的にエッジでは行っていません。ユーザーごとのレート制限は、ベアラートークンを検証し、呼び出し元の ID を明確に扱えるオリジンで行うべきものです。ユーザーや IP に基づくレート制限を実現するさまざまな手法については、ほかにもいくつかのブログで紹介しています。たとえば、 Limiting requests to a web application using a Gatekeeper Solution を参照してください。
著者について
翻訳はプロフェッショナルサービスの鈴木(隆)が担当しました。


