サーバーサイド - TECH PLAY - TECH PLAY

TECH PLAY

サーバーサイド

イベント

マガジン

技術ブログ

RAGとJevで「ベクトルDBに根拠となる答えがない」ことを見分ける仕組みを作った話 はじめに こんにちは。1か月間のインターンで、サーバーサイド(Go)を担当した橋本です。 インターンでは、AIエージェントの機能追加の設計・実装に携わりました。この記事では、その中でもRAG(Retrieval-Augmented Generation)の検索の部分に焦点を当てます。RAGは、質問に関係する文書を探してLLMに渡し、その文書をもとに回答を作成する仕組みです。ここではデリッシュキッチンの情報を検索し、AIの回答の根拠として使えるかどうかを検討しました。 作ってみて一番難しかったのは、「ベクトルDBに根拠となる答えがないことをどのように見分けるか」でした。関係の薄い情報をLLMに渡してしまうと、LLMはそれをもとにもっともらしいが正確ではない回答を生成してしまう可能性があります。よってベクトルDBに根拠となる情報が無い場合は、LLMに回答させないようにする必要があります。 この記事では、次の2つのトピックに絞ります。 HYBRIDとANNという2つの検索を組み合わせて、候補を絞る仕組み 絞った候補が質問に答えているかを、TypeSafeのAIモデルJevで文書ごとに判定する仕組み HYBRIDとANNはベクトルDBに対する検索方式で、具体的なアルゴリズムは後述します。 最後に、これらの手法についてローカルで計測した結果も記述しています。なお、以下に出てくる質問や文書の例は、説明のために作ったものです。 全体の流れ 前提として、ベクトルDBとインデックスはDatabricksのAI Searchというサービスにてホストされており、バックエンドサーバーからベクトルDBに対して直接リクエストするような形式を取っています。 またインデックスは、文書を段落レベルの単位(チャンク)に分けた上で埋め込みベクトルを生成しています。 処理の全体像は以下の通りです。 検索方式を決めるまで インターンで一番やりがいを感じたのは、この検索方式を決めるところでした。自分の考えを特に多く入れられた部分でもあるので、どう考えて今の形に落ち着いたのかを書きたいと思います。検索方式は何度か変わり、HYBRIDから始めてANNに移り、最後に両方を組み合わせる形になりました。 欲しかった候補 例えば「ハンバーグの焼き時間は?」と聞かれたとします。候補に来てほしいのは、「ハンバーグの作り方」のようなハンバーグについての文書です。「鶏むね肉のソテーの焼き時間」のような、別の料理の焼き時間の文書ではありません。 ベクトルの近さだけで探すと、別の料理の文書でも質問と同じく「焼き時間」について書かれていれば上位に来るおそれがありました。そこで最初は、HYBRIDを使うことを考えました。HYBRIDはベクトルの近さとキーワードの一致を合わせて並べるので、「ハンバーグ」という言葉が入った文書が上位に来やすく、聞かれている対象を外しにくくなります。 HYBRID単体では根拠となる回答があるかを判断できない HYBRIDを使う場合、scoreが閾値より低ければ「答えが無い」と判定するつもりでした。ところが、設計を詰める中で、HYBRIDではこの判定が難しいことに気づきました。 HYBRIDは、ベクトル検索とキーワード検索の結果を、Reciprocal Rank Fusion(RRF)で1つにまとめる検索です。DatabricksのAI Searchでは、チャンクxについて次の値を計算します(rankは、それぞれの検索でのxの順位で、1位を1と数えます。60はRRFの定数kで、AI Searchでは60です)。 AI Searchが返すscoreは、この値を取りうる最大値で割り、最大が1になるようにしたものです。一定の値で割るだけなので、scoreが順位で決まることは変わりません。 このように、HYBRIDのscoreは2つの検索の 順位 から計算するので、検索語とチャンクがどれだけ近いかの目安にはなりません。インデックスの文書がどれも質問と関係なくても、上位の文書には、順位に応じた高いscoreが付きます。そのためscoreを見ても、答えのある文書があるのか、関係のない文書しか無いのかを区別できません。 ANNと、見出しを足した埋め込み 次に、ANN(ベクトルの近さだけで探す検索)に切り替えました。AI SearchのANNは、検索語のベクトルqとチャンクのベクトルxの距離(ユークリッド距離)から、次の式でscoreを計算します。 ‖q − x‖ はqとxのユークリッド距離です。距離が0ならscoreは1になり、遠いほど小さくなります。使っているOpenAIの埋め込みは長さ1に正規化されているので、距離は最大でも2になり、実際のscoreは0.2〜1の間に収まります。 HYBRIDでは、どの文書も関係なくても上位に高いscoreが付きますが、ANNでは、どのチャンクも遠ければscoreも低いままです。ANNのscoreは検索語とチャンクの近さそのものなので、閾値を付けられます。残った心配は、聞かれている対象を外すことです。この点をインデックスの側で補うために、埋め込む文を工夫しました。 RAGでは、文書を段落のような小さな単位(チャンク)に分け、チャンクごとにベクトルにするのが一般的です。文書全体を1つのベクトルにすると、そのベクトルはいろいろな関心事を含み、細かい質問に対応しくくなってしまいます。 ただ、チャンクに分けると、今度は文脈が抜け落ちます。文書の中ではタイトルや見出しが何について書かれているかを示していますが、段落だけを切り出すと、その手がかりが無くなります。例えば、次の段落があるとします。 中火で 5 分ほど焼き、裏返してふたをします。 この段落だけでは、何を焼くのかが分かりません。同じ文書のタイトルが「ハンバーグの基本の作り方」で、段落が「焼き方」という見出しの下にあれば、「ハンバーグは中火で5分ほど焼く」という内容だと分かります。もし段落だけをベクトルにすると、「ハンバーグ」や「焼き方」で聞かれてもこの段落はヒットしないでしょう。 そこで、埋め込む文には、チャンクの本文の前に、文書のタイトルと、そのチャンクが属する見出しをつなげて入れました。 文書のタイトル / 見出し / チャンクの本文 見出しが何階層かある文書では、チャンクのすぐ上にある見出しだけでなく、それより上の階層の見出しまでつなげます。すぐ上の見出しだけでは、上の階層が示す文脈(「作り方」なのか「アレンジ」なのか)が抜け落ちるからです。 HYBRIDとANNの並行 ただ、見出しを足しても、ANNが見ているのはあくまでベクトルの近さです。固有の料理名や略語のように、言葉がそのまま一致するかどうかが決め手になる質問では、近さだけだと、似た話題の別の文書が上位に来るおそれが残りました。一方で、「答えが無い」を見分けるための閾値には、近さそのものを表すANNのscoreが要ります。 そこで、自分が出した案をもとにチームで相談して、2つを組み合わせる形に決めました。並べ替えはキーワードの一致も使えるHYBRIDに、明らかに遠い候補を閾値で落とすのは近さそのものをscoreで返すANNに任せます。インデックスに入れる見出しの工夫は、そのまま残しています。 具体的には、2つを並行に問い合わせ、HYBRIDの結果のうち、ANNの閾値を通ったチャンクを持つ文書のものだけを残します。次のコードは、説明のために簡略化したものです。 // HYBRID と ANN(閾値つき)を並行に問い合わせる。片方が失敗したら、もう片方も止める var annHits, hybridHits []*Hit g, gctx := errgroup.WithContext(ctx) g.Go( func () (err error ) { annHits, err = index.Query(gctx, annQuery); return }) g.Go( func () (err error ) { hybridHits, err = index.Query(gctx, hybridQuery); return }) if err := g.Wait(); err != nil { return nil , err } // annQuery に閾値を付けてあるので、annHits には閾値を通ったチャンクだけが入っている。 // そのチャンクを持つ文書を「近い」とみなす near := map [ int64 ] struct {}{} for _, h := range annHits { near[h.DocID] = struct {}{} } // HYBRID の結果は、近い文書のものだけを残す(並び順は HYBRID のまま) hits := make ([]*Hit, 0 , len (hybridHits)) for _, h := range hybridHits { if _, ok := near[h.DocID]; ok { hits = append (hits, h) } } 文書の単位で照らし合わせているのは、ANNとHYBRIDでは、同じ文書でも別のチャンクが返ってくることがあるからです。 自分で作ったベクトルを入れるインデックスでもHYBRIDを使えるかは、Databricksのドキュメントからは分からなかったので、ローカルで試して動くことを確かめました。ただ、少数の質問でANNだけの場合と比べたところ、最終の結果にはほとんど差が出ませんでした(後の「ローカルで試した結果」で詳しく記述しています)。HYBRIDを足した効果はまだ言い切れないので、設定でANNだけの形にも切り替えられるようにしました。 文書ごとのまとめ インデックスから返ってくるのはチャンクの単位です。そこで、同じ文書から来たチャンクを1つの候補に束ね、文書のscoreは、その文書のチャンクの(HYBRIDの)scoreの最大値にします。このscoreは候補を並べる順番にだけ使い、上位10文書を判定にかけます。判定を通った文書からも、この順番のまま上位3文書を選びます。 同じ話題だが答えていない文書 ここまでで、明らかに意味が異なる候補は落とすことができます。 ただ、同じ話題だが質問の回答にはならない文書を完全に落とすことはまだできていません。 例えば「ハンバーグの焼き時間は?」と聞かれ、インデックスには「ハンバーグの由来」を紹介する文書しかないとします。この文書は焼き時間には答えていませんが、ハンバーグについて書かれているので、チャンクのベクトルは検索語のベクトルに近くなります。ANNの閾値を厳しくすれば落とせるかもしれませんが、そうすると今度は、答えている文書まで落ちてしまいます。 考えた案は次の3つでした。 案 結論 理由 ANNの閾値を厳しくする 見送り 答えている文書まで落ちる。閾値だけでは、同じ話題だが答えていない文書を分けられない 質問と文書の主題が一致するかを見る 見送り 主題が一致していても、聞かれたこと(焼き時間)が書かれているとは限らない 文書ごとにAIモデルで判定する 採用 この文書はこの質問に答えているかを、直接聞ける 最終的にチームで決めたのは、ANNの閾値で明らかに遠いものを落とし、残った上位10文書をAIモデルで判定する形です。 従来のLLMでは推論速度に少なからず影響が出るのが懸念だったのですが、Jevを使用すれば比較的影響を少なく実装できると思い、今回試してみることになりました。 Jevで文書ごとに判定する Jevとnoul型 判定にはTypeSafeのJevを使いました。Jevは従来のLLMのようなテキスト生成をするのではなく、高速かつ型安全なデータ構造を出力することに特化した新しいAIモデルです。今回はCloudflareのWorkers AIのAPIから呼びだしています。 Jevに渡す問い( questions )の型にはいくつかあり、今回は noul を選択しました。 noul は、trueとfalseの基準を渡すと、trueに当たる確率を0〜1の数で返します。答えているかどうかを確率で受け取れるので、判定の閾値を後から調整できます。 判定に渡すもの 1文書ごとに、次の形でリクエストを送ります(文言は簡略化し、一般的な言い方に直しています)。 { " model ": " typesafe/jev ", " input ": { " state ": " 質問: ハンバーグの焼き時間は? \n タイトル: …… \n\n [大見出し / 中見出し] \n 本文の段落…… \n\n [大見出し / 中見出し] \n 本文の段落…… ", " questions ": { " answers_question ": { " type ": " noul ", " instructions ": " You decide whether the blocks in the state answer the user's question. ... Use the headings only to understand where each block sits; do not count a heading alone as an answer. ", " criteria ": { " true ": " The text of the blocks alone answers the question, at least in part. ", " false ": " The blocks are about the same topic but do not answer the specific thing that was asked, or they are about something else. " } } } } } state に判定の材料(質問と、見出し付きのチャンク)を、 questions に何を判定させるかを入れます。 工夫したのは次の4点です。 渡すのは、その文書のチャンクのうち、検索のscoreが高い上位3つだけです。文書全体を渡すと長くなり、判定も遅くなるからです。答えが書かれた段落は質問と内容が近いので、上位3つに入っているはずだと考えました チャンクには見出しも付けて渡しますが、見出しはその段落が文書のどこにあるかを示す手がかりとしてだけ使うよう、指示に書きました。例えば見出しに「焼き時間」とあっても、本文に焼き時間が書かれていなければ、答えているとは数えません falseの基準には、「同じ話題だが、聞かれたことには答えていない」場合をはっきり書きました。ANNの閾値(ベクトルの近さ)で落とせずに残るのは、まさにこういう文書だからです 広い質問は別に扱います。「ハンバーグについて教えて」のような質問なら、ハンバーグを説明している文書は答えていると数える、と指示に書きました。これが無いと、広い質問では「具体的なことに答えていない」として落ちかねません 返ってきた確率が判定の閾値(今は仮で0.5)以上なら、答えているとします。 ローカルで試すと、同じ文書でもANNとHYBRIDでは当たるチャンクが違い、判定に渡す3チャンクが変わる文書が58件ありました。それらの文書で、ANNだけのときとHYBRID+ANNのときのJevの確率の差は、中央値で0.01でした。渡すチャンクが多少変わっても、判定はほとんど変わらないようです。ただし、差が最大で0.61開いたものもあり、0.5をまたいで、通るかどうかが逆になったものが2件ありました。 並行判定とやり直し 10文書は並行して判定します。1文書ずつ順番に呼ぶと、待ち時間が文書の数だけ積み重なるからです。どれか1つの判定でエラーが起きても、その文書を判定できなかったものとして扱うだけで、ほかの文書の判定はそのまま続けます。前処理のHYBRIDとANNの問い合わせでは、片方が失敗したら両方を止めるerrgroupを使いましたが、ここでは1つの失敗でほかを止めたくないので、sync.WaitGroupを使っています。 // 1 文書の判定結果。判定できなかったときは Err に入れ、他の文書は止めない type Verdict struct { Probability float64 // 「答えている」の確率 Err error } // 文書ごとの失敗は Verdict.Err で返すので、ここでは常に nil を返す func (j *JevJudge) Judge(ctx context.Context, query string , docs []*Candidate) ([]*Verdict, error ) { verdicts := make ([]*Verdict, len (docs)) var wg sync.WaitGroup for i, d := range docs { wg.Go( func () { // Go 1.25 の sync.WaitGroup.Go p, err := j.judgeOne(ctx, query, d) verdicts[i] = &Verdict{Probability: p, Err: err} }) } wg.Wait() return verdicts, nil } Jevの判定1回にかかる時間は中央値で247 msでした。並行にしたので、判定の段階は数百msで終わっています。 判定でエラーが起きた文書は、最後に選ぶ3文書がまだそろわず、時間も残っているときに限り、1回だけ判定をやり直します。一方、答えていないと判定された文書はやり直しません。エラーと違って、判定そのものはできているからです。 判定の結果から、返すものは次の3通りに決まります。 判定の結果 返すもの 答えている文書が1つ以上ある その文書(上位3つまで) すべての文書が「答えていない」と判定された 「答えが無い」 答えている文書が無く、判定できなかった文書が残った エラー 3つ目を「答えが無い」にしないのは、判定できなかった文書に答えが書かれているかもしれないからです。その場合に「答えが無い」と返すと、実際には答えがあるのに無いと伝えてしまいます。 閾値の決め方と、ローカルで試した結果 ANNの閾値と判定の閾値は、今は仮の値です。どちらの値にも同じトレードオフがあります。厳しくすると、答えが無い質問では正しく0件にできる代わりに、答えがある質問では正しい文書まで落としてしまいます。 そのため、評価では次の2つを分けて測ります。 指標 測るもの 検索の正しさ 正解の文書が上位3文書に入るか 答えが無い質問 答えが無い質問で、正しく0件になるか RAGの評価では、LLMが渡された文書のとおりに答えを書けているかを測ることがよくあります。ただ、これだけでは検索の失敗が見えません。 例えば「ハンバーグの焼き時間は?」に対して、検索が鶏むね肉のソテーの文書を渡してしまったとします。LLMがその文書のとおりに「片面3分ずつです」と答えれば、文書に忠実なので、この測り方では満点になります。しかし、ユーザーが聞いたのはハンバーグの焼き時間なので、答えとしては間違いです。そのため、検索が正しい文書を選べているか(上の表の「検索の正しさ」)は、別に測る必要があります。 評価に使う質問には、答えが文書に書かれている質問、どの文書にも答えが無い質問、答えが一部にしか書かれていない質問の3種類を混ぜます。なお、評価のために外部のサービスであるJevへ送るのは、ユーザーが実際に送った質問ではなく、評価用に作った質問だけです。 ローカルで試した結果 評価用に作った15問、Jev判定なしと判定ありの差を比べました。Jevで判定すると、答えが無い質問の多くで、正しく「答えが無い」と返せるようになりました。その代わり、答えがある質問12問のうち1〜2問では、文書を全部落としてしまいました。 答えが無い質問に文書を返してしまった(5問中) 答えがある質問で文書を全部落とした(12問中) 判定なし 5 0 Jevで判定(ANNだけ) 1 1 Jevで判定(HYBRID+ANN) 1 2 どちらの列も、数が少ないほど良い結果です。 今回はどの組み合わせでもANNの閾値を0にしていたので、閾値では何も落ちません。そのため判定なしの場合は、どんな質問にも上位3文書がそのまま返り、答えがある質問では12問すべてで文書が残りました。一方で、「明日の東京の天気は?」「スマホの電池を長持ちさせる方法は?」のような、インデックスのどの文書とも関係のない質問にも文書を返してしまいました。 Jevで判定すると、答えが無い質問5問のうち4問で、正しく「答えが無い」と返せました。一方、答えがある質問でも、文書が1つも残らないものが、ANNだけで1問、HYBRID+ANNで2問ありました。そのうちの1つは、「薄力粉と強力粉の違いは?」のような、2つのものの違いを聞く質問です。インデックスには薄力粉の文書と強力粉の文書が別々にあり、どちらも1つだけでは違いに答えていないので、両方とも落ちました。文書を1つずつ判定する作りの弱みで、複数の文書にまたがる質問には別の扱いが要ります。 ANNだけとHYBRID+ANNの差は、今回はほとんど出ませんでした。判定にかける上位10文書は、一部が入れ替わっているだけでした。それでも、最終の結果が違ったのは1問だけで、それも判定に渡したチャンクの違いによるものでした。 推論速度の中央値は次のとおりです。 段階 時間(中央値) 検索語の埋め込み 145 ms 検索(ANNだけ) 213 ms 検索(HYBRID+ANN) 242 ms HYBRID+ANNは2本を並行に問い合わせるので、ANNだけとの差は30 msほどです。判定の段階は、前に書いたとおり数百msです。 この結果には、次の注意点があります。 質問は評価用に作ったもので、実際のユーザーの質問ではありません 閾値は仮の値です(判定は0.5、ANNは0)。ANNの閾値で遠い候補を落とす効果は、まだ測れていません 学んだこと HYBRIDのscoreは「近さ」ではない。 順位から作った値は並べ替えには使えても、答えの有無を閾値で見分けるのには使えません。scoreが何から計算されているかを確かめてから使う必要がある。 ベクトルの近さで分かるのは「意味が近いか」まで。 「聞かれたことに答えているか」は、近さとは別の問い。 判定の結果は確率で受け取る。 はい/いいえで受け取ると、閾値を動かせません。Jevの noul のように確率で返すものを使えば、評価を見ながら調整できます 文書ごとの判定は、複数の文書にまたがる質問に弱い。 「AとBの違い」のような質問は、1つの文書だけでは答えられないので、文書が全部落ちることがあります おわりに RAGの「根拠となる答えが無い」ことをどう見分けるかは、作る前に思っていたよりずっと奥が深い問題でした。 1か月のインターンで、検索方式の選び方から判定の仕組みまで自分で案を考えて提案し、チームと相談しながら形にできました。評価では、判定を足すと、答えが無い質問の多くを排除できる傾向も見えました。 一方で、答えがある質問でも、文書を全部落とす場合があることや、複数の文書にまたがる質問に弱いことも分かりました。さらに質問を増やした評価は、これからの課題です。 お忙しい中、レビューをしてくださり、相談にも乗ってくださった 配属先のチームの皆さん、特に岩崎さん、ありがとうございました。
本記事は 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 サーバーに転送されます。 検査の境界を理解する:エッジとオリジン Amazon CloudFront では、AWS WAF は CloudFront Functions や Lambda@Edge が実行される前にリクエストを評価します ( AWS WAF のリクエスト評価順序   ) 。そのため、 AWS WAF はエッジ関数が追加したヘッダーを参照できず、呼び出し元が最初に送信した内容だけを参照します。 MCP トラフィックの場合、利用できるフィールドは MCP-Protocol-Version 、 Mcp-Method 、 Mcp-Name 、 Authorization ヘッダーの 4 つです。 AWS WAF は最初の 3 つに対して直接マッチングやレート制限を行えます。一方、ベアラートークンの検証やデコードはできないため、トークンの検証はオリジンで行います。 この境界によって、セキュリティ対策は 2 つに分かれます。プロトコルやリクエスト形式に基づく制御 ( バージョンチェック、ツールの許可リスト、引数の検査、 SSRF パターンの検出、 SQL インジェクションの検出、 IP ベースのレート制限 ) は AWS WAF で行います。アイデンティティに基づく制御 ( ユーザーごとのレート制限、ロールに基づくツールへのアクセス、トークンの検証、きめ細かな認可 ) は、ベアラートークンを検証して呼び出し元が既知のプリンシパルであることを確認したあとに、 MCP サーバーで行います。 AWS WAF のルールスタック AWS WAF には、ベースラインの保護に向けたルールの推奨事項と、ルールの順序に関するベストプラクティスがあります。ベースラインとしてこれらのパターンに従うことをおすすめします。こちらの ブログ で、参考情報とあわせて詳しく説明しています。各ルールの JSON 内にある 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 で各呼び出し元のアイデンティティにラベルを付ける エージェントによるトラフィックは、マシンによるトラフィックです。マシンの呼び出し元に付与できるもっとも強力なシグナルは、正規化されたアイデンティティラベルです。これにより、「どのエージェントが呼び出しているのか」と「どのツールを呼び出しているのか」を組み合わせて扱えます。この組み合わせこそがこのルールの特長であり、他のボディを検査するすべてのルールより前に Bot Control を配置する理由です。 AWS WAF Bot Control は、各リクエストにラベルを付与します。 MCP に関連するラベルのファミリーは、 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 エンドポイントでは不審なものとして扱う Bot Control の共通の保護レベルには、デフォルトのアクションが Block に設定されたサブルールがいくつか含まれています。そのうち 5 つは、正当なエージェントのトラフィックに対して誤検知を起こすため、 MCP エンドポイントでは Count にオーバーライドする必要があります。 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 ではなくデータセンターから送信されます。そのため、このルールをそのまま有効にすると、正当な呼び出し元のほとんどがブロックされてしまいます。 これら 5 つをオーバーライドしても、Bot Control のそれ以外の保護機能は維持されます。具体的には、大量のリクエストでエンドポイントを攻撃するボットを止める Targeted の大量リクエスト向けルール ( TGT_VolumetricSession 、 TGT_VolumetricIpTokenAbsent ) 、スクレイパーや SEO カテゴリのブロック、そして後続のアイデンティティとツールを組み合わせたルールの入力となる Web Bot Auth ( WBA ) の判定です。グループ全体に対してウェブ ACL レベルで OverrideAction: Count を設定するのではなく、マネージドルールグループのステートメントで RuleActionOverrides を使用してください。前者の方法では、ルールグループ全体が Count になり、Bot Control の利点が失われてしまうからです。 ルールのロジック: 検査レベルを COMMON にした 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" } } 5 つのサブルールはすべて、オーバーライド後もラベル ( 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:検査の前にサイズの大きすぎるリクエストボディをブロックする CloudFront における AWS WAF のボディ検査のデフォルトの上限は 16 KB です。ウェブ ACL の AssociationConfig を使用すると、この上限を 64 KB まで引き上げられます。本記事では上限を 32 KB に引き上げています。これは、一般的な MCP ツールの引数には十分なサイズでありつつ、過大なペイロードを招かない値です。 注意: AWS WAF を Application Load Balancer (ALB) に関連付けた場合、ボディ検査の上限は 8 KB に制限されており、引き上げることはできません。 MCP エンドポイントの前段に ALB を配置する場合は、ツールの引数を 8 KB 以下に抑えるか、ALB の前段に CloudFront ディストリビューションを配置して AWS WAF をそちらに関連付けてください。 設定した上限を超える部分は、各ルールの OversizeHandling の設定に従って処理されます。ボディを検査するすべてのルールで OversizeHandling: MATCH を設定すると安全ではありますが、ノイズが多くなります。正当なサイズの大きいボディが、すべてのルールに引っかかってしまうためです。代わりに、最初の段階で一度だけフェイルクローズする方針を取ります。具体的には、 32 KB を超えるボディを単一のサイズ制約ルールで拒否します。この設定があれば、サイズの大きすぎるボディは後続のルールに到達しないため、後続のすべてのルールで OversizeHandling: NO_MATCH を安全に使用できます。 この事前チェックを 32 KB で機能させるには、ウェブ ACL のボディ検査の上限を 32 KB 以上に設定する必要があります。デフォルトの 16 KB のままだと、16 KB から 32 KB の間のリクエストで 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:呼び出し可能なツールを許可リストで制限する 組織が 20 個のツールを公開していたとしても、ツールのレイヤーで最小権限の原則を適用するのであれば、エージェントには特定のタスクに必要なツールの呼び出しだけを許可すべきです。正規表現パターンセットを使用して呼び出し可能な範囲を制限しておけば、プロンプトインジェクションによってエージェントが execute_shell や delete_all_records を呼び出そうとしても、リクエストはエッジで 403 として止められ、サーバーには到達しません。 Mcp-Name はファーストクラスのヘッダーであるため、このルールはボディを解析しない、純粋なヘッダールールになります。 このルールは、 動的ラベル補間 を使用して、AWS WAF のリクエスト ID を含む JSON 形式の 403 レスポンスボディを返します。 ${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 をブロックする 多くの MCP サーバーは、データベースにクエリを実行するツールを公開しています。 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 引数に破壊的なキーワードが含まれている場合にのみ発動します。 ルールのロジック ( 2 つのルールの組み合わせ ) : 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 を検出してブロックする 呼び出し元が指定した宛先に対してデータを取得 (fetch) 、名前解決 (resolve) 、送信 (post) する MCP ツールは、 SSRF の攻撃対象になりやすいものです。内部 IP 、クラウドのメタデータエンドポイント、サービス内部の DNS 名を指定する引数は、よく見られるパターンです。ボディを検査する単一のルールで、一般的なパターンをカバーできます。具体的には、インスタンスメタデータサービス (IMDS) のエンドポイント ( 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" } } ここで 2 つの注意点を挙げておきます。 1 つ目に、このルールは多層防御の一部であり、 SSRF に対する主要な対策ではありません。 AWS WAF のテキスト変換は 1 回しか実行されないため、 10 進数や 8 進数による IP のエンコーディング、 DNS リバインディング、マルチパスエンコーディングは、このルールをすり抜けてしまいます。主要な対策はネットワークレベルで行います。ツールを実行する環境で IMDSv2 を必須にし、セキュリティグループや VPC エンドポイントポリシーで送信先を制限してください。 2 つ目に、正当な理由でプライベートアドレスと通信するツール ( たとえば、 Kubernetes クラスターのイントロスペクションツール ) がある場合は、ラベルのマッチングを使って除外してください。グローバルなルールを緩めるよりも、「この特定のツールは .svc.cluster.local への到達を許可する」と定義する方が、判断が容易になります。 ユースケース 7:クエリ形式の入力に含まれる SQL インジェクションを検出する AWS WAF に SQL インジェクションの検出を組み込む方法は 2 つあります。AWS マネージドルールグループの AWSManagedRulesSQLiRuleSet を使用する方法と、組み込みのプリミティブである SqliMatchStatement を使用する方法です。マネージドルールグループは生のボディをルールセットと照合して検査するため、HTML フォームや URL エンコードされた API に対しては適切なデフォルトの選択肢です。しかし MCP の場合、ペイロードは JSON であり、脆弱性のあるフィールド ( sql 、 query 、 filter など ) は $.params.arguments 内の文字列です。JSON エンコードされても有効なままの典型的なインジェクションペイロードは、生のボディの検査をすり抜ける可能性があります。たとえば、値として埋め込まれた 1 UNION SELECT username, password FROM users は周囲の JSON 構造によって偽装されてしまいます。 MCP において信頼性の高いアプローチは、 AWS WAF の SQL パーサーを 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 ) を検出しつつ、通常のフィルター値にはフラグを立てません。 これに関連して、ここで述べておくべき設計上のポイントがあります。ツールが呼び出し元から任意の生の SQL を受け付ける場合、AWS WAF の感度設定では「安全な 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) ブロックルールは全体像の半分にすぎません。 AWS WAF のログ記録 ( Amazon CloudWatch Logs、Amazon S3、Amazon Data Firehose への出力 ) を使用すると、リクエストごとの記録を取得できます。レコードにはヘッダー、終了ルール、適用されたラベル、最終アクションが含まれます。 Mcp-Method と Mcp-Name は httpRequest.headers に含まれているため、各ログエントリを見れば、どのツールが呼び出され、それがどう処理されたのかがわかります。 ツール名の抽出: AWS WAF のログでは、 httpRequest.headers は {name, value} オブジェクトの配列です。次の Amazon CloudWatch Logs Insights のクエリは、ツールごとに抽出・集計します。 fields @timestamp, action, terminatingRuleId, @message | parse @message /"name":"[Mm][Cc][Pp]-[Nn][Aa][Mm][Ee]","value":"(?<tool>[^"]+)"/ | filter ispresent(tool) | stats count() as invocations by tool | sort invocations desc | limit 10 図 2. 呼び出し回数の多いツールを表示するサンプルダッシュボード ツールの列挙攻撃の検出: 正常に動作している MCP クライアントは、2〜3 個のツールを予測可能なパターンで繰り返し呼び出します。プロンプトインジェクションによってエージェントが能力探索へと誘導されると、送信元 IP ごとの呼び出しツールの種類数が急増します。数分のうちに 3 種類から 15 種類へと呼び出すツールが増えた IP は、振る舞いの形状が変化しています。このクエリでは、承認済みツールに対するパラメーターの改ざんのような、より巧妙な手法は検出できません。それでも、単なるスループットではなく意図の変化を捉えられるため、早期警告として役立ちます。 次のクエリは、5 種類を超えるツールを呼び出している IP を抽出します。 parse @message /"name":"[Mm][Cc][Pp]-[Nn][Aa][Mm][Ee]","value":"(?<tool>[^"]+)"/ | parse @message /"clientIp":"(?<ip>[^"]+)"/ | filter ispresent(tool) | stats countDistinct(tool) as tools_called_by_ip, count() as total_reqs_by_ip by ip | filter tools_called_by_ip > 5 | sort tools_called_by_ip desc 図 3. 送信元 IP ごとの呼び出しツールの種類数。単一の IP から呼び出されるツールの種類数が急増している場合、プロンプトインジェクションによる能力探索が行われている可能性があります。 振る舞いのベースライン:エージェントのアイデンティティ × ツールのフィンガープリント: Bot Control の CategoryAI ルールは、エンドポイントを呼び出している検証済みの AI ボットを検出します。通常の運用では、検証済みのエージェントと、そのエージェントが呼び出すツールの関係は安定しています。未検証またはなりすましのエージェントには bot:name ラベルが付与されないため、この方法ではそれらを検出できません。それでも、昨日まで存在しなかった新しい ( エージェント、ツール ) ペアが出現した場合、上流で何かが変化したことを示すため、有用なベースラインになります。次のクエリは、この対応関係を作成します。 parse @message /awswaf:managed:aws:bot-control:bot:category:(?<category>[^"]+)/ | parse @message /awswaf:managed:aws:bot-control:bot:name:(?<bot_name>[^"]+)/ | parse @message /"name":"[Mm][Cc][Pp]-[Nn][Aa][Mm][Ee]","value":"(?<tool>[^"]+)"/ | filter category = "ai" and ispresent(tool) | stats count() as calls by bot_name, tool | sort calls desc 図 4. MCP エンドポイントへの検証済みと未検証のエージェントのトラフィックを示す AI トラフィック分析ダッシュボード 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 のブロックは、内部のインフラストラクチャへの到達が実際に試みられていることを示します。 未承認ツールのブロックの急増 :プロンプトインジェクションの可能性があるイベントとして調査します。ブロックされたツール呼び出しが急増している場合、エージェントが操作されている可能性があります。 レート制限への継続的な到達 :人によるレビューにエスカレーションします。グローバルなレート上限に継続的に達している呼び出し元は、手動で調査すべきほど異常な動作をしています。 安全にデプロイする すべてのルールは、Block を有効にする前に Count モードでデプロイしてください。Count モードでは、トラフィックをブロックせずにリクエストを評価し、一致した結果を Amazon CloudWatch のメトリクスに記録します ( テストとチューニング ) 。サンプリングされたリクエストを確認して誤検知を特定し、スコープダウンステートメントやラベルに基づく除外でチューニングします ( サンプリングされたリクエスト ) 。影響を切り分け、必要に応じて個別のルールを元に戻せるように、ルールは 1 つずつ Block に切り替えてください。Block に切り替えたあとも、 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 を参照してください。 著者について Jaiganesh Girinathan Jaiganesh Girinathan は、AWS のコンテンツ配信ネットワークとエッジコンピューティングの機能を専門とするプリンシパルエッジスペシャリストソリューションアーキテクトです。過去 20 年にわたり世界中のメディア業界のお客様を支援し、プラットフォームのモダナイズと拡張に取り組んできました。お客様の重要なニーズに応えるソリューションの構築に情熱を注いでいます。仕事以外では、たいてい星空を眺めています。 Harith Shantan Gaddamanugu Harith は、AWS のシニアエッジスペシャリストソリューションアーキテクトです。世界中の何百万人ものユーザーが利用する、重要なインフラストラクチャとセキュリティのソリューションを設計しています。クラウドの境界防御とウェブの高速化に関する 10 年の専門知識を活かし、レジリエントなアーキテクチャを構築する大企業を支援しています。仕事以外では、家族とのハイキングや風景写真の撮影を楽しんでいます。 Kaustubh Phatak Kaustubh は、AI/ML システムとエンタープライズ向けセキュリティソリューションを専門とするプロダクトリーダーです。AI を活用したプロダクトを大規模に展開する部門横断チームを率い、セキュリティアーキテクトや CISO と緊密に連携しながら、AI のイノベーションとサイバーセキュリティリスクが交わる領域の課題に取り組んできました。とくに、従来のフレームワークが通用しない新しい技術領域において、複雑な技術的機能をビジネス価値へと結びつけることに注力しています。 Ted Middleton Ted Middleton は、AWS のエッジスペシャリストソリューションアーキテクトチームのグローバルリーダーであり、以前は CloudFront チームのプリンシパルプロダクトマネージャーを務めていました。CDN とエッジサービスにおいて 20 年以上の経験を持っています。 翻訳はプロフェッショナルサービスの鈴木(隆)が担当しました。
この記事は 2026 年 8 月 27 日に公開された Building audio provenance for AI-generated content at the source with AWS を翻訳したものです。 AI 生成オーディオは、世界中のメディア・サプライチェーンにますます組み込まれています。欧州市場向けにこのコンテンツを生成する場合、 EU AI 法第 50 条 に基づき、2026 年 12 月 2 日までにコンプライアンス要件を満たす必要があります。本記事では、合成の段階で AI 生成オーディオに暗号技術を用いた来歴情報 (プロヴェナンス) の透かしを埋め込み、ライブストリーミングメディアパイプラインを通じて検証可能な管理の連鎖 (チェーン・オブ・カストディ) を保持する、 Amazon Web Services (AWS) のパイプライン「SENTINEL」の構築方法について解説します。本記事では、アーキテクチャ、AWS サービスの実装、デプロイ手法、および SENTINEL の構築中に直面したエンジニアリング上のトレードオフについて取り上げます。 なぜ 2026 年に急務になったのか? EU AI 法第 50 条は、欧州連合 (EU) で提供される AI 生成コンテンツに対し、機械可読な来歴情報の表示を義務付けています。2026 年 8 月 2 日より後に提供を開始したシステムは、直ちに準拠する必要があります。既存のシステムの期限は 2026 年 12 月 2 日です。違反した場合、最大 1,500 万ユーロまたは全世界売上高の 3% のいずれか高い方の制裁金が科されます。 Universal Music Group と Warner Music Group による業界の著作権和解は、AI 生成音楽に帰属メタデータが必要であることを示しています。 業界では、NextGen TV 放送向けの VP1 透かし形式を規定する ATSC A/334 規格が認知されています。 2026 年 7 月 9 日、欧州委員会は AI 生成コンテンツの透明性に関する行動規範 (Code of Practice on Transparency of AI-Generated Content) を、第 50 条への準拠を示すうえで適切なものとして承認しました。最終版の規範は、プロバイダーに対し、(1) 一般に利用可能な検出ソリューション、(2) 知覚できない透かしを伴うデジタル署名付きメタデータ、(3) 検出のために提出されたコンテンツを保持しない取り扱い (ゼロリテンション)、(4) 2027 年 2 月 2 日を期限とする相互運用性などの最低限の義務を定めています。これらの義務により、オーディオプロバイダーに求められる技術要件は、2025 年 12 月の第 1 次草案で想定されていたものより厳しくなっています。 オーディオの来歴情報に、新しい技術は必要ありません。業界にはすでに必要な要素がそろっており、SENTINEL はそれらを AWS 上で稼働するパイプラインとして組み上げています。 AudioSeal は、MIT ライセンスのオープンソースの透かし技術で、堅牢で知覚できないという特長があります。SENTINEL は AudioSeal を使って透かしを生成し、埋め込みを合成の段階に移すことで、ペイロードを生成モデルと、 Amazon Bedrock Guardrails による安全ポリシーの評価に結び付けます。 C2PA 2.0 は、暗号署名されたオープンなコンテンツクレデンシャルのマニフェストを提供します。SENTINEL はこのマニフェストを作成・署名し、配信チェーン全体に引き継ぎます。さらに、C2PA の「ソフトバインディング」を備えた Durable Content Credentials は、SENTINEL が実装する復元の仕組みそのものを定義しています。これは、途中の処理で埋め込みメタデータが削除された場合に、不可視の透かしを使ってリポジトリからマニフェストを復元する仕組みです。 SynthID は、生成時の埋め込みと C2PA を組み合わせる同じ方向性を、単一ベンダーの製品群の中で大規模に実現しています。SENTINEL は、このモデルをサードパーティのあらゆる AWS パイプラインで利用できるようにします。 これらの構成要素を組み合わせたものが、Durable Content Credentials です。これは、署名付きのマニフェストと、処理を経ても残るソフトバインディングで構成されます。AI 生成オーディオがライブ制作パイプラインを流れるとき、オーディオには 2 つの来歴情報のアセットが付随します。C2PA マニフェスト (メディアコンテナに添付される署名付きメタデータ) と、AudioSeal の透かし (オーディオ波形に直接埋め込まれる 16 ビットのペイロード) です。マニフェストは、メタデータのパススルーを実装していないトランスコーダーによって削除されることがよくあります。一方、透かしはオーディオ信号そのものの中にあるため、コーデックのトランスコード、再量子化、±6 dB のゲイン変更、10% へのクロッピングを経ても残ります。ただし、タイムストレッチや中程度の加算ノイズには耐えられません。SENTINEL は、残った透かしをソフトバインディングとして使い、削除されたマニフェストを来歴情報のレジストリから復元します。こうして、ライブオーディオ向けの耐久性のある C2PA コンテンツクレデンシャルを AWS 上で実装しています。 この実装では、AI コンテンツを Amazon Polly (ニューラルエンジン) で合成し、 AWS Elemental MediaLive でトランスコードし、 AWS Elemental MediaPackage で HTTP Live Streaming (HLS) セグメントに再パッケージします。さらに、 AWS Elemental MediaTailor のサーバーサイド広告挿入 (SSAI) で加工し、 Amazon CloudFront で配信します。また、Amazon Bedrock Guardrails がポリシーの評価を行います。 ソリューションの概要 SENTINEL は、4 つの機能プレーンにまたがって動作します。 生成 (プレーン 1) – AI による合成の間に、パルス符号変調 (PCM) の生のオーディオに透かしを埋め込む 変換 (プレーン 2) – メディア処理の各段階で、管理の連鎖を示す署名付きアサーションを付与する レジストリ (プレーン 3) – 組織をまたいだアンカリングを備えた、改ざん検知可能な来歴情報のストレージ 復元 (プレーン 4) – キャプチャした 1 秒以上のオーディオから、来歴情報のチェーン全体を再構築するエッジ API 次の図は、生成から復元まで、オーディオが SENTINEL の 4 つのプレーンをどのように流れるかを示しています。 図 1: SENTINEL の 4 プレーンアーキテクチャ オーディオは合成時に透かしが埋め込まれ (プレーン 1)、メディア処理の各段階でアサーションが付与され (プレーン 2)、来歴情報は改ざん検知可能なレジストリに保存され (プレーン 3)、チェーン全体はエッジで復元できます (プレーン 4)。 共有のコントロールプレーン ( Amazon API Gateway 、 Amazon Cognito 、 Amazon DynamoDB 、 Amazon CloudWatch ) が、すべてのプレーンに対して認証、API 管理、オブザーバビリティを提供します。 プレーン 1: 合成の時点での透かし SENTINEL は、オーディオをプレーン 1 から送り出す前に、各合成リクエストに対して次の 4 つの処理を行います。 安全ポリシーの証明 – Amazon Bedrock Guardrails が、ApplyGuardrail API を使用して、設定されたコンテンツポリシーのカテゴリに照らして合成リクエストを評価します。Guardrails は評価結果 (action フィールドと assessments 配列) を返します。これは通常の API レスポンスであり、署名されたアーティファクトではありません。そこで SENTINEL は、暗号による信頼の連鎖を自ら確立します。まず、ガードレールのポリシーハッシュ (ガードレール ID、バージョン、評価結果を含む、シリアライズした ApplyGuardrail レスポンスの SHA-256) を計算し、そのハッシュに AWS Key Management Service (AWS KMS) (ECDSA_SHA_256) で署名します。KMS キーは、FIPS 140-3 セキュリティレベル 3 の検証を受けた HSM で保護されています。署名されたハッシュは透かしのペイロードの一部となり、そのオーディオが特定の安全ポリシー設定のもとで生成されたことを示す、永続的で検証可能な記録になります。改ざん検知性をもたらすのは、Guardrails ではなくこの署名です。 暗号鍵の導出 – AWS KMS が、管理の連鎖のタイムスタンプに対する FIPS 140-3 レベル 3 の署名とあわせて、GenerateDataKey でテナントごとの一時的なデータキーを導出し、マルチテナントでの暗号学的な分離を維持します。 オーディオの合成 – Amazon Polly (ニューラルエンジン) が、16 kHz の生の PCM 波形を生成します。 ボコーダー層での透かしの埋め込み – 透かしの埋め込み処理は Amazon SageMaker 上で動作し、AudioSeal のジェネレーターを使用します。エンコードやパッケージングの前に、16 ビットのペイロードを生の PCM 波形に埋め込みます。知覚できないこと (不可知覚性) は、AudioSeal が学習した知覚損失と、品質ゲート付きの SENTINEL の適応的な α 探索によって実現しています。安全策として、BS.1387 に由来するマスキング閾値のチェックも実行します。測定した最悪ケースのマージンは +22.3 dB で、467 フレーム中、可聴なフレームは 0 でした (検証済み)。 波形に埋め込まれる透かしは 16 ビットで、アセットの SHA-256 コンテンツハッシュの先頭 2 バイトを、レジストリの検索キーとして保持します。コンテンツハッシュ、モデルの ID、ガードレールのポリシーハッシュ、テナントのキー ID、タイムスタンプ、ナンスを結び付ける来歴情報の 128 ビットの完全なペイロードは、署名付きの C2PA マニフェストに格納され、切り詰めていない完全な値はレジストリに保持されます。透かしは指し示し、マニフェストが証明します。 エンコード前のこの段階で埋め込むことで、オーディオの忠実度が最も高い時点で透かしが組み込まれます。Advanced Audio Coding (AAC) 圧縮から HLS のパケット化、広告挿入まで、その後のすべての処理は、すでに来歴情報のペイロードを含むオーディオに対して行われます。 次の表は、透かし埋め込みのエンジニアリング目標をまとめたものです。 指標 目標 根拠 音声品質の知覚評価 (PESQ) の劣化 0.23 MOS 以下 ITU-T P.862 (聴取者には知覚されない) スケール不変の信号対雑音比 (SI-SNR) 25 dB 以上 信号の完全性 埋め込みのレイテンシー オーディオ 1 秒あたり 50 ms 未満 リアルタイムストリーミングの要件 復元可能な最小のオーディオ長 1.0 秒以上 モニタリングサービスで実用的 ペイロード容量 16 ビット マニフェストの検索キー MP3 128k または AAC 96k 後の透かしのビット誤り率 (BER) 4% 以下 AudioSeal が公表した堅牢性 (ICML 2024) 注: これらは、AudioSeal が公表した ICML 2024 の結果とアーキテクチャ上の制約に基づくエンジニアリング目標です。多様な種類のコンテンツで本番環境向けに検証するには、MUSHRA (Multiple Stimuli with Hidden Reference and Anchor) の聴取パネルによる厳密な A/B テストが必要です。 プレーン 2: ライブストリーム全体での管理の連鎖 メディア放送パイプラインの各段階で、AWS Elemental サービスからの Amazon EventBridge イベントが、transformer-signer という AWS Lambda 関数を起動します。この関数は、暗号署名されたアサーションを来歴情報のチェーンに追加します。 AWS Elemental MediaLive – インジェストとトランスコードの時点でのアサーション AWS Elemental MediaPackage v2 – HLS、Common Media Application Format (CMAF)、Dynamic Adaptive Streaming over HTTP (DASH) への再パッケージの時点でのアサーション AWS Elemental MediaTailor – SSAI の時点でのアサーション 各アサーションは prev_assertion_id で直前のアサーションを参照し、AWS KMS の非対称キー (ECDSA_SHA_256) で署名されます。その結果、次のような連結されたチェーンが作られます。 { "assertion_id": "ast-7f3a9b2c", "transform_type": "transcode", "created_at": "2026-05-20T14:23:07.442Z", "digest": "a8f2e1...", "signature": "MGQCMBn...", "prev_assertion_id": "ast-2d8e4a1f", "sequence_number": 3 } アサーションの挿入、削除、並べ替えのいずれによる改ざんも、チェーンの暗号上の完全性を損ないます。アサーションの処理に失敗した場合は、 Amazon Simple Queue Service (Amazon SQS) のデッドレターキュー (DLQ) に送られ、再試行されます。 プレーン 3: 改ざん検知可能なレジストリ SENTINEL は、次の表に示す複数の層からなるレジストリに、来歴情報のチェーンを保存します。 層 サービス 目的 プライマリのマニフェストストア Amazon Aurora PostgreSQL 互換エディション Serverless v2 低レイテンシーのクエリ。追記専用のジャーナルをアプリケーション層で実装 (ハッシュで連結した行) し、挿入のみの権限とトリガーで強制 組織をまたいだアンカリング ファーストパーティまたはサードパーティのブロックチェーン連携 (Hyperledger Fabric) 組織の信頼境界をまたいだハッシュのアンカリング C2PA マニフェストの作成 AWS Lambda (Rust、c2pa-rs) 標準に準拠した C2PA コンテンツクレデンシャルのマニフェスト 変更不可能なアーカイブ Amazon Simple Storage Service (Amazon S3) の Object Lock (COMPLIANCE モード) 長期の法的保持のための WORM (Write Once Read Many) ストレージ フィンガープリントのインデックス Amazon OpenSearch Serverless 透かしが削除された場合の、自動コンテンツ認識 (ACR) によるフィンガープリント照合 プレーン 4: エッジでの復元 API どの下流プラットフォームも、キャプチャした 1 秒以上のオーディオを、Amazon CloudFront と Lambda@Edge にデプロイされた復元 API に送信し、来歴情報のチェーン全体を受け取ります。処理は、次の 2 つの経路のいずれかをたどります。 主経路 (透かしが残っている場合) – Lambda@Edge が AudioSeal の検出器で 16 ビットのペイロードを抽出し、Aurora でマニフェストを検索します。そして、チェーン全体 (モデルの ID、ガードレールの証明、すべての変換アサーション) を、信頼度スコアとともに C2PA 互換のマニフェストとして返します。 フォールバック経路 (透かしが劣化または削除された場合) – システムが、オーディオのフィンガープリントを OpenSearch Serverless の ACR インデックスと照合し、信頼度スコアを下げた確率的な一致結果を返します。この副次的な経路は、 コンテンツ配信ネットワーク (CDN) での積極的なトランスコードや敵対的な処理によって、埋め込まれた透かしが劣化した場合に対応します。 目標レイテンシーは、CloudFront のグローバルエッジネットワークを通じて、99 パーセンタイル (p99) で 1 秒未満です。 AWS WAF がレート制限とボット対策を、 AWS Shield Advanced が DDoS 対策を提供します。次の図は、SENTINEL の実装における機能フローの概要です。 図 2: SENTINEL の機能データAmazon CloudFronフロー SENTINEL の実装で得られた主な知見は次のとおりです。 AudioSeal の 16 ビットのメッセージ上限により、マルチフレームのエンコードが必要になる – AudioSeal の事前学習済みモデルは、1 フレームあたり 16 ビットを扱います。128 ビットのペイロードを連続する 8 フレームに分けて埋め込むと、完全に読み取るのに約 8 秒のオーディオが必要になり、順序に依存する壊れやすいデコードになります。ただし SENTINEL は、16 ビットをそのままレジストリの検索キーとして使い、完全なペイロードは署名付きマニフェストに保持します。これにより、復元に必要なオーディオは 1 秒に保たれます。AudioSeal のマルチビット拡張モードやカスタム学習によるキーの拡張は、ロードマップに含まれています。 ボコーダーの後での埋め込みが、現実的な出発点になる – 改変した HiFi-GAN ボコーダーの内部で透かしを埋め込むと、先行技術との差別化は最も大きくなりますが、Amazon SageMaker の p4d.24xlarge インスタンス (NVIDIA A100 GPU 8 基) で 2〜4 週間のカスタムボコーダー学習が必要です。SENTINEL の現在の実装は、AudioSeal をそのまま使い、ボコーダーの出力直後、かつ非可逆エンコードの前に埋め込みます。ロードマップでは、今後のフェーズでボコーダー内部での埋め込みに移行します。 Lambda@Edge のコールドスタートには、アーキテクチャでの対策が必要になる – Lambda@Edge 関数は、リージョンの Lambda よりもコールドスタートのレイテンシーが高く、プロビジョニングされた同時実行もサポートしていません。SENTINEL は、CloudFront のキャッシュ優先のアーキテクチャ (透かしのハッシュでマニフェストの検索結果をキャッシュ) と、軽量な関数設計 (最小限の依存関係、重いフレームワークを使わない) によってこれを軽減し、p99 の復元レイテンシーを目標の 1 秒未満に保っています。計算負荷の高い検出 (AudioSeal の推論) は、プロビジョニングされた同時実行を利用できるリージョンの Lambda 関数に転送します。 まとめ SENTINEL は、後処理のステップとしてではなく、生成の間に透かしを埋め込み、署名付きのアサーションによってあらゆる変換を通じて来歴情報を追跡します。これにより、オーディオと切り離せない管理の連鎖が作られます。このソリューションは、 Amazon Bedrock 、AWS KMS、 AWS Elemental Media Services 、Amazon Aurora、Amazon CloudFront などの AWS マネージドサービスで構築されているため、すでに運用しているインフラストラクチャを使ってパイプラインをデプロイできます。 オーディオの来歴情報は、差別化要因ではなくコンプライアンス要件です。EU AI 法の 2026 年 12 月の期限は、2026 年 7 月に最終化された行動規範が一般に利用可能な検出ソリューションとゼロリテンションでの取り扱いを義務付けたことで、さらに厳しくなりました。音楽業界の帰属に関する義務付けや放送規格も、AI オーディオを生成するのであれば、来歴情報は生成元で組み込まなければならない、という点で一致しています。SENTINEL は、後処理のワークフローが追いつくのを待たずに、これらの義務を満たすための設計図を提供します。 SENTINEL のリファレンス実装は、2026 年 10 月に GitHub で公開される予定です。2026 年 12 月 2 日の期限までに AI 生成オーディオに検証可能な来歴情報を組み込めるよう、 AWS for Media and Entertainment Blog で最新情報をご確認ください。 参考資料 AWS マネジメントコンソール で Amazon Bedrock を使い始める。 コンテンツクレデンシャルのマニフェストについては、 C2PA 2.0 仕様 を参照する。 著者について Saurav Bhattacharya Saurav Bhattacharya は、AWS のシニアソリューションアーキテクトで、通信、ブロードバンド、メディアの分野で豊富な経験を持っています。現在は、メディア & エンターテインメント業界の課題の解決に注力し、技術営業を加速するとともに、お客様のデジタルトランスフォーメーションを加速するスケーラブルなソリューションを構築しています。 参考リンク AWS Media Services AWS Media & Entertainment Blog (日本語) AWS Media & Entertainment Blog (英語) AWS のメディアチームの問い合わせ先: awsmedia@amazon.co.jp ※ 毎月のメルマガをはじめました。最新のニュースやイベント情報を発信していきます。購読希望は上記宛先にご連絡ください。 翻訳は SA 金目、SA 石井が担当しました。原文は こちら をご覧ください。

動画

書籍