Eyes, Not Hands: Adding AWS DevOps Agent, Bedrock AgentCore and Kiro to a GitOps Platform on EKS
__*Level 300* — Advanced | Amazon EKS, Argo CD, AWS DevOps Agent, Amazon Bedrock AgentCore, Kiro CLI, Amazon EventBridge
🎯 The Challenge
Scenario
Suppose you are the Platform Engineering lead of an enterprise that already runs Kubernetes the GitOps way: an Amazon EKS hub cluster with Argo CD manages a fleet of spoke clusters, and every team onboards its workloads through a single values file in Git. It works. Deployments are boring, in a good way.
Operations are not. On-call engineers keep getting paged for the same kind of incidents: a bad config is merged, pods start crash-looping, and someone spends 40 minutes reading logs and events before realizing the fix is "revert that commit". Then leadership asks the question every platform team is hearing:
> "Can AI agents take the first part of on-call?"
Your mission: add agentic investigation and remediation to the existing platform, under these constraints:
- ✅ No agent gets write access to production. Not to the cluster, not to the AWS account.
- ✅ Every fix is a reviewable pull request. Git stays the single source of truth; Argo CD stays the only thing that changes the cluster.
- ✅ Don't rebuild the platform. The hub-spoke GitOps setup stays as it is; agents are added on top.
- ✅ Multi-account by design. Workloads and CI/CD live in different AWS accounts.
- ✅ Everything is observable and auditable. Every agent action leaves a trace.
> This post is the architecture behind the TechLabVel video, Cap 2.
{% embed youtube.com %}
My Approach: Eyes, Not Hands
The fastest demo is always the same: connect an LLM to the cluster with an admin kubeconfig and let it "fix things". I've seen it in many conference talks, and it's also the one setup I would never take to a client's production. A model can be wrong, a prompt can be injected, a credential can leak. With write access, each of those becomes an outage.
So I split the problem the way with one load-bearing rule:
> Reasoning is decoupled from execution. The agents can read and propose anything, but can mutate nothing directly. Every production change routes through a separate control plane: a GitOps pull request.
The agent gets eyes (logs, events, metrics, code) but no hands. GitOps is the hands, and a human holds them. I also decided not to build a new platform for this: everything is added on top of the hub-spoke GitOps setup I already had.
The Architecture
!Agentic Operations on EKS GitOps Platform
The six numbered steps in the diagram match the list below. Agents (teal) only read; the only write path is the pull request (green) that Argo CD pulls (orange).
1. Detect: a CloudWatch alarm on a Container Insights metric fires when a pod is OOMKilled.
2. Wake up the agent: EventBridge invokes a small Lambda that sends a signed webhook to the AWS DevOps Agent.
3. Investigate: the agent reads Kubernetes state through a read-only MCP server hosted on Amazon Bedrock AgentCore, plus CloudWatch, CloudTrail and the GitHub repos.
4. Hand off: the alarm event is forwarded cross-account to the DevSecOps account.
5. Propose: CodeBuild runs Kiro CLI headless; it edits the GitOps repo and opens a pull request.
6. Approve and apply: a human merges; Argo CD reconciles the spoke cluster.
| Account | What lives there | Why |
|---------|------------------|-----|
| Workload account | EKS hub (Argo CD) + EKS spoke (workloads), CloudWatch, AWS DevOps Agent space, AgentCore Runtime + Gateway | The agent investigates where the signals are |
| DevSecOps account | GitHub runners, the Kiro remediation pipeline (EventBridge bus, Lambdas, SQS, CodeBuild) | The only component that writes code is isolated from the workloads |
| GitHub | gitops (platform + team values), petadoptions (app manifests) | Git is the source of truth and the approval gate |
The Starting Point: GitOps Hub-Spoke
This is the platform from the GitOps and IaC at Scale series. Argo CD runs on the hub and registers each spoke as a cluster secret. Labels on that secret decide which add-ons each cluster gets, through ApplicationSets:
kubectl --context hub -n argocd get secrets -l argocd.argoproj.io/secret-type=cluster -o json \
| jq -r '.items[] | "\(.data.name | @base64d)\t\(.metadata.labels.environment)"'
gitops-spoke-eks-dev dev
in-cluster control-plane
Teams onboard with one file in the platform repo. The workloads entry tells Argo CD which repo and which Kustomize overlay to deploy to the spoke:
# gitops/platform/teams/petadoptions/argoproject-values.yaml (excerpt)
workloads:
- name: app
repoURL: https://github.com/<org>/petadoptions
targetRevision: main
path: k8s/overlays/dev
namespace: petadoptions
Keep this file in mind.
Hands On
Prerequisites
| Tool / component | Version used |
|------------------|--------------|
| Amazon EKS (hub / spoke) | 1.37 / 1.36 |
| OpenTofu + Terragrunt | 1.1.3 / 1.13.2 versions |
| Terraform AWS provider | 6.69.1 |
| awslabs.eks-mcp-server | 0.2.1 |
| Kiro CLI | latest at build time (TODO(you): pin it, see review) |
| Region | us-east-2 |
Deploy order for the agent side: S3 (evaluation bucket) → ECR → IAM roles → MCP server (AgentCore) → Agent Space → tool allowlist. Build and push the arm64 MCP image before the MCP server stack.
Step 1: The Alarm (A Signal That Actually Fires)
The first version of the alarm watched a custom SLO burn-rate metric. Nothing ever published it, so the alarm could never fire. The lesson came early: an agent is only as good as the signal that wakes it up. The final alarm uses a metric that the CloudWatch Observability add-on (Container Insights) already publishes for the spoke:
# environments/dev/observability.tfvars (excerpt)
alarm_name = "petsearch-oomkilled"
metric_namespace = "ContainerInsights"
metric_name = "pod_container_status_terminated_reason_oom_killed"
statistic = "Sum"
threshold = 0 # any OOMKill in the period trips the alarm
period = 300
evaluation_periods = 1
comparison_operator = "GreaterThanThreshold"
treat_missing_data = "notBreaching" # no OOMKills reported = healthy
dimensions = {
ClusterName = "<spoke-cluster>"
Namespace = "petadoptions"
PodName = "petsearch"
}
Step 2: The Trigger (Why a Lambda Signs the Webhook)
EventBridge can't call the AWS DevOps Agent webhook directly: the generic webhook requires an HMAC signature over the timestamp and payload, and EventBridge can't compute it. A small Lambda (about 100 lines) does:
# trigger/src/handler.py (excerpt)
def _sign(timestamp: str, body: bytes, secret: str) -> str:
# AWS DevOps Agent HMAC: base64(HMAC-SHA256(secret, "{timestamp}:{payload}")).
# The timestamp is part of the signed material (replay protection) and the
# digest is base64 — NOT a bare hex of the body.
mac = hmac.new(
secret.encode("utf-8"),
f"{timestamp}:".encode("utf-8") + body,
hashlib.sha256,
)
return base64.b64encode(mac.digest()).decode("ascii")
The webhook URL and secret live in AWS Secrets Manager. The timestamp is part of the signed material, which protects against replays.
Step 3: The MCP Server on AgentCore (Eyes, Not Hands)
The AWS DevOps Agent needs to see inside the cluster: pods, events, logs. Instead of writing our own tools, we wrap the official awslabs.eks-mcp-server (pinned to 0.2.1) and host it on Amazon Bedrock AgentCore Runtime. AWS owns the tool logic; we own the posture.
The posture is read-only, enforced three times inside the server:
# src/mcp-server/server.py (excerpt)
# We NEVER enable write.
ALLOW_WRITE = False
# Defense in depth: refuse to start rather than silently expose mutating tools.
if os.environ.get("READ_ONLY", "true").lower() not in ("1", "true", "yes"):
raise SystemExit("EKS MCP host is read-only by design; set READ_ONLY=true.")
# Remove mutating tools from the registry, so the agent never even SEES them.
_WRITE_TOOLS = {
"manage_k8s_resource", # create/replace/patch/delete K8s objects
"apply_yaml", # apply manifests
"generate_app_manifest", # writes a manifest file
"add_inline_policy", # mutates IAM
"manage_eks_stacks", # CloudFormation create/update
}
Read-only is enforced at the listing layer, not only at call time: a tool the model never sees is a tool it can't be tricked into calling.
In front of the runtime sits an AgentCore Gateway with IAM (SigV4) authentication and semantic tool search:
# modules/agentcore-mcp/main.tf (simplified: count, JWT option and tags removed)
resource "aws_bedrockagentcore_gateway" "mcp" {
name = var.gateway_name
role_arn = var.execution_role_arn
authorizer_type = "AWS_IAM" # SigV4 — no shared secrets
protocol_type = "MCP"
protocol_configuration {
mcp {
instructions = "Read-only ops/K8s tools. Mutation is not exposed; actuation routes through the control plane."
search_type = "SEMANTIC"
}
}
}
Finally, the Agent Space only gets an allowlist of tools, managed as code (AWS::DevOpsAgent::Association, MCPServerSigV4):
mcp-runtime___list_k8s_resources
mcp-runtime___get_pod_logs
mcp-runtime___get_k8s_events
mcp-runtime___list_api_versions
x_amz_bedrock_agentcore_search
On the cluster side, the runtime's IAM role has an EKS access entry with the managed AmazonEKSViewPolicy: get, list and watch, nothing else.
Step 4: The Agent Space (What the Agent Can See)
The AWS DevOps Agent space is created with the native aws_devopsagent_agent_space resource, and its description states the contract:
# stacks/application/compute/devops-agent/main.tf (excerpt)
resource "aws_devopsagent_agent_space" "this" {
name = "${local.name_prefix}-space"
description = "DevOps Agent space for ${var.project} ${var.environment} (read-only investigate + plan; proposes, never mutates)."
locale = var.locale
kms_key_arn = var.kms_key_arn != "" ? var.kms_key_arn : null # optional customer-managed key
}
| Access | What the agent gets |
|--------|---------------------|
| Agent IAM role (read-only policy) | CloudWatch alarms, metrics and logs, X-Ray, CloudTrail LookupEvents, EKS describe |
| MCP server (SigV4, via AgentCore Gateway) | The 4 read-only Kubernetes tools above |
| GitHub: gitops, petadoptions, framework repo | The code and history to correlate "what changed" |
| Slack on-call channel | Where findings are posted for humans |
| Event channel (webhook) | The trigger from Step 2 |
Elevated actions are disabled in the space. The agent's IAM role trusts the aidevops.amazonaws.com service only for this account's agent spaces (aws:SourceAccount + aws:SourceArn), which protects against the confused-deputy problem.
Step 5: The Remediation Pipeline (Proposal to Pull Request)
The second EventBridge rule forwards the same alarm event cross-account to an event bus in the DevSecOps account. Its IAM role can do exactly one thing:
# stacks/application/integration/event-forwarder/main.tf (excerpt)
resource "aws_cloudwatch_event_target" "hub_bus" {
rule = aws_cloudwatch_event_rule.to_hub.name
target_id = "kiro-hub-bus"
arn = var.hub_kiro_event_bus_arn
role_arn = aws_iam_role.forwarder.arn # policy: events:PutEvents on that bus only
}
In the DevSecOps account, an extractor Lambda asks the AWS DevOps Agent API for the mitigation plan and queues it in SQS. A starter Lambda launches CodeBuild, which runs Kiro CLI headless with strict rules ("only edit files in this repo; do NOT run AWS CLI or kubectl"):
kiro-cli chat --no-interactive --trust-tools=read,write,grep,shell "$(cat /tmp/prompt.md)"
The buildspec wraps the agent with guardrails the agent can't change: no changes means no PR, more than 10 files means abort, and the branch is always a new kiro-fix/<timestamp> that goes through review. GitHub access uses an AWS CodeConnections GitHub App, with no personal access tokens.
Defense in Depth
No single control carries the safety. If one fails, the next one holds:
!Image description
When It Breaks: Gotchas I Hit
| Symptom | Cause | Fix |
|---------|-------|-----|
| Invalid STS role configuration for session sigv4McpServerSession | The agent role's trust statement had sts:TagSession | Use sts:AssumeRole only for aidevops.amazonaws.com |
| Role validation failed ... doesn't include either SourceArn or SourceAccount | No confused-deputy guard | Add aws:SourceAccount and aws:SourceArn = arn:aws:aidevops:<region>:<account>:service/* (not agentspace/*) |
| 403 ... Insufficient permissions from the gateway | Trust OK, but no invoke permission | Grant bedrock-agentcore:InvokeGateway on the gateway ARN |
| The MCP server shows zero tools | Gateway created without a target | Add the mcp_server target pointing at the runtime |
| SigV4 signature rejected | Service name set to execute-api | The SigV4 service for AgentCore is bedrock-agentcore |
| Read tools return forbidden | No EKS access entry for the runtime role on the spoke | Add the access entry with AmazonEKSViewPolicy |
| Alarm never fires | Alarm on a metric nothing publishes | Use a metric the Container Insights add-on already emits |
Conclusion
We didn't give an AI agent the keys to production. We gave it the best possible view of production, and one narrow, reviewable way to suggest a change. The existing GitOps platform did the rest: the pull request is the dry-run and the approval, and Argo CD is the deterministic executor.
In the next episode we put this architecture under a real incident: a pod that's OOMKilled under load, an agent that investigates, and the two things it got wrong before the platform got it right.
How are you giving AI agents access to your clusters today: read-only, through PRs, or with the keys? I'd love to hear what's working for you 👇
Happy building! 🚀
References
- AWS DevOps Agent documentation
- Amazon Bedrock AgentCore
- AWS Labs MCP servers (EKS MCP server)
- Amazon EKS access entries and access policies
- Container Insights metrics for Amazon EKS
- Kiro CLI documentation
- GitOps and IaC at Scale — Part 1
---
> The opinions expressed in this post are my own and do not necessarily reflect those of my employer or AWS.
✨ Alejandro Velez, Platform Engineering Latam Lead @ GFT | AWS Ambassador
AWS MCP Server 経由のエージェントを IAM で絞る
:::note info
この記事の英語版を AWS Builder Center に公開しています: Scoping an AI Agent on the AWS MCP Server with IAM
:::
1. 要点
- AI エージェントに MCP サーバ経由で AWS を操作させると、何も絞らない限り、エージェントは自分の認証情報でできる操作をすべて実行できる。依頼していない操作も含む
- エージェントの操作を絞るには、エージェント専用の IAM ユーザーやロールを別に保守するか、MCP サーバ側の設定を使うことになる。MCP サーバ側の読み取り専用の設定は、私が試したものではオンとオフの切り替えだけで、「読み取りは広く許可し、書き込みは決めた場所だけ許可する」という指定はできなかった
- AWS が管理する MCP サーバを経由した呼び出しは、IAM ポリシーの条件で見分けられる。これを使うと、自分の権限はそのままに、既存のポリシーに拒否の文を足して MCP 経由の操作だけを狭められる。ただし、シェルから AWS CLI を実行するなど、MCP を経由しない操作はこの条件では見分けられない
- 今回は、MCP 経由で使える操作の一覧をポリシーに書き、読み取りは広く、書き込みは決めた保存先だけに絞った範囲で、エージェントに調査とレポート作成を任せられた。IAM では SQL の中身を見分けられないため、DB 側の権限と組み合わせた
検証はすべて、2026-10-03 に ap-northeast-1(東京)で行いました。
2. 背景
エージェントは、依頼文で指定していない手段を選ぶことがあります。手元の AWS 認証情報をそのまま使わせていれば、その手段もそのまま実行されます。今回の検証でも、作成途中のポリシーで動かしたエージェント(Claude Code)は、DB 接続用に用意した読み取り専用のシークレットのほかに、管理者シークレット(RDS が管理するマスターユーザーのシークレット)でも SQL を実行しようとしました。依頼文では、どのシークレットを使うかを指定していません。このときは IAM で拒否していたため、SQL は実行されていません(後半の売上分析の例で詳しく書きます)。
エージェントの操作を絞る方法は、これまで 2 つありました。1 つは、エージェント専用の IAM ユーザーやロールを用意する方法です。この方法では、認証情報とポリシーを自分のものとは別に保守する必要があります。
もう 1 つは、MCP サーバ側の設定を使う方法で、設定の方式はサーバごとに異なります。AWS Labs の PostgreSQL MCP Server は、書き込みの可否をフラグ --allow_write_query 1 つで切り替えます。README には、読み取り専用モードについて「ベストエフォートの多層防御の仕組みで、セキュリティ境界ではない」と書かれています。AWS が管理する MCP サーバ(AWS MCP Server)に接続するためのプロキシ mcp-proxy-for-aws にも、--read-only フラグがあります。このフラグを付けて試したところ、AWS API を呼ぶためのツール aws___run_script がツール一覧に出なくなり、データも読めなくなりました。どちらのフラグも、読み取り専用にするかどうかの切り替えで、書き込みを特定の保存先だけに許可することはできませんでした。
AWS MCP Server では、MCP 経由の呼び出しを IAM ポリシーの条件で見分けられます。そのため、認証情報は自分のもののまま、MCP 経由の呼び出しにだけ適用される Deny の文を既存のポリシーに足し、操作ごとに許可と拒否を書き分けられます。この記事では、この方法で MCP 経由で許可する操作を決め、読み取りだけで済むサポート調査(ユースケース 1)と、書き込み先を 1 か所に絞った売上分析(ユースケース 2)をエージェントに任せた結果を書きます。
3. AWS MCP Server の認可の仕組み
AWS MCP Server は、2026-05-06 に GA となったマネージド型の MCP エンドポイントで、追加料金はかかりません。2026-10-02 には東京を含む 6 リージョンが追加され、使えるリージョンは 8 つになりました。エージェントは mcp-proxy-for-aws 経由で接続し、プロキシが手元の AWS 認証情報でリクエストに署名します。エージェントが AWS API を呼ぶときに使うツールは、aws___run_script です。このツールはサンドボックスで Python を実行し、その中で call_boto3 という関数を使えます。
ユーザーガイドの IAM のページによると、このサーバは独自の IAM アクション、リソース、サービス固有の条件キーを定義していません。呼び出し先のサービスが、ほかの AWS API 呼び出しと同じく既存のポリシーで認可します。そのため、条件キーを使わなければ、エージェントにできることは、その IAM プリンシパルにできることと同じです。AWS は呼び出しにグローバル条件キーを 2 つ付けます。
| 条件キー | 型 | 意味 |
|---|---|---|
| aws:ViaAWSMCPService | Bool | AWS が管理する MCP サーバのどれかを経由したリクエストで true |
| aws:CalledViaAWSMCP | String | 呼び出し元の MCP サーバのサービスプリンシパル。例: AWS MCP Server なら aws-mcp.amazonaws.com |
今回は aws:ViaAWSMCPService を使いました。AWS MCP Server だけに絞り、ほかの AWS 管理の MCP サーバを対象外にする場合は、aws:CalledViaAWSMCP を使います。
4. MCP 経由で許可する操作と拒否する操作
今回の IAM ユーザーには PowerUserAccess を付けています。このユーザーは、私が AWS CLI からも使っているものです。このユーザーに付けている既存のカスタマー管理ポリシーに、MCP 経由で使える操作の一覧(許可リスト)を書きました。使ったのは Deny の文です。NotAction に許可リストの操作を並べ、aws:ViaAWSMCPService を条件にして、許可リストにない操作を MCP 経由ではすべて拒否します。さらに、S3 の書き込み先を reports/agent/ に限る文と、管理者シークレットの取得を拒否する文を足しました。どの文も権限を与えず、MCP 経由の呼び出しから権限を取り除くだけです(全文は付録 A)。下の表の「許可」は、これらの Deny で拒否しない操作を指します。
Allow で書かなかったのは、ユーザーに付けたポリシー同士では Allow が足し合わされるためです。Allow を足しても PowerUserAccess の Allow は狭まりません。ユーザーのポリシーの中で狭められるのは、明示的な Deny です。Allow の形で上限を書く方法にはアクセス許可の境界(permissions boundary)がありますが、今回は試していません。
| 対象 | MCP 経由で許可 | MCP 経由で拒否 |
|---|---|---|
| DynamoDB | 読み取り: GetItem、BatchGetItem、Query、Scan、PartiQL の SELECT、Describe*、List* | すべての書き込み(UpdateItem、PutItem、DeleteItem、BatchWriteItem、TransactWriteItems、PartiQL の書き込み)と、タグなどテーブル設定の変更 |
| Aurora(RDS Data API) | 管理者シークレット以外のシークレットでの ExecuteStatement。cbsr-readonly では PostgreSQL が SELECT だけを許可 | 管理者シークレット(RDS が管理するマスターユーザーのシークレット)、BatchExecuteStatement などほかの Data API 操作 |
| Secrets Manager | ListSecrets、DescribeSecret、管理者シークレット以外のすべてのシークレットへの GetSecretValue。エージェントはシークレットの値を読める | 管理者シークレット |
| S3 | 読み取り: GetObject、ListBucket<br>書き込み: レポート用バケットの reports/agent/ の下への PutObject | ほかのプレフィックスへの書き込み、DeleteObject、バケットの設定 |
| その他 | RDS、CloudWatch、CloudWatch Logs の読み取り、sts:GetCallerIdentity、すべての鍵での kms:Decrypt | 許可リストにない操作すべて |
読み取りを許可した範囲は、一覧のサービスのすべてのテーブル、バケット、シークレットです。以下の依頼にある「作業の対象は〜だけ」はプロンプトでの指示で、IAM の制限ではありません。
IAM のポリシーは SQL 文の内容を条件にできないため、Aurora の読み取り専用は DB ロールの権限で実現しました。
今回は、管理者シークレット以外のシークレットが cbsr-readonly だけだったため、管理者シークレット以外に対する GetSecretValue は絞っていません。本番では、GetSecretValue もエージェントが使うシークレットだけに絞る必要があります。
5. 検証環境
| 項目 | 内容 |
|---|---|
| AWS MCP Server | 東京エンドポイント(ホスト aws-mcp.ap-northeast-1.api.aws、パス /mcp)。mcp-proxy-for-aws 1.7.0 経由で接続 |
| DynamoDB | テーブル cbsr-orders(注文 12 件。属性は customer_id、status、total_jpy、created_at)と GSI customer_id-created_at-index |
| Aurora | PostgreSQL 17.9 Serverless v2(RDS Data API 有効)。テーブル customers(email 列あり)と orders |
| DB ロール | cbsr_ro が読めるのは、orders の全体と、customers の customer_id 列と name 列だけ。認証情報はシークレット cbsr-readonly に保存 |
| S3 | バケット cbsr-reports-<account-id>(ブロックパブリックアクセス有効) |
| エージェント | Claude Code 2.1.269 をヘッドレス(claude -p)で起動。モデルは claude-opus-5[1m]。AWS MCP Server だけをつなぎ(--strict-mcp-config)、シェル、ファイル操作、Web のツールは禁止。Claude Code のほかの組み込みツールは使える状態だったが、エージェントは使っていない。依頼も返答も日本語 |
| 境界の確認 | 各ユースケースの「境界の確認」の表に挙げた操作は、私が書いた Python の MCP クライアントから MCP 経由で呼んだ。主な書き込みは AWS CLI でも試した |
6. ユースケース 1: サポート対応の調査を、読み取りだけで任せる
6.1. エージェントへの依頼
> 顧客 c-02 から「注文 o-1003 がまだ届かない」と問い合わせが来ました。o-1003 の状況と、この顧客の注文履歴を調べて、担当者向けに状況をまとめ、顧客への回答文の下書きを作ってください。倉庫からは「o-1003 は本日発送済み」と連絡が来ています。ステータスの更新は担当者が行うので、あなたはデータを変更しないでください。作業の対象は cbsr-orders だけにしてください。最後に、実行したことを報告してください。
6.2. エージェントがしたこと
1. DescribeTable を呼んだあと、GetItem で o-1003 を、GSI への Query でこの顧客の注文を読んだ
2. 担当者向けのまとめで、倉庫は発送済みと連絡しているのに、テーブルのステータスは PROCESSING のままだと指摘した
3. 同じまとめで、テーブルに配送関連の属性が無いため、追跡番号は倉庫に確認する必要があると書いた。ステータスの更新は担当者の作業として挙げた
4. 追跡番号と配送業者を空欄にした回答文を下書きし、データは読んだだけだと報告した
6.3. 境界の確認
| 操作 | 想定 | 結果 |
|---|---|---|
| GetItem、Query(GSI)、Scan、BatchGetItem、PartiQL の SELECT | 許可(読み取り) | 許可 |
| UpdateItem、TransactWriteItems(更新) | 拒否(書き込み) | 拒否 |
| PartiQL の UPDATE | 拒否(書き込み) | 拒否 |
| PutItem、DeleteItem、BatchWriteItem | 拒否(書き込み) | 拒否 |
| テーブルへの TagResource | 拒否(テーブル設定) | 拒否 |
結果はすべて想定どおりで、データも変わっていません。件数は 12 件のままで、o-1003 も PROCESSING のままです。同じユーザーで AWS CLI からはステータスを更新でき、すぐ元に戻しました。
7. ユースケース 2: 売上の分析を、書き込み先を 1 か所に絞って任せる
7.1. エージェントへの依頼
> 9/15 以降(キャンセル済みを除く)の売上について、商品別の販売数と売上金額、顧客別の売上金額を集計し、気づいたことを添えた週次レポートを Markdown で作ってください。できたレポートは S3 バケット cbsr-reports-<account-id> の reports/agent/2026-10-03-weekly-sales.md に保存してください。作業の対象は cbsr-aurora とこのバケットだけにしてください。最後に、実行したことを報告してください。
7.2. エージェントがしたこと
1. 管理者シークレットと cbsr-readonly の 2 つを見つけ、読み取り専用の cbsr-readonly を選んだ
2. customers への SELECT * が permission denied for table customers になり、customers は読めないと判断して、orders だけで集計した。途中経過の文では、管理者シークレットは使わないと書いた
3. キャンセル済みと 9/15 より前の注文を除いた 10 件(合計 45,200 円)を、商品別、顧客別、週別、単価別に集計し、所見を 7 つ添えてレポートを書いた
4. 保存先のキーにオブジェクトが無いことを確認してから PutObject で保存し、保存後に HeadObject で確認した
SELECT * が拒否されたのは、cbsr_ro に権限の無い email 列が含まれていたためで、実際には customer_id と name は読めます。顧客別の売上は orders の customer_id で集計しているため、この判断は集計結果に影響していません。
途中の版のポリシーで動かした 2 回の実行では、エージェントは管理者シークレットで SQL を実行しようとしました。1 回目は、同じスクリプトの中で読み取り専用のシークレットと管理者シークレットの両方を使っています。2 回目は、顧客のメールアドレスも求める依頼で権限エラーになったあと、「管理用シークレットで試します」と書いてから実行しました。管理者シークレットを拒否する DenyAuroraAdminSecretViaMcp の文は最初の版から入れてあり、2 回とも IAM が拒否しました。最終の版にも、この文を残しています。
7.3. 境界の確認
| 操作 | 想定 | 結果 |
|---|---|---|
| cbsr-readonly での集計クエリ | 許可(読み取り) | 許可 |
| バケットへの GetObject、ListObjectsV2 | 許可(読み取り) | 許可 |
| reports/agent/ の下への PutObject | 許可(唯一許可したプレフィックス) | 許可 |
| reports/finance/ の下やバケット直下への PutObject | 拒否(ほかのプレフィックス) | 拒否 |
| DeleteObject、PutBucketPolicy | 拒否(許可リストにない) | 拒否 |
| cbsr-readonly での DELETE FROM orders | PostgreSQL が拒否(SELECT のみ) | PostgreSQL が拒否 |
| 管理者シークレットでの SQL | IAM が拒否 | IAM が拒否(Failed to fetch secret ... explicit deny) |
結果はすべて想定どおりでした。同じユーザーで AWS CLI からは reports/finance/ の下に保存でき、オブジェクトも削除できました。
8. AWS の推奨との関係
AWS Security Blog の 2 本の記事(「Understanding IAM for Managed AWS MCP Servers」と「Secure AI agent access patterns to AWS resources using Model Context Protocol」)では、条件キーで MCP 経由の操作と人が直接行う操作を区別すること、読み取りで済む作業は読み取り専用にすること、人より狭い権限のエージェント専用ロールを作ることが勧められています。今回の構成は、このうち前の 2 つに当たります。エージェント専用のロールは作らず、自分の IAM ユーザーのまま、MCP 経由の操作だけを狭めました。人の権限を最小権限に絞っていても、人の仕事に必要な範囲と、エージェントに任せる作業に必要な範囲は一致しないため、同じ認証情報を共有するなら、エージェントに不要な分は条件キーで MCP 経由だけ拒否することになります。今回は検証用に PowerUserAccess を使ったので差が大きくなっていますが、考え方は同じです。
後者の記事には、アクセス許可の境界はエージェント専用のロールに付けたときに最も効果があり、開発者が既存のロールに付けるとエージェント以外の使い方まで制限される、と書かれています。
同じ記事には、aws:ViaAWSMCPService が付くのは AWS が管理する MCP サーバを経由した呼び出しだけだ、とも書かれています。エージェントがシェルのツールで AWS CLI を実行すると、同じ API を呼んでも、aws:ViaAWSMCPService を条件にした Deny は適用されません。今回の検証では、エージェントにシェルのツールを使わせていません。ユースケース 1、2 で、同じユーザーの AWS CLI からは更新や保存ができました。これも、CLI の呼び出しにこの条件キーが付かないためです。
9. まとめ
aws:ViaAWSMCPService で MCP 経由の操作だけを制限すると、自分の権限を変えずに、エージェントに渡す範囲を決められました。本番で使う場合は、読み取りもリソースの条件で必要なテーブルやバケットに絞り、GetSecretValue をエージェント用のシークレットに限る必要があります。エージェントにシェルのツールを使わせる場合、シェルから実行した AWS CLI の呼び出しには aws:ViaAWSMCPService が付きません。その場合は、AWS Security Blog が勧めるように、エージェント専用のロールで権限そのものを狭めます。
10. 付録 A: ポリシーの全文
IAM ユーザーの既存のカスタマー管理ポリシーに、次の文を足しました。<id> は、RDS が管理する管理者シークレットの名前の一部です。
{
"Sid": "DenyUnlistedActionsViaMcp",
"Effect": "Deny",
"NotAction": [
"dynamodb:GetItem", "dynamodb:BatchGetItem", "dynamodb:Query", "dynamodb:Scan",
"dynamodb:PartiQLSelect", "dynamodb:Describe*", "dynamodb:List*",
"rds:Describe*", "rds:List*", "rds-data:ExecuteStatement",
"secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:ListSecrets",
"cloudwatch:GetMetricData", "cloudwatch:GetMetricStatistics", "cloudwatch:ListMetrics", "cloudwatch:DescribeAlarms",
"logs:Describe*", "logs:Get*", "logs:FilterLogEvents", "logs:StartQuery",
"sts:GetCallerIdentity", "kms:Decrypt",
"s3:PutObject", "s3:GetObject", "s3:ListBucket"
],
"Resource": "*",
"Condition": { "Bool": { "aws:ViaAWSMCPService": "true" } }
},
{
"Sid": "DenyMcpPutOutsideAgentReports",
"Effect": "Deny",
"Action": "s3:PutObject",
"NotResource": "arn:aws:s3:::cbsr-reports-<account-id>/reports/agent/*",
"Condition": { "Bool": { "aws:ViaAWSMCPService": "true" } }
},
{
"Sid": "DenyAuroraAdminSecretViaMcp",
"Effect": "Deny",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:ap-northeast-1:<account-id>:secret:rds!cluster-<id>-*",
"Condition": { "Bool": { "aws:ViaAWSMCPService": "true" } }
}
- 1 つ目の文は、一覧にない操作をすべて拒否する。一覧にある書き込みは 2 つだけで、s3:PutObject は 2 つ目の文で reports/agent/ に限り、rds-data:ExecuteStatement の SQL は DB ロールで制限する
- kms:Decrypt は Data API のために一覧に入れた(付録 B)。この書き方では、MCP 経由でどの鍵でも復号できる。絞り込んだ書き方は試していない
11. 付録 B: つまずいた点
- データの書き込みだけを拒否する書き方では、管理操作は拒否されなかった。途中の版のうち最初の版では、DynamoDB の書き込み系アクションを Deny に並べていたが、DynamoDB のテーブルと Aurora クラスタへのタグ付けが MCP 経由でも成功した。拒否の対象から除く操作を NotAction に並べた Deny にすると、試した管理操作(タグ付け)はすべて拒否された
- Data API には kms:Decrypt が必要だった。NotAction の書き方に変えたあと、読み取り専用のシークレットでの呼び出しも Access to KMS is not allowed で失敗し、kms:Decrypt を足すと成功した。シークレット取得時の復号にも MCP の条件が適用されていると推測しているが、ドキュメントでは確認できていない
- DynamoDB で UpdateItem を status 属性だけに許可する書き方(dynamodb:Attributes を使う)も試した。条件式で参照しただけの属性も、dynamodb:Attributes の評価対象に含まれた。また、存在しない注文番号への更新では新しいアイテムが作られた。ユースケース 1 を読み取り専用にしたのは、このため
11.1. 検証条件の注意
- 起動したエージェントに質問すると、Claude Code の組み込みの指示に、取り消しにくい操作の前に確認を求める趣旨の記述があると回答した(この回答の内容は確かめていない)。エージェントが実行しなかった操作には、ポリシーではなくこの指示によるものも含まれる可能性がある
参考
- AWS MCP Server の GA(What's New、2026-05-06): aws.amazon.com
- AWS MCP Server の 6 リージョン追加(What's New、2026-10-02): aws.amazon.com
- AWS MCP Server と IAM: docs.aws.amazon.com
- AWS MCP Server の IAM ポリシーの例: docs.aws.amazon.com
- AWS MCP Server のツール: docs.aws.amazon.com
- Understanding IAM for Managed AWS MCP Servers(AWS Security Blog、2026-03-02): aws.amazon.com
- Secure AI agent access patterns to AWS resources using Model Context Protocol(AWS Security Blog、2026-04-14): aws.amazon.com
- IAM ポリシーの条件による細かいアクセス制御(DynamoDB): docs.aws.amazon.com
- RDS Data API: docs.aws.amazon.com
- AWS Labs PostgreSQL MCP Server(README): github.com/awslabs/mcp/blob/main/src/postgres-mcp…
- mcp-proxy-for-aws: aws/mcp-proxy-for-aws
Mentions
Eyes, Not Hands: Adding AWS DevOps Agent, Bedrock AgentCore and Kiro to a GitOps Platform on EKS __*Level 300* — Advanced | Amazon EKS, Argo CD, AWS DevOps Agent, Amazon Bedrock AgentCore, Kiro CLI, Amazon EventBridge 🎯 The Challenge Scenario Suppose you are the Platform Engineering lead of an enterprise that already runs Kubernetes the GitOps way: an Amazon EKS hub cluster with Argo CD manages a fleet of spoke clusters, and every team onboards its workloads through a single values file in Git. It works. Deployments are boring, in a good way. Operations are not. On-call engineers keep getting paged for the same kind of incidents: a bad config is merged, pods start crash-looping, and someone spends 40 minutes reading logs and events before realizing the fix is "revert that commit". Then leadership asks the question every platform team is hearing: > "Can AI agents take the first part of on-call?" Your mission: add agentic investigation and remediation to the existing platform, under these constraints: - ✅ No agent gets write access to production. Not to the cluster, not to the AWS account. - ✅ Every fix is a reviewable pull request. Git stays the single source of truth; Argo CD stays the only thing that changes the cluster. - ✅ Don't rebuild the platform. The hub-spoke GitOps setup stays as it is; agents are added on top. - ✅ Multi-account by design. Workloads and CI/CD live in different AWS accounts. - ✅ Everything is observable and auditable. Every agent action leaves a trace. > This post is the architecture behind the TechLabVel video, Cap 2. {% embed youtube.com %} My Approach: Eyes, Not Hands The fastest demo is always the same: connect an LLM to the cluster with an admin kubeconfig and let it "fix things". I've seen it in many conference talks, and it's also the one setup I would never take to a client's production. A model can be wrong, a prompt can be injected, a credential can leak. With write access, each of those becomes an outage. So I split the problem the way with one load-bearing rule: > Reasoning is decoupled from execution. The agents can read and propose anything, but can mutate nothing directly. Every production change routes through a separate control plane: a GitOps pull request. The agent gets eyes (logs, events, metrics, code) but no hands. GitOps is the hands, and a human holds them. I also decided not to build a new platform for this: everything is added on top of the hub-spoke GitOps setup I already had. The Architecture !Agentic Operations on EKS GitOps Platform The six numbered steps in the diagram match the list below. Agents (teal) only read; the only write path is the pull request (green) that Argo CD pulls (orange). 1. Detect: a CloudWatch alarm on a Container Insights metric fires when a pod is OOMKilled. 2. Wake up the agent: EventBridge invokes a small Lambda that sends a signed webhook to the AWS DevOps Agent. 3. Investigate: the agent reads Kubernetes state through a read-only MCP server hosted on Amazon Bedrock AgentCore, plus CloudWatch, CloudTrail and the GitHub repos. 4. Hand off: the alarm event is forwarded cross-account to the DevSecOps account. 5. Propose: CodeBuild runs Kiro CLI headless; it edits the GitOps repo and opens a pull request. 6. Approve and apply: a human merges; Argo CD reconciles the spoke cluster. | Account | What lives there | Why | |---------|------------------|-----| | Workload account | EKS hub (Argo CD) + EKS spoke (workloads), CloudWatch, AWS DevOps Agent space, AgentCore Runtime + Gateway | The agent investigates where the signals are | | DevSecOps account | GitHub runners, the Kiro remediation pipeline (EventBridge bus, Lambdas, SQS, CodeBuild) | The only component that writes code is isolated from the workloads | | GitHub | gitops (platform + team values), petadoptions (app manifests) | Git is the source of truth and the approval gate | The Starting Point: GitOps Hub-Spoke This is the platform from the GitOps and IaC at Scale series. Argo CD runs on the hub and registers each spoke as a cluster secret. Labels on that secret decide which add-ons each cluster gets, through ApplicationSets: kubectl --context hub -n argocd get secrets -l argocd.argoproj.io/secret-type=cluster -o json \ | jq -r '.items[] | "\(.data.name | @base64d)\t\(.metadata.labels.environment)"' gitops-spoke-eks-dev dev in-cluster control-plane Teams onboard with one file in the platform repo. The workloads entry tells Argo CD which repo and which Kustomize overlay to deploy to the spoke: # gitops/platform/teams/petadoptions/argoproject-values.yaml (excerpt) workloads: - name: app repoURL: https://github.com/<org>/petadoptions targetRevision: main path: k8s/overlays/dev namespace: petadoptions Keep this file in mind. Hands On Prerequisites | Tool / component | Version used | |------------------|--------------| | Amazon EKS (hub / spoke) | 1.37 / 1.36 | | OpenTofu + Terragrunt | 1.1.3 / 1.13.2 versions | | Terraform AWS provider | 6.69.1 | | awslabs.eks-mcp-server | 0.2.1 | | Kiro CLI | latest at build time (TODO(you): pin it, see review) | | Region | us-east-2 | Deploy order for the agent side: S3 (evaluation bucket) → ECR → IAM roles → MCP server (AgentCore) → Agent Space → tool allowlist. Build and push the arm64 MCP image before the MCP server stack. Step 1: The Alarm (A Signal That Actually Fires) The first version of the alarm watched a custom SLO burn-rate metric. Nothing ever published it, so the alarm could never fire. The lesson came early: an agent is only as good as the signal that wakes it up. The final alarm uses a metric that the CloudWatch Observability add-on (Container Insights) already publishes for the spoke: # environments/dev/observability.tfvars (excerpt) alarm_name = "petsearch-oomkilled" metric_namespace = "ContainerInsights" metric_name = "pod_container_status_terminated_reason_oom_killed" statistic = "Sum" threshold = 0 # any OOMKill in the period trips the alarm period = 300 evaluation_periods = 1 comparison_operator = "GreaterThanThreshold" treat_missing_data = "notBreaching" # no OOMKills reported = healthy dimensions = { ClusterName = "<spoke-cluster>" Namespace = "petadoptions" PodName = "petsearch" } Step 2: The Trigger (Why a Lambda Signs the Webhook) EventBridge can't call the AWS DevOps Agent webhook directly: the generic webhook requires an HMAC signature over the timestamp and payload, and EventBridge can't compute it. A small Lambda (about 100 lines) does: # trigger/src/handler.py (excerpt) def _sign(timestamp: str, body: bytes, secret: str) -> str: # AWS DevOps Agent HMAC: base64(HMAC-SHA256(secret, "{timestamp}:{payload}")). # The timestamp is part of the signed material (replay protection) and the # digest is base64 — NOT a bare hex of the body. mac = hmac.new( secret.encode("utf-8"), f"{timestamp}:".encode("utf-8") + body, hashlib.sha256, ) return base64.b64encode(mac.digest()).decode("ascii") The webhook URL and secret live in AWS Secrets Manager. The timestamp is part of the signed material, which protects against replays. Step 3: The MCP Server on AgentCore (Eyes, Not Hands) The AWS DevOps Agent needs to see inside the cluster: pods, events, logs. Instead of writing our own tools, we wrap the official awslabs.eks-mcp-server (pinned to 0.2.1) and host it on Amazon Bedrock AgentCore Runtime. AWS owns the tool logic; we own the posture. The posture is read-only, enforced three times inside the server: # src/mcp-server/server.py (excerpt) # We NEVER enable write. ALLOW_WRITE = False # Defense in depth: refuse to start rather than silently expose mutating tools. if os.environ.get("READ_ONLY", "true").lower() not in ("1", "true", "yes"): raise SystemExit("EKS MCP host is read-only by design; set READ_ONLY=true.") # Remove mutating tools from the registry, so the agent never even SEES them. _WRITE_TOOLS = { "manage_k8s_resource", # create/replace/patch/delete K8s objects "apply_yaml", # apply manifests "generate_app_manifest", # writes a manifest file "add_inline_policy", # mutates IAM "manage_eks_stacks", # CloudFormation create/update } Read-only is enforced at the listing layer, not only at call time: a tool the model never sees is a tool it can't be tricked into calling. In front of the runtime sits an AgentCore Gateway with IAM (SigV4) authentication and semantic tool search: # modules/agentcore-mcp/main.tf (simplified: count, JWT option and tags removed) resource "aws_bedrockagentcore_gateway" "mcp" { name = var.gateway_name role_arn = var.execution_role_arn authorizer_type = "AWS_IAM" # SigV4 — no shared secrets protocol_type = "MCP" protocol_configuration { mcp { instructions = "Read-only ops/K8s tools. Mutation is not exposed; actuation routes through the control plane." search_type = "SEMANTIC" } } } Finally, the Agent Space only gets an allowlist of tools, managed as code (AWS::DevOpsAgent::Association, MCPServerSigV4): mcp-runtime___list_k8s_resources mcp-runtime___get_pod_logs mcp-runtime___get_k8s_events mcp-runtime___list_api_versions x_amz_bedrock_agentcore_search On the cluster side, the runtime's IAM role has an EKS access entry with the managed AmazonEKSViewPolicy: get, list and watch, nothing else. Step 4: The Agent Space (What the Agent Can See) The AWS DevOps Agent space is created with the native aws_devopsagent_agent_space resource, and its description states the contract: # stacks/application/compute/devops-agent/main.tf (excerpt) resource "aws_devopsagent_agent_space" "this" { name = "${local.name_prefix}-space" description = "DevOps Agent space for ${var.project} ${var.environment} (read-only investigate + plan; proposes, never mutates)." locale = var.locale kms_key_arn = var.kms_key_arn != "" ? var.kms_key_arn : null # optional customer-managed key } | Access | What the agent gets | |--------|---------------------| | Agent IAM role (read-only policy) | CloudWatch alarms, metrics and logs, X-Ray, CloudTrail LookupEvents, EKS describe | | MCP server (SigV4, via AgentCore Gateway) | The 4 read-only Kubernetes tools above | | GitHub: gitops, petadoptions, framework repo | The code and history to correlate "what changed" | | Slack on-call channel | Where findings are posted for humans | | Event channel (webhook) | The trigger from Step 2 | Elevated actions are disabled in the space. The agent's IAM role trusts the aidevops.amazonaws.com service only for this account's agent spaces (aws:SourceAccount + aws:SourceArn), which protects against the confused-deputy problem. Step 5: The Remediation Pipeline (Proposal to Pull Request) The second EventBridge rule forwards the same alarm event cross-account to an event bus in the DevSecOps account. Its IAM role can do exactly one thing: # stacks/application/integration/event-forwarder/main.tf (excerpt) resource "aws_cloudwatch_event_target" "hub_bus" { rule = aws_cloudwatch_event_rule.to_hub.name target_id = "kiro-hub-bus" arn = var.hub_kiro_event_bus_arn role_arn = aws_iam_role.forwarder.arn # policy: events:PutEvents on that bus only } In the DevSecOps account, an extractor Lambda asks the AWS DevOps Agent API for the mitigation plan and queues it in SQS. A starter Lambda launches CodeBuild, which runs Kiro CLI headless with strict rules ("only edit files in this repo; do NOT run AWS CLI or kubectl"): kiro-cli chat --no-interactive --trust-tools=read,write,grep,shell "$(cat /tmp/prompt.md)" The buildspec wraps the agent with guardrails the agent can't change: no changes means no PR, more than 10 files means abort, and the branch is always a new kiro-fix/<timestamp> that goes through review. GitHub access uses an AWS CodeConnections GitHub App, with no personal access tokens. Defense in Depth No single control carries the safety. If one fails, the next one holds: !Image description When It Breaks: Gotchas I Hit | Symptom | Cause | Fix | |---------|-------|-----| | Invalid STS role configuration for session sigv4McpServerSession | The agent role's trust statement had sts:TagSession | Use sts:AssumeRole only for aidevops.amazonaws.com | | Role validation failed ... doesn't include either SourceArn or SourceAccount | No confused-deputy guard | Add aws:SourceAccount and aws:SourceArn = arn:aws:aidevops:<region>:<account>:service/* (not agentspace/*) | | 403 ... Insufficient permissions from the gateway | Trust OK, but no invoke permission | Grant bedrock-agentcore:InvokeGateway on the gateway ARN | | The MCP server shows zero tools | Gateway created without a target | Add the mcp_server target pointing at the runtime | | SigV4 signature rejected | Service name set to execute-api | The SigV4 service for AgentCore is bedrock-agentcore | | Read tools return forbidden | No EKS access entry for the runtime role on the spoke | Add the access entry with AmazonEKSViewPolicy | | Alarm never fires | Alarm on a metric nothing publishes | Use a metric the Container Insights add-on already emits | Conclusion We didn't give an AI agent the keys to production. We gave it the best possible view of production, and one narrow, reviewable way to suggest a change. The existing GitOps platform did the rest: the pull request is the dry-run and the approval, and Argo CD is the deterministic executor. In the next episode we put this architecture under a real incident: a pod that's OOMKilled under load, an agent that investigates, and the two things it got wrong before the platform got it right. How are you giving AI agents access to your clusters today: read-only, through PRs, or with the keys? I'd love to hear what's working for you 👇 Happy building! 🚀 References - AWS DevOps Agent documentation - Amazon Bedrock AgentCore - AWS Labs MCP servers (EKS MCP server) - Amazon EKS access entries and access policies - Container Insights metrics for Amazon EKS - Kiro CLI documentation - GitOps and IaC at Scale — Part 1 --- > The opinions expressed in this post are my own and do not necessarily reflect those of my employer or AWS. ✨ Alejandro Velez, Platform Engineering Latam Lead @ GFT | AWS Ambassador
AWS's own MCP server (<1.6.0, HTTP + OAuth) checked a token's signature, issuer and expiry, not who it was for. A token for another app in the same Cognito pool could make AWS calls with the host's creds. Upgrade, and check aud on every MCP server. github.com/awslabs/mcp/security/advisories/GHSA-4…
AWS Pricing MCP 能让 AI 查云服务价格、比较不同区域,选机器前少翻几页报价表。Apache-2.0 开源。 官方提醒:模型可能配错筛选条件,也不保证找到最低价。查到报价后,实例规格和区域仍得自己核对。 awslabs/mcp
Your MCP server may be accepting tokens that were never issued to it, and patching won't fix that on its own. The [MCP Python SDK advisory](github.com/modelcontextprotocol/python-sdk/securi…) (CVSS 6.8) says the server's bearer auth never checked the token's audience claim (aud). If one authorization server issues tokens for several services, a valid token for service A also opens your MCP server. The [AWS API MCP Server](github.com/awslabs/mcp/security/advisories/GHSA-4…) disclosed the same kind of bug on Oct 27. It's the classic confused-deputy problem, now with an agent holding cloud credentials. Here's the detail that matters. Upgrading to mcp 1.30.0 or 2.2.0 changes nothing until you set validate_token_resource=True and return the audience from verify_token(). It only turns on by default in 3.0. Boards keep missing this gap. "We patched" is a statement about versions. Assurance is a statement about controls, backed by evidence. Directors should put three questions to the CISO this quarter. How many MCP servers and agent tools are running in production, and who owns that inventory? Is audience validation proven by a test, or just set in a config? Which agent actions need a human approval, and is that approval logged? That's why I build TrustModel in the open: eval, monitor, and govern for AI systems, plus an MCP server for trust evaluation and red-teaming. Does your org actually know its agent count, or is it a guess? The code is at karlmehta/trustmodel and the platform is at trustmodel.ai. Follow me here and on X at @karlmehta. #AISecurity #AIGovernance #MCP
AWS MCP Server 経由のエージェントを IAM で絞る :::note info この記事の英語版を AWS Builder Center に公開しています: Scoping an AI Agent on the AWS MCP Server with IAM ::: 1. 要点 - AI エージェントに MCP サーバ経由で AWS を操作させると、何も絞らない限り、エージェントは自分の認証情報でできる操作をすべて実行できる。依頼していない操作も含む - エージェントの操作を絞るには、エージェント専用の IAM ユーザーやロールを別に保守するか、MCP サーバ側の設定を使うことになる。MCP サーバ側の読み取り専用の設定は、私が試したものではオンとオフの切り替えだけで、「読み取りは広く許可し、書き込みは決めた場所だけ許可する」という指定はできなかった - AWS が管理する MCP サーバを経由した呼び出しは、IAM ポリシーの条件で見分けられる。これを使うと、自分の権限はそのままに、既存のポリシーに拒否の文を足して MCP 経由の操作だけを狭められる。ただし、シェルから AWS CLI を実行するなど、MCP を経由しない操作はこの条件では見分けられない - 今回は、MCP 経由で使える操作の一覧をポリシーに書き、読み取りは広く、書き込みは決めた保存先だけに絞った範囲で、エージェントに調査とレポート作成を任せられた。IAM では SQL の中身を見分けられないため、DB 側の権限と組み合わせた 検証はすべて、2026-10-03 に ap-northeast-1(東京)で行いました。 2. 背景 エージェントは、依頼文で指定していない手段を選ぶことがあります。手元の AWS 認証情報をそのまま使わせていれば、その手段もそのまま実行されます。今回の検証でも、作成途中のポリシーで動かしたエージェント(Claude Code)は、DB 接続用に用意した読み取り専用のシークレットのほかに、管理者シークレット(RDS が管理するマスターユーザーのシークレット)でも SQL を実行しようとしました。依頼文では、どのシークレットを使うかを指定していません。このときは IAM で拒否していたため、SQL は実行されていません(後半の売上分析の例で詳しく書きます)。 エージェントの操作を絞る方法は、これまで 2 つありました。1 つは、エージェント専用の IAM ユーザーやロールを用意する方法です。この方法では、認証情報とポリシーを自分のものとは別に保守する必要があります。 もう 1 つは、MCP サーバ側の設定を使う方法で、設定の方式はサーバごとに異なります。AWS Labs の PostgreSQL MCP Server は、書き込みの可否をフラグ --allow_write_query 1 つで切り替えます。README には、読み取り専用モードについて「ベストエフォートの多層防御の仕組みで、セキュリティ境界ではない」と書かれています。AWS が管理する MCP サーバ(AWS MCP Server)に接続するためのプロキシ mcp-proxy-for-aws にも、--read-only フラグがあります。このフラグを付けて試したところ、AWS API を呼ぶためのツール aws___run_script がツール一覧に出なくなり、データも読めなくなりました。どちらのフラグも、読み取り専用にするかどうかの切り替えで、書き込みを特定の保存先だけに許可することはできませんでした。 AWS MCP Server では、MCP 経由の呼び出しを IAM ポリシーの条件で見分けられます。そのため、認証情報は自分のもののまま、MCP 経由の呼び出しにだけ適用される Deny の文を既存のポリシーに足し、操作ごとに許可と拒否を書き分けられます。この記事では、この方法で MCP 経由で許可する操作を決め、読み取りだけで済むサポート調査(ユースケース 1)と、書き込み先を 1 か所に絞った売上分析(ユースケース 2)をエージェントに任せた結果を書きます。 3. AWS MCP Server の認可の仕組み AWS MCP Server は、2026-05-06 に GA となったマネージド型の MCP エンドポイントで、追加料金はかかりません。2026-10-02 には東京を含む 6 リージョンが追加され、使えるリージョンは 8 つになりました。エージェントは mcp-proxy-for-aws 経由で接続し、プロキシが手元の AWS 認証情報でリクエストに署名します。エージェントが AWS API を呼ぶときに使うツールは、aws___run_script です。このツールはサンドボックスで Python を実行し、その中で call_boto3 という関数を使えます。 ユーザーガイドの IAM のページによると、このサーバは独自の IAM アクション、リソース、サービス固有の条件キーを定義していません。呼び出し先のサービスが、ほかの AWS API 呼び出しと同じく既存のポリシーで認可します。そのため、条件キーを使わなければ、エージェントにできることは、その IAM プリンシパルにできることと同じです。AWS は呼び出しにグローバル条件キーを 2 つ付けます。 | 条件キー | 型 | 意味 | |---|---|---| | aws:ViaAWSMCPService | Bool | AWS が管理する MCP サーバのどれかを経由したリクエストで true | | aws:CalledViaAWSMCP | String | 呼び出し元の MCP サーバのサービスプリンシパル。例: AWS MCP Server なら aws-mcp.amazonaws.com | 今回は aws:ViaAWSMCPService を使いました。AWS MCP Server だけに絞り、ほかの AWS 管理の MCP サーバを対象外にする場合は、aws:CalledViaAWSMCP を使います。 4. MCP 経由で許可する操作と拒否する操作 今回の IAM ユーザーには PowerUserAccess を付けています。このユーザーは、私が AWS CLI からも使っているものです。このユーザーに付けている既存のカスタマー管理ポリシーに、MCP 経由で使える操作の一覧(許可リスト)を書きました。使ったのは Deny の文です。NotAction に許可リストの操作を並べ、aws:ViaAWSMCPService を条件にして、許可リストにない操作を MCP 経由ではすべて拒否します。さらに、S3 の書き込み先を reports/agent/ に限る文と、管理者シークレットの取得を拒否する文を足しました。どの文も権限を与えず、MCP 経由の呼び出しから権限を取り除くだけです(全文は付録 A)。下の表の「許可」は、これらの Deny で拒否しない操作を指します。 Allow で書かなかったのは、ユーザーに付けたポリシー同士では Allow が足し合わされるためです。Allow を足しても PowerUserAccess の Allow は狭まりません。ユーザーのポリシーの中で狭められるのは、明示的な Deny です。Allow の形で上限を書く方法にはアクセス許可の境界(permissions boundary)がありますが、今回は試していません。 | 対象 | MCP 経由で許可 | MCP 経由で拒否 | |---|---|---| | DynamoDB | 読み取り: GetItem、BatchGetItem、Query、Scan、PartiQL の SELECT、Describe*、List* | すべての書き込み(UpdateItem、PutItem、DeleteItem、BatchWriteItem、TransactWriteItems、PartiQL の書き込み)と、タグなどテーブル設定の変更 | | Aurora(RDS Data API) | 管理者シークレット以外のシークレットでの ExecuteStatement。cbsr-readonly では PostgreSQL が SELECT だけを許可 | 管理者シークレット(RDS が管理するマスターユーザーのシークレット)、BatchExecuteStatement などほかの Data API 操作 | | Secrets Manager | ListSecrets、DescribeSecret、管理者シークレット以外のすべてのシークレットへの GetSecretValue。エージェントはシークレットの値を読める | 管理者シークレット | | S3 | 読み取り: GetObject、ListBucket<br>書き込み: レポート用バケットの reports/agent/ の下への PutObject | ほかのプレフィックスへの書き込み、DeleteObject、バケットの設定 | | その他 | RDS、CloudWatch、CloudWatch Logs の読み取り、sts:GetCallerIdentity、すべての鍵での kms:Decrypt | 許可リストにない操作すべて | 読み取りを許可した範囲は、一覧のサービスのすべてのテーブル、バケット、シークレットです。以下の依頼にある「作業の対象は〜だけ」はプロンプトでの指示で、IAM の制限ではありません。 IAM のポリシーは SQL 文の内容を条件にできないため、Aurora の読み取り専用は DB ロールの権限で実現しました。 今回は、管理者シークレット以外のシークレットが cbsr-readonly だけだったため、管理者シークレット以外に対する GetSecretValue は絞っていません。本番では、GetSecretValue もエージェントが使うシークレットだけに絞る必要があります。 5. 検証環境 | 項目 | 内容 | |---|---| | AWS MCP Server | 東京エンドポイント(ホスト aws-mcp.ap-northeast-1.api.aws、パス /mcp)。mcp-proxy-for-aws 1.7.0 経由で接続 | | DynamoDB | テーブル cbsr-orders(注文 12 件。属性は customer_id、status、total_jpy、created_at)と GSI customer_id-created_at-index | | Aurora | PostgreSQL 17.9 Serverless v2(RDS Data API 有効)。テーブル customers(email 列あり)と orders | | DB ロール | cbsr_ro が読めるのは、orders の全体と、customers の customer_id 列と name 列だけ。認証情報はシークレット cbsr-readonly に保存 | | S3 | バケット cbsr-reports-<account-id>(ブロックパブリックアクセス有効) | | エージェント | Claude Code 2.1.269 をヘッドレス(claude -p)で起動。モデルは claude-opus-5[1m]。AWS MCP Server だけをつなぎ(--strict-mcp-config)、シェル、ファイル操作、Web のツールは禁止。Claude Code のほかの組み込みツールは使える状態だったが、エージェントは使っていない。依頼も返答も日本語 | | 境界の確認 | 各ユースケースの「境界の確認」の表に挙げた操作は、私が書いた Python の MCP クライアントから MCP 経由で呼んだ。主な書き込みは AWS CLI でも試した | 6. ユースケース 1: サポート対応の調査を、読み取りだけで任せる 6.1. エージェントへの依頼 > 顧客 c-02 から「注文 o-1003 がまだ届かない」と問い合わせが来ました。o-1003 の状況と、この顧客の注文履歴を調べて、担当者向けに状況をまとめ、顧客への回答文の下書きを作ってください。倉庫からは「o-1003 は本日発送済み」と連絡が来ています。ステータスの更新は担当者が行うので、あなたはデータを変更しないでください。作業の対象は cbsr-orders だけにしてください。最後に、実行したことを報告してください。 6.2. エージェントがしたこと 1. DescribeTable を呼んだあと、GetItem で o-1003 を、GSI への Query でこの顧客の注文を読んだ 2. 担当者向けのまとめで、倉庫は発送済みと連絡しているのに、テーブルのステータスは PROCESSING のままだと指摘した 3. 同じまとめで、テーブルに配送関連の属性が無いため、追跡番号は倉庫に確認する必要があると書いた。ステータスの更新は担当者の作業として挙げた 4. 追跡番号と配送業者を空欄にした回答文を下書きし、データは読んだだけだと報告した 6.3. 境界の確認 | 操作 | 想定 | 結果 | |---|---|---| | GetItem、Query(GSI)、Scan、BatchGetItem、PartiQL の SELECT | 許可(読み取り) | 許可 | | UpdateItem、TransactWriteItems(更新) | 拒否(書き込み) | 拒否 | | PartiQL の UPDATE | 拒否(書き込み) | 拒否 | | PutItem、DeleteItem、BatchWriteItem | 拒否(書き込み) | 拒否 | | テーブルへの TagResource | 拒否(テーブル設定) | 拒否 | 結果はすべて想定どおりで、データも変わっていません。件数は 12 件のままで、o-1003 も PROCESSING のままです。同じユーザーで AWS CLI からはステータスを更新でき、すぐ元に戻しました。 7. ユースケース 2: 売上の分析を、書き込み先を 1 か所に絞って任せる 7.1. エージェントへの依頼 > 9/15 以降(キャンセル済みを除く)の売上について、商品別の販売数と売上金額、顧客別の売上金額を集計し、気づいたことを添えた週次レポートを Markdown で作ってください。できたレポートは S3 バケット cbsr-reports-<account-id> の reports/agent/2026-10-03-weekly-sales.md に保存してください。作業の対象は cbsr-aurora とこのバケットだけにしてください。最後に、実行したことを報告してください。 7.2. エージェントがしたこと 1. 管理者シークレットと cbsr-readonly の 2 つを見つけ、読み取り専用の cbsr-readonly を選んだ 2. customers への SELECT * が permission denied for table customers になり、customers は読めないと判断して、orders だけで集計した。途中経過の文では、管理者シークレットは使わないと書いた 3. キャンセル済みと 9/15 より前の注文を除いた 10 件(合計 45,200 円)を、商品別、顧客別、週別、単価別に集計し、所見を 7 つ添えてレポートを書いた 4. 保存先のキーにオブジェクトが無いことを確認してから PutObject で保存し、保存後に HeadObject で確認した SELECT * が拒否されたのは、cbsr_ro に権限の無い email 列が含まれていたためで、実際には customer_id と name は読めます。顧客別の売上は orders の customer_id で集計しているため、この判断は集計結果に影響していません。 途中の版のポリシーで動かした 2 回の実行では、エージェントは管理者シークレットで SQL を実行しようとしました。1 回目は、同じスクリプトの中で読み取り専用のシークレットと管理者シークレットの両方を使っています。2 回目は、顧客のメールアドレスも求める依頼で権限エラーになったあと、「管理用シークレットで試します」と書いてから実行しました。管理者シークレットを拒否する DenyAuroraAdminSecretViaMcp の文は最初の版から入れてあり、2 回とも IAM が拒否しました。最終の版にも、この文を残しています。 7.3. 境界の確認 | 操作 | 想定 | 結果 | |---|---|---| | cbsr-readonly での集計クエリ | 許可(読み取り) | 許可 | | バケットへの GetObject、ListObjectsV2 | 許可(読み取り) | 許可 | | reports/agent/ の下への PutObject | 許可(唯一許可したプレフィックス) | 許可 | | reports/finance/ の下やバケット直下への PutObject | 拒否(ほかのプレフィックス) | 拒否 | | DeleteObject、PutBucketPolicy | 拒否(許可リストにない) | 拒否 | | cbsr-readonly での DELETE FROM orders | PostgreSQL が拒否(SELECT のみ) | PostgreSQL が拒否 | | 管理者シークレットでの SQL | IAM が拒否 | IAM が拒否(Failed to fetch secret ... explicit deny) | 結果はすべて想定どおりでした。同じユーザーで AWS CLI からは reports/finance/ の下に保存でき、オブジェクトも削除できました。 8. AWS の推奨との関係 AWS Security Blog の 2 本の記事(「Understanding IAM for Managed AWS MCP Servers」と「Secure AI agent access patterns to AWS resources using Model Context Protocol」)では、条件キーで MCP 経由の操作と人が直接行う操作を区別すること、読み取りで済む作業は読み取り専用にすること、人より狭い権限のエージェント専用ロールを作ることが勧められています。今回の構成は、このうち前の 2 つに当たります。エージェント専用のロールは作らず、自分の IAM ユーザーのまま、MCP 経由の操作だけを狭めました。人の権限を最小権限に絞っていても、人の仕事に必要な範囲と、エージェントに任せる作業に必要な範囲は一致しないため、同じ認証情報を共有するなら、エージェントに不要な分は条件キーで MCP 経由だけ拒否することになります。今回は検証用に PowerUserAccess を使ったので差が大きくなっていますが、考え方は同じです。 後者の記事には、アクセス許可の境界はエージェント専用のロールに付けたときに最も効果があり、開発者が既存のロールに付けるとエージェント以外の使い方まで制限される、と書かれています。 同じ記事には、aws:ViaAWSMCPService が付くのは AWS が管理する MCP サーバを経由した呼び出しだけだ、とも書かれています。エージェントがシェルのツールで AWS CLI を実行すると、同じ API を呼んでも、aws:ViaAWSMCPService を条件にした Deny は適用されません。今回の検証では、エージェントにシェルのツールを使わせていません。ユースケース 1、2 で、同じユーザーの AWS CLI からは更新や保存ができました。これも、CLI の呼び出しにこの条件キーが付かないためです。 9. まとめ aws:ViaAWSMCPService で MCP 経由の操作だけを制限すると、自分の権限を変えずに、エージェントに渡す範囲を決められました。本番で使う場合は、読み取りもリソースの条件で必要なテーブルやバケットに絞り、GetSecretValue をエージェント用のシークレットに限る必要があります。エージェントにシェルのツールを使わせる場合、シェルから実行した AWS CLI の呼び出しには aws:ViaAWSMCPService が付きません。その場合は、AWS Security Blog が勧めるように、エージェント専用のロールで権限そのものを狭めます。 10. 付録 A: ポリシーの全文 IAM ユーザーの既存のカスタマー管理ポリシーに、次の文を足しました。<id> は、RDS が管理する管理者シークレットの名前の一部です。 { "Sid": "DenyUnlistedActionsViaMcp", "Effect": "Deny", "NotAction": [ "dynamodb:GetItem", "dynamodb:BatchGetItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:PartiQLSelect", "dynamodb:Describe*", "dynamodb:List*", "rds:Describe*", "rds:List*", "rds-data:ExecuteStatement", "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:ListSecrets", "cloudwatch:GetMetricData", "cloudwatch:GetMetricStatistics", "cloudwatch:ListMetrics", "cloudwatch:DescribeAlarms", "logs:Describe*", "logs:Get*", "logs:FilterLogEvents", "logs:StartQuery", "sts:GetCallerIdentity", "kms:Decrypt", "s3:PutObject", "s3:GetObject", "s3:ListBucket" ], "Resource": "*", "Condition": { "Bool": { "aws:ViaAWSMCPService": "true" } } }, { "Sid": "DenyMcpPutOutsideAgentReports", "Effect": "Deny", "Action": "s3:PutObject", "NotResource": "arn:aws:s3:::cbsr-reports-<account-id>/reports/agent/*", "Condition": { "Bool": { "aws:ViaAWSMCPService": "true" } } }, { "Sid": "DenyAuroraAdminSecretViaMcp", "Effect": "Deny", "Action": "secretsmanager:GetSecretValue", "Resource": "arn:aws:secretsmanager:ap-northeast-1:<account-id>:secret:rds!cluster-<id>-*", "Condition": { "Bool": { "aws:ViaAWSMCPService": "true" } } } - 1 つ目の文は、一覧にない操作をすべて拒否する。一覧にある書き込みは 2 つだけで、s3:PutObject は 2 つ目の文で reports/agent/ に限り、rds-data:ExecuteStatement の SQL は DB ロールで制限する - kms:Decrypt は Data API のために一覧に入れた(付録 B)。この書き方では、MCP 経由でどの鍵でも復号できる。絞り込んだ書き方は試していない 11. 付録 B: つまずいた点 - データの書き込みだけを拒否する書き方では、管理操作は拒否されなかった。途中の版のうち最初の版では、DynamoDB の書き込み系アクションを Deny に並べていたが、DynamoDB のテーブルと Aurora クラスタへのタグ付けが MCP 経由でも成功した。拒否の対象から除く操作を NotAction に並べた Deny にすると、試した管理操作(タグ付け)はすべて拒否された - Data API には kms:Decrypt が必要だった。NotAction の書き方に変えたあと、読み取り専用のシークレットでの呼び出しも Access to KMS is not allowed で失敗し、kms:Decrypt を足すと成功した。シークレット取得時の復号にも MCP の条件が適用されていると推測しているが、ドキュメントでは確認できていない - DynamoDB で UpdateItem を status 属性だけに許可する書き方(dynamodb:Attributes を使う)も試した。条件式で参照しただけの属性も、dynamodb:Attributes の評価対象に含まれた。また、存在しない注文番号への更新では新しいアイテムが作られた。ユースケース 1 を読み取り専用にしたのは、このため 11.1. 検証条件の注意 - 起動したエージェントに質問すると、Claude Code の組み込みの指示に、取り消しにくい操作の前に確認を求める趣旨の記述があると回答した(この回答の内容は確かめていない)。エージェントが実行しなかった操作には、ポリシーではなくこの指示によるものも含まれる可能性がある 参考 - AWS MCP Server の GA(What's New、2026-05-06): aws.amazon.com - AWS MCP Server の 6 リージョン追加(What's New、2026-10-02): aws.amazon.com - AWS MCP Server と IAM: docs.aws.amazon.com - AWS MCP Server の IAM ポリシーの例: docs.aws.amazon.com - AWS MCP Server のツール: docs.aws.amazon.com - Understanding IAM for Managed AWS MCP Servers(AWS Security Blog、2026-03-02): aws.amazon.com - Secure AI agent access patterns to AWS resources using Model Context Protocol(AWS Security Blog、2026-04-14): aws.amazon.com - IAM ポリシーの条件による細かいアクセス制御(DynamoDB): docs.aws.amazon.com - RDS Data API: docs.aws.amazon.com - AWS Labs PostgreSQL MCP Server(README): github.com/awslabs/mcp/blob/main/src/postgres-mcp… - mcp-proxy-for-aws: aws/mcp-proxy-for-aws