免責事項
本記事は筆者個人の見解であり、所属組織の公式見解・開発体制・業務内容を示すものではありません。記事内の技術的表現は 2026年6月時点の実機検証に基づくものであり、特定の製品・サービスの性能・仕様を保証するものではありません。
なお、AWS アカウント ID・IP アドレス・リソース名などは検証環境の実値をマスク・置換しています。また一部の挙動(CloudTrail の sourceIPAddress / userAgent 等)は実機観測値であり、公式ドキュメントの記載とは異なる場合があります。実運用前にご自身の環境・最新の公式ドキュメントでの確認をお願いします。
はじめに
こんにちは!Dirbatoの社内技術横断支援組織Backbeatに所属している柴田です!
2026年5月、AWS から AWS MCP Server(マネージドなリモート MCP)が GA しました。
AWS CLI 相当の操作を MCP ツール(
call_aws/run_script等)としてエージェントに公開する、AWS マネージドのリモート MCP サーバ。ローカルのmcp-proxy-for-awsが SigV4 署名してリモートエンドポイントに中継する。
- 公式(GA アナウンス): The AWS MCP Server is now generally available
- 公式ドキュメント: Agent Toolkit for AWS
AWS MCP Server のセキュリティについては、すでに優れた先行記事があります。Accenture(acntechjp)さんの記事は条件キーによる人間とエージェントの権限分離を、フューチャーさんの記事はIAM ガードレール設計と CloudTrail 監査を、それぞれ丁寧に検証されています(とても参考になりました。ありがとうございます)。
本記事は、それらと重複する条件キー(検証A)・CloudTrail 監査(検証B)は「再現と補強」にとどめ、まだ誰も実測していない次の3点に主眼を置きます。
-
run_scriptの Python サンドボックスを内部まで解剖(検証C)— 何が遮断され、どこまで隔離されているか -
call_awsとrun_scriptで権限境界は同じか(検証C+)— ツールごとにポリシーを書き分ける必要があるのか -
マネージド MCP とローカル MCP の使い分け(検証E)
「エージェントに AWS の鍵を渡して大丈夫なのか?」を、一段深い実装レベルで確かめます。検証は全5本(A〜E)構成です。
- 最小権限 — MCP 経由のリクエストだけを狙って Deny できるか(条件キー / 再現・補強)
- 監査 — CloudTrail で「エージェント起点」を機械的に切り分けられるか(再現・補強)
-
サンドボックス — run_scriptの Python 実行環境はどこまで隔離されているか -
権限の一貫性 — call_awsとrun_scriptで権限境界は同じか -
マルチアカウント / ローカル MCP との使い分け
環境: AWS アカウント
123456789012/ regionap-northeast-1/ IAM ユーザーamazonq(mcp-proxy-for-aws==1.6.0)
こんな人に読んでほしい
- エージェント(Claude Code / Q 等)に AWS 操作を任せたいが、権限設計が不安な人
- MCP 経由の操作を CloudTrail で監査・切り分けしたい人
-
run_scriptの Python サンドボックスがどこまで安全か知りたい人 - マネージド AWS MCP Server と、ローカルの awslabs MCP 群の使い分けを決めたい人
要点サマリー
忙しい方向けに先に結論を。すべて実機で確認した一次データです。
| 検証項目 | 一言結果 |
|---|---|
|
|
aws:CalledViaAWSMCP で 「どの MCP 経由か」を厳密に識別して Deny 可能。S3 バケットポリシー1枚で実証 |
|
|
MCP 起点は sourceIPAddress / userAgent が共に aws-mcp.amazonaws.com。userIdentity は実ユーザーのまま → 切り分けと責任追跡が両立 |
|
|
実行前の静的 AST 検証で os/socket/subprocess/eval 等を遮断。ファイルは /tmp 限定、外部通信は call_boto3(AWS API)経由のみ |
|
|
同じ条件キー Deny が call_aws と run_script の両経路に効いた。「コードは隔離するが権限は隔離しない」 |
|
|
機能は実在するが プロキシ起動時のオプトイン(aws_profile パラメータ)。CLI の --profile は拒否される |
最大の発見: AWS MCP Server は「便利な CLI ラッパー」ではなく、条件キー・CloudTrail・サンドボックスの三層でガバナンスを設計されたサービスだった。aws:CalledViaAWSMCP をたった1つの Deny 文に入れるだけで、エージェントの全操作面(CLI 経路もサンドボックス内 boto3 経路も)を一律に制御できる。
1. アーキテクチャ — 鍵はローカルから出ない
- 認証は既存の AWS 認証チェーン(
~/.aws/credentials)で SigV4 署名。長期認証情報はローカルに留まる。 - MCP Server 自体は無料。課金はエージェントが呼ぶ AWS API の実費のみ。
- サーバ実体は us-east-1 / eu-central-1。叩く対象リージョンは
--metadata AWS_REGION=...で指定。
ここで本記事の主役になるのが、MCP 経由のリクエストに AWS が自動で付与するグローバル条件キーです。
| 条件キー | 型 | 意味 |
|---|---|---|
aws:ViaAWSMCPService |
Bool | AWS マネージド MCP 経由なら true
|
aws:CalledViaAWSMCP |
String |
どの MCP サーバ経由か(例 aws-mcp.amazonaws.com / eks-mcp.amazonaws.com) |
📌 プレビュー期にあった IAM アクション
aws-mcp:InvokeMcp/CallReadOnlyTool/CallReadWriteToolは廃止済み・無効。現在は上記の条件キー方式が正典です。
出典: How AWS MCP Server works with IAM
2. 検証A:条件キーで「MCP 経由の削除」だけを Deny する
S3 バケットに、MCP 経由の DeleteObject だけを拒否するバケットポリシーを置きます。
{
"Sid": "DenyDeleteViaAWSMCP",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::mcp-poc-condkey-test-123456789012/*",
"Condition": {
"StringEquals": { "aws:CalledViaAWSMCP": "aws-mcp.amazonaws.com" }
}
}
call_aws 経由で削除すると——
$ aws s3api delete-object --bucket mcp-poc-condkey-test-... --key obj-cplus-callaws.txt
An error occurred (AccessDenied) when calling the DeleteObject operation:
User: arn:aws:iam::123456789012:user/amazonq is not authorized to perform: s3:DeleteObject
... with an explicit deny in a resource-based policy
狙い通り AccessDenied。条件値を eks-mcp.amazonaws.com(不一致)にすると同じ削除が成功することも確認しました。つまりこのキーは「MCP 経由かどうか」だけでなく 「どの MCP か」まで厳密に識別します。
💡 この条件キーは IAM ポリシー・SCP だけでなくリソースベースポリシー(バケットポリシー)でも使えます。「このバケットへの破壊的操作は、たとえ管理者でも MCP 経由なら拒否」を、リソース側1枚で完結できます。
3. 検証B:CloudTrail で「エージェント起点」を切り分ける
MCP 経由で実行された 下流の AWS API 呼び出し(例: PutBucketPolicy)を CloudTrail LookupEvents で見ると、実機ではこうなりました(今回の操作分を実測)。
// MCP 経由(aws-mcp で実行)
{
"eventName": "PutBucketPolicy",
"eventSource": "s3.amazonaws.com",
"sourceIPAddress": "aws-mcp.amazonaws.com", // ← MCP 起点の刻印
"userAgent": "aws-mcp.amazonaws.com", // ← 同上
"eventType": "AwsApiCall",
"userIdentity.invokedBy":"aws-mcp.amazonaws.com", // ← 「MCP 経由」をここでも識別可能
"userIdentity.arn": "arn:aws:iam::123456789012:user/amazonq" // ← 実ユーザーは保持
}
// 比較: 同じ操作をマネジメントコンソールから実行
{
"sourceIPAddress": "203.0.113.x", // ← 実IP(マスク)
"userAgent": "Mozilla/5.0 ... <ブラウザ>",
"userIdentity.arn": "arn:aws:iam::123456789012:root"
}
🔒 マスキングについて: 本記事の AWS アカウント ID(
123456789012)、IP アドレス(203.0.113.x)、バケット名などは検証環境の実値をマスク・置換しています。挙動の本質は変わりません。
つまり、「誰が(実ユーザー)」と「何経由で(エージェント)」を同時に記録できる。下流 API の sourceIPAddress / userAgent が aws-mcp.amazonaws.com になるので、これでフィルタすればエージェント起点の操作だけを機械抽出できます。AccessDenied で拒否された操作も記録されます。
📌 注記(事実の出どころ): 上記の
sourceIPAddress/userAgent = aws-mcp.amazonaws.comは本検証での実機観測値です(執筆時点で公式ドキュメントに明記された値ではありません)。文字列aws-mcp.amazonaws.comは公式上は条件キーaws:CalledViaAWSMCPの値(サービスプリンシパル)として定義されています。なお同様の観測(userAgent/userIdentity.invokedBy = aws-mcp.amazonaws.com、sourceIPAddressは固定値)はフューチャーさんの記事でも報告されており、本検証と整合します。userIdentity.invokedByも併用すると、より堅牢に切り分けられます。
CloudTrail は2層で記録される
調べると、MCP 操作は CloudTrail 上で2つの層として残ることが分かりました。
| 層 | 何のイベントか | 主なフィールド(公式 / 実測) | 用途 |
|---|---|---|---|
| 下流 API 層 | 実際に叩かれた AWS API(PutBucketPolicy 等) |
eventSource=s3.amazonaws.com / sourceIP=userAgent=aws-mcp.amazonaws.com(実測) |
エージェントが資源に何をしたかを追跡 |
| MCP メタ層 | MCP ツール呼び出しそのもの |
eventSource=aws-mcp.<region>.api.aws / eventName=CallTool / eventType=AwsMcpEvent / requestParameters.method=call_aws(公式ログ例) |
どのツール(call_aws/run_script)を使ったかを追跡 |
下流 API 層で「資源への影響」を、MCP メタ層で「使われたツール」を見る——両方を突き合わせると監査が完成します。公式準拠でフィルタするなら、メタ層の eventType=AwsMcpEvent や requestParameters.method が確実です。
出典(MCP メタ層のログ例): Logging AWS MCP Server with CloudTrail
⚠️ 注意:
DeleteObjectなどのデータイベントは管理イベント履歴(LookupEvents)に出ません。オブジェクト単位で追うには S3 データイベントの記録を有効化する必要があります。
4. 検証C:run_script サンドボックスを解剖する
run_script は boto3 が書ける Python 実行環境です。「Python が動く」=「任意コードが動く」だと怖いので、どこまで隔離されているかを総当たりで実測しました。
4-1. 防御は「実行時」ではなく「コード検証時」
ブロック対象を含むスクリプトは、1行も実行されずに検証エラーで全件列挙・拒否されます。文字列リテラルのパスまで静的に見ています。
Code validation failed:
Line 2: Blocked import: os
Line 2: Blocked import: socket
Line 4: Blocked function: eval()
Line 3: Disallowed file path: x ← open("x") はNG。ファイルは /tmp/ のみ
4-2. 遮断されているもの(実測)
| 種別 | 遮断対象(抜粋) |
|---|---|
| モジュール |
os, sys, socket, subprocess, urllib, http, ssl, ctypes, threading, multiprocessing, pickle, base64, hashlib, inspect, importlib, pathlib, shutil … |
| ビルトイン関数 |
eval, exec, compile, __import__, getattr, setattr, globals, locals, vars, type, hasattr, id, memoryview … |
| 属性アクセス |
.__class__ 等の dunder 属性も静的拒否(型→基底クラス経由の脱出を封鎖) |
| ファイル |
open() は許可だが パスは /tmp/ 限定。/etc/passwd 等は静的拒否 |
許可されるのは json / math / datetime / re / collections / asyncio などの純粋計算・データ整形系のみ。
4-3. これが意味すること
| 攻撃面 | 結果 |
|---|---|
| 外部ネットワークへの持ち出し |
socket/urllib/ssl 不可 → 外部通信は call_boto3(AWS API)経由のみ
|
| IMDS からの認証情報窃取 |
socket 不可 → 169.254.169.254 への接続コードすら書けない
|
| ローカル秘密の読み出し |
os/sys 不可 → 環境変数に到達不能。FS は /tmp のみ |
| サンドボックス脱出 |
eval/exec/__import__/getattr/.__class__ を封鎖 |
「Python が動く」とはいえ、外に出る手段と内省する手段を入口(AST 検証)で塞いだ強固なサンドボックスでした。なお run_script は 最低1つ call_boto3 を含むことも強制されます(AWS API を呼ばないスクリプトは "No AWS API call found" で拒否)。
5. 検証C+:権限境界は call_aws と run_script で同じか?
ここが本記事の山場です。run_script のサンドボックス内 call_boto3 も、実 IAM プリンシパル user/amazonq のまま実行されます(=コードは隔離するが権限は隔離しない)。
では、検証Aの「MCP 経由 DeleteObject Deny」は run_script 内の削除にも効くのか?
# run_script 内で削除を試行
await call_boto3(service_name="s3", operation_name="DeleteObject",
params={"Bucket": BUCKET, "Key": "obj-cplus-runscript.txt"})
# → AccessDenied: ... with an explicit deny in a resource-based policy
効きました。 call_aws と run_script(call_boto3) の両方に同一の条件キー値 aws-mcp.amazonaws.com が刻印されます。
つまり、ガバナンス設計者にとっての結論はシンプルです——
aws:CalledViaAWSMCPを入れた Deny 文を1枚書けば、エージェントが CLI 経路で叩こうがサンドボックス内 boto3 で叩こうが、まとめて制御できる。
経路ごとにポリシーを書き分ける必要はありません。
6. 検証D:マルチアカウント / クロスロール
「別アカウント・別ロールに切り替えられるか」も試しました。直感的に CLI の --profile を付けると——
$ aws sts get-caller-identity --profile dev
The following global arguments cannot be set: --profile
拒否されます(cli_command 内のグローバル引数は禁止=正しい挙動)。正しい機構は別物でした。
| 項目 | 仕様(公式 + 実機) |
|---|---|
| 正しい機構 | プロキシが call_aws/run_script 等のツールスキーマに aws_profile パラメータを注入し、値でプロファイル別の専用接続にルーティング(バックエンド転送前に除去) |
| 有効化条件 | 起動時に複数プロファイルを宣言した時だけ(--profile prod dev staging フラグ or AWS_MCP_PROXY_PROFILES 環境変数) |
| セキュリティ | ①起動時アローリスト(エージェントは他プロファイルを発見不可)②ステートレスなパーコール署名 ③read-only を既定にし書込プロファイルは明示選択を推奨 ④prod 利用はクライアント側フックでゲート |
本セッションは単一プロファイル起動だったため
aws_profileパラメータは出現せず、cross-account 経路は存在しませんでした。sts:AssumeRole権限も持たないプリンシパルだったため、ライブのクロスロールは再現不可。**「設定していなければ、エージェントは勝手に別アカウントへ越境できない」**という事実自体がセキュリティ上の安心材料です。
出典: Multi-profile support
7. 検証E:マネージド AWS MCP Server と「ローカル MCP」の使い分け
awslabs はローカル実行型の MCP 群(aws-api-mcp-server、サービス別 MCP 等)も提供しています。マネージドな AWS MCP Server とどう棲み分けるか。
| 観点 | マネージド AWS MCP Server | ローカル MCP(awslabs 各種) |
|---|---|---|
| 実行場所 | リモート(aws-mcp.*.api.aws)。ローカルは SigV4 署名の中継のみ |
自分のマシンで実行 |
| 認証情報の経路 | ローカル資格情報で署名、API 実行はマネージド側 | ローカル資格情報を直接使用 |
aws:CalledViaAWSMCP 刻印 |
あり(条件キーで一元ガバナンス可) | なし(通常のユーザー API と区別不可) |
| CloudTrail 上の見え方 | sourceIP/userAgent = aws-mcp.amazonaws.com |
通常の API 呼び出しと同じ |
| サンドボックス |
run_script の AST 静的検証で強制 |
サーバ実装依存(隔離保証は基本なし) |
| カバー範囲 | AWS CLI 相当を広く | サービス特化・専用ツールが充実 |
| オフライン / 閉域 | 不可(リモート前提) | 可(ローカル完結) |
使い分けの指針:
- ガバナンス・監査を効かせて広く AWS を触らせたい → マネージド AWS MCP Server(条件キー+CloudTrail+サンドボックスの三層が効く)
- 閉域・特定サービスの専用ツール・自前制御が要る → ローカル MCP
8. まとめ
| 検証 | 結論 |
|---|---|
| A 最小権限 |
aws:CalledViaAWSMCP で「どの MCP 経由か」を厳密識別して Deny。リソースベースでも可 |
| B 監査 |
sourceIP/userAgent = aws-mcp.amazonaws.com+実ユーザー保持で切り分けと追跡が両立 |
| C サンドボックス | AST 静的検証で os/socket/eval 等を遮断、FS は /tmp のみ、外部は call_boto3 だけ |
| C+ 権限の一貫性 | 同一 Deny が CLI 経路もサンドボックス boto3 経路も止める |
| D マルチアカウント |
aws_profile パラメータ(起動時オプトイン)。未設定なら越境不可 |
| E 使い分け | ガバナンス重視=マネージド/閉域・特化=ローカル |
エージェントに AWS を任せる不安の多くは、**「条件キー1枚の Deny」と「CloudTrail の1フィルタ」**で実務的に解消できる、というのが実機検証の結論でした。AWS MCP Server は単なる便利ツールではなく、ガバナンスを前提に設計されたサービスです。
本記事は実機検証に基づくドラフトです。検証は単一アカウント・単一プロファイル・限定権限プリンシパルで実施しているため、マルチアカウント(検証D)はドキュメント仕様の確認に留まる点をご了承ください。