<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Ernst</title>
    <description>The latest articles on DEV Community by David Ernst (@dmernst).</description>
    <link>https://dev.to/dmernst</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038693%2F19d37645-e7ae-4e4b-8466-cc5b4167f7e7.png</url>
      <title>DEV Community: David Ernst</title>
      <link>https://dev.to/dmernst</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmernst"/>
    <language>en</language>
    <item>
      <title>The Easy Button for AWS Account Details and Cost Savings</title>
      <dc:creator>David Ernst</dc:creator>
      <pubDate>Wed, 28 Oct 2026 01:14:07 +0000</pubDate>
      <link>https://dev.to/dmernst/the-easy-button-for-aws-account-details-and-cost-savings-1dkd</link>
      <guid>https://dev.to/dmernst/the-easy-button-for-aws-account-details-and-cost-savings-1dkd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbe7rnlnk2vcrlzbjw147.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbe7rnlnk2vcrlzbjw147.png" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How I gave an AI agent enough access to map an AWS account and build a savings report, and no&amp;nbsp;more.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first question I get when I show an AI agent doing discovery on a live AWS account isn’t about speed. It’s about&amp;nbsp;access.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“You gave it&amp;nbsp;what?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s the right question. An agent that maps an account in minutes is useful. An agent that can change things in someone else’s account is a liability. The speed is what gets attention. &lt;strong&gt;The read-only part is what makes it safe to do at&amp;nbsp;all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post isn’t a getting-started tutorial. It walks through the role, the two MCP servers, the places where “read-only” quietly wasn’t, and what the agent found when I pointed it at a real bill. The key code is inline, and the full Terraform, Kiro config, prompts, and CloudTrail check script are in the companion repo: &lt;a href="https://github.com/dmernst/aws-readonly-discovery-agent" rel="noopener noreferrer"&gt;https://github.com/dmernst/aws-readonly-discovery-agent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do this a day ahead:&lt;/strong&gt; opt in to Cost Optimization Hub and Compute Optimizer. Both are free, and each can take up to 24 hours to produce its first recommendations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS MCP Server&lt;/strong&gt;, the managed remote server that became generally available in May 2026. It gives the agent the whole AWS API through a sandboxed &lt;code&gt;aws___run_script&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Billing and Cost Management MCP server&lt;/strong&gt;, the open-source server from AWS Labs. It wraps Cost Explorer, Cost Optimization Hub, Compute Optimizer, Savings Plans, Budgets, and the Price List&amp;nbsp;API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6t6238o1wvreh4nnysv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6t6238o1wvreh4nnysv.png" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1. Two MCP servers, one read-only role, and CloudTrail as the record. The customer’s organization adds its own guardrail on&amp;nbsp;top.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One is managed and remote; the other runs on my laptop. That difference matters, and I’ll come back to&amp;nbsp;it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-only
&lt;/h2&gt;

&lt;p&gt;When people say “give it read-only access,” they usually mean: attach ReadOnlyAccess and move on. That's the wrong policy for this job. ReadOnlyAccess includes reading &lt;strong&gt;data&lt;/strong&gt;, not just metadata: s3:GetObject, DynamoDB scans, CloudWatch Logs events. A discovery agent needs to know a bucket exists and how big it is. It doesn't need to open the&amp;nbsp;objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Read-only should mean the agent can see the shape of the environment, not its contents.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the guardrail is four layers, and each one catches something the others&amp;nbsp;miss:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;IAM role:&lt;/strong&gt; metadata reads plus a scoped cost policy, with an explicit deny on data-plane reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro approvals:&lt;/strong&gt; with Autopilot off, I approve every tool call and shell command before it&amp;nbsp;runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro steering:&lt;/strong&gt; written rules about what the agent does, what it cites, and when it&amp;nbsp;stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail:&lt;/strong&gt; proof after the&amp;nbsp;fact.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Layer 1: The IAM&amp;nbsp;role
&lt;/h3&gt;

&lt;p&gt;The role starts from ViewOnlyAccess, the AWS managed job-function policy for listing and describing resources without reading their contents. Sessions are capped at one hour; a discovery run is an afternoon, not a&amp;nbsp;week.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"discovery"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role_name&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Read-only discovery and cost analysis for AI agents via MCP"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trust&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
  &lt;span class="nx"&gt;max_session_duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max_session_hours&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"view_only"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discovery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trust policy names exactly who can assume it, with an optional external ID for when a partner assumes the role into a customer&amp;nbsp;account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"trust"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowNamedPrincipals"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sts:SetSourceIdentity"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trusted_principal_arns&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;    &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="s2"&gt;"condition"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;external_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
        &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:ExternalId"&lt;/span&gt;
        &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The cost permissions: where “read-only” quietly&amp;nbsp;isn’t
&lt;/h3&gt;

&lt;p&gt;The Billing and Cost Management server’s README lists the permissions it needs. Most are what you’d expect: ce:Get*, compute-optimizer:Get*, pricing:GetProducts. Read the whole list and you'll also find s3:PutObject, s3:PutStorageLensConfiguration, athena:CreateWorkGroup, athena:CreateTable, and several ce:Start* calls. None of it is malicious; Storage Lens needs somewhere to write Athena results. But &lt;strong&gt;copying the README's list gives your "read-only" agent write access to S3 and&amp;nbsp;Athena.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I scoped it to Get, List, and Describe, and gave up Storage Lens analysis in exchange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CostExplorerRead"&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetCostAndUsage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetCostAndUsageWithResources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetCostAndUsageComparisons"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetCostComparisonDrivers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetCostForecast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetUsageForecast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetDimensionValues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetTags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetCostCategories"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetAnomalies"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetReservationCoverage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetReservationUtilization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetReservationPurchaseRecommendation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetSavingsPlansCoverage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetSavingsPlansUtilization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetSavingsPlansUtilizationDetails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetSavingsPlansPurchaseRecommendation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:GetSavingsPlanPurchaseRecommendationDetails"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:ListCostAllocationTags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:ListCostCategoryDefinitions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ce:DescribeCostCategoryDefinition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the cost policy follows the same pattern for Cost Optimization Hub, Compute Optimizer (including GetIdleRecommendations), Savings Plans, Budgets, Pricing, Free Tier, and CloudWatch metrics. It cost nothing in practice: CloudTrail shows 31 billing calls from the agent, and none was&amp;nbsp;denied.&lt;/p&gt;

&lt;h3&gt;
  
  
  The explicit&amp;nbsp;deny
&lt;/h3&gt;

&lt;p&gt;The last statement denies data-plane reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DenyDataPlaneReads"&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"s3:GetObjectVersion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ssm:GetParameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ssm:GetParameters"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ssm:GetParametersByPath"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"dynamodb:GetItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"dynamodb:BatchGetItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"dynamodb:Query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"dynamodb:Scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"logs:GetLogEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"logs:FilterLogEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"logs:StartQuery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"lambda:GetFunction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ec2:GetPasswordData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"ec2:GetConsoleOutput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"rds-data:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"sqs:ReceiveMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"kinesis:GetRecords"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"athena:GetQueryResults"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"glue:GetConnection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"glue:GetConnections"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ViewOnlyAccess shouldn't grant most of these today. The deny is insurance against tomorrow: a managed policy update, or a teammate attaching one more policy "just for this run." An explicit deny wins over any allow. (lambda:GetFunction is here because its response includes a link to download the function's code.) Writes need no deny; nothing in the role allows&amp;nbsp;one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The condition keys, and the gap they&amp;nbsp;leave
&lt;/h3&gt;

&lt;p&gt;The AWS MCP Server adds two condition keys to every request it forwards, aws:ViaAWSMCPService and aws:CalledViaAWSMCP, so IAM policies and SCPs can treat agent traffic differently from a&amp;nbsp;human's.&lt;/p&gt;

&lt;p&gt;The gap: &lt;strong&gt;only AWS managed MCP servers add those keys.&lt;/strong&gt; The billing server runs locally and calls Cost Explorer with my profile’s credentials. To IAM, those calls look exactly like me running the AWS&amp;nbsp;CLI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Condition keys are a good second layer. The role has to be the&amp;nbsp;first.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy it
&lt;/h3&gt;

&lt;p&gt;Two things before the first command. &lt;strong&gt;Get off root:&lt;/strong&gt; my first aws login on this laptop was as the root user, which can't assume a role and can't be limited by policy, so I created an IAM user with an admin profile for the deploy. &lt;strong&gt;Say no to the CLI's offer:&lt;/strong&gt; recent versions of aws configure offer to "Configure AWS skills and the AWS MCP server for your AI coding agent(s)." That installs at the user level with your admin profile. The only MCP config should be the one in the workspace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;terraform
&lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars   &lt;span class="c"&gt;# set trusted_principal_arns&lt;/span&gt;
terraform init
terraform plan &lt;span class="nt"&gt;-out&lt;/span&gt; tfplan
terraform apply tfplan
terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; aws_config_profile &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.aws/config   &lt;span class="c"&gt;# then set source_profile&lt;/span&gt;
aws sts get-caller-identity &lt;span class="nt"&gt;--profile&lt;/span&gt; mcp-discovery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan should show one role, one policy attachment, and two inline policies; anything else, stop and read it. If plan fails with No valid credential sources found, set AWS_PROFILE to your admin profile. And if you write your own boundary tests, skip ec2 create-tags --dry-run on a made-up ID: EC2 checks the resource before the permission, so it returns InvalidVpcID.NotFound and proves nothing. create-vpc --dry-run and a real s3api create-bucket give clean&amp;nbsp;denials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Wiring both servers into&amp;nbsp;Kiro
&lt;/h2&gt;

&lt;p&gt;Kiro reads MCP servers from&amp;nbsp;.kiro/settings/mcp.json in the workspace, which takes precedence over the user-level file. Two gotchas: open the folder that contains&amp;nbsp;.kiro/, not its parent, and &lt;strong&gt;trust the folder&lt;/strong&gt;, then quit Kiro completely and reopen it. Until then the log says workspace scope skipped: workspace is not trusted and nothing&amp;nbsp;starts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aws-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"mcp-proxy-for-aws-cli@1.7.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"https://aws-mcp.us-east-1.api.aws/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS_REGION=us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-discovery"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"autoApprove"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"billing-cost-management"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"awslabs.billing-cost-management-mcp-server@0.0.39"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FASTMCP_LOG_LEVEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS_PROFILE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-discovery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS_REGION"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"autoApprove"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SigV4 through the proxy lets both servers share one profile, so CloudTrail shows one role and one session name. Versions are pinned, because in a customer account I want to know exactly what ran. autoApprove is empty and Autopilot is off, so Kiro asks before every tool call and shell&amp;nbsp;command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0u6uv5ellqmj1bwd31if.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0u6uv5ellqmj1bwd31if.png" width="800" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2. Autopilot off (bottom right): every tool call and shell command waits for my approval.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I asked:&lt;/strong&gt; &lt;em&gt;“Which AWS account and role are you connected as? List the tools you have from each MCP server, and tell me which of them can&amp;nbsp;write.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first config had one more line: --read-only. AWS's setup guide says it hides write-capable tools, which sounded like exactly what I wanted. The smoke test showed what it actually does: aws-mcp came up with six tools, and &lt;strong&gt;&lt;code&gt;aws___run_script&lt;/code&gt;, the one tool that calls AWS APIs, was gone.&lt;/strong&gt; The agent could read about AWS all day; it couldn't list a single resource in my&amp;nbsp;account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjyabt4n1u0jy15kk1uk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjyabt4n1u0jy15kk1uk.png" width="799" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3. With *&lt;/em&gt;--read-only: six tools, and none of them can see the&amp;nbsp;account.*&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;The flag hides tools. The role decides what any tool can actually&amp;nbsp;do.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I removed it. I’d already tested the role from the CLI: ec2 create-vpc returned UnauthorizedOperation, s3api create-bucket returned AccessDenied, and ssm get-parameter came back with an explicit deny in an identity-based policy. The agent now sees a tool that could write, and AWS rejects the call if it&amp;nbsp;tries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl82hvbl2xf761epstyx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl82hvbl2xf761epstyx3.png" width="300" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4. Without the flag: eight tools, including *&lt;/em&gt;&lt;code&gt;aws___run_script.&lt;/code&gt;*&lt;/p&gt;

&lt;p&gt;The billing server listed about 50 tools, and Kiro flagged two as write-capable on its own: the Savings Plans start_* jobs. The role doesn't allow ce:Start*, so both would be&amp;nbsp;denied.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0peuql68ozphwo0nenl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe0peuql68ozphwo0nenl.png" width="799" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5. Kiro’s audit of the billing server: about 50 tools, two that can&amp;nbsp;write.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The smoke test also showed a path I hadn’t planned for. &lt;strong&gt;Before touching either MCP server, the agent ran the aws CLI in Kiro's terminal.&lt;/strong&gt; It failed only because I don't keep a default profile, but an admin profile one --profile flag away is a second road into the account that skips the role entirely. I closed it with a steering rule and Autopilot off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Steering the&amp;nbsp;agent
&lt;/h2&gt;

&lt;p&gt;Steering files are Markdown rules the agent includes in every conversation. IAM decides what the agent &lt;em&gt;can&lt;/em&gt; do. Steering decides what it &lt;em&gt;should&lt;/em&gt; do, and how it reports&amp;nbsp;back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;inclusion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Discovery and savings rules
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Read only. Never attempt a create, update, delete, start, stop, tag, or put call, even if a tool offers one. If a finding needs a change, write it as a recommendation for a human.
- Evidence for every finding: the tool and API, the resource ARN or ID, the time window, and the numbers.
- Separate what you observed from what you inferred.
- No secrets or data. If a call is denied, record the denial and move on; do not look for a workaround.
- Cost Explorer calls cost money (about $0.01 per request). Use monthly granularity unless daily is needed, and don't repeat the same query.
- AWS access only through the MCP servers. Never run the aws CLI or SDK scripts in the terminal, and never pass --profile or read ~/.aws.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule I care most about is the third one. Compute Optimizer gives you a measured recommendation; an agent reasoning from metadata gives you an inference. Both are useful, and they should never share a column in a report. It’s the same habit I learned in Bar Raiser debriefs: what did we observe, and what are we assuming?&lt;/p&gt;

&lt;h2&gt;
  
  
  The account
&lt;/h2&gt;

&lt;p&gt;I didn’t want to demo this on an empty account, so I used a sandbox that runs a small serverless app and belongs to an AWS Organization. I don’t own it, which made it the right test: this is how a partner gets into a customer account. The account’s admin deployed the same Terraform, trusting only my user and requiring an external ID, and I added one&amp;nbsp;profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;profile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;mcp-discovery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;role_arn&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;arn:aws:iam::&amp;lt;their-account-id&amp;gt;:role/mcp-discovery-readonly&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;source_profile&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;my-signin-profile&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;external_id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;agreed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;external&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;role_session_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kiro-discovery&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That took about ten minutes, with a separate copy of the Terraform folder and its own state, so a terraform destroy could never hit the wrong account. Before handing it to the agent, I checked the boundary from the CLI. Reads worked and writes were denied, and the most reassuring check: &lt;strong&gt;calling AssumeRole without the external ID was denied, even from the user the role&amp;nbsp;trusts.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery: mapping the&amp;nbsp;account
&lt;/h2&gt;

&lt;p&gt;My first sweep hit a guardrail I hadn’t put there. Thirteen of seventeen Regions came back with&amp;nbsp;this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;... is not authorized to perform: ec2:DescribeVolumes with an explicit deny in a service control policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The organization’s SCP limits the account to four US Regions, and my role can’t override it, which is exactly how it should work. The error text tells you which layer said no: “identity-based policy” means my role, “service control policy” means the customer’s organization.&lt;/p&gt;

&lt;p&gt;Then the agent made its first real mistake. It wrote a regex over its own tool output to sort allowed Regions from denied ones, got the lists backwards, and kept going. I stopped it and started a fresh chat with the four Regions named up&amp;nbsp;front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I asked:&lt;/strong&gt; &lt;em&gt;“A service control policy limits this account to us-east-1, us-east-2, us-west-1, and us-west-2, so only scan those four Regions and don’t treat denials elsewhere as findings. Do a read-only discovery: inventory compute, storage, databases, networking, and anything that bills hourly. Write **reports/inventory.md with counts by service and Region and a Mermaid diagram of how the main pieces connect."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrtu0c2ph9pdljzcxk0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrtu0c2ph9pdljzcxk0l.png" width="750" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6. Second try: four Regions named up front, services swept in parallel batches. 8 minutes, 8 tool calls, 2.68 Kiro&amp;nbsp;credits.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It found a tidy serverless app, almost entirely in us-east-1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute:&lt;/strong&gt; 9 Lambda functions; no EC2, containers, or Auto Scaling&amp;nbsp;groups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; 2 DynamoDB tables (on-demand) and 4 S3 buckets; no RDS, ElastiCache, or OpenSearch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Front door:&lt;/strong&gt; CloudFront in front of a static site, and one API Gateway REST&amp;nbsp;API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glue and operations:&lt;/strong&gt; 4 EventBridge schedules, 2 SNS topics, 12 CloudWatch alarms, and 4 Secrets Manager&amp;nbsp;secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; default VPCs&amp;nbsp;only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F524qxnrvxyjlichvg3lb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F524qxnrvxyjlichvg3lb.png" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 7. What discovery mapped, and the two services only the bill revealed.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Widening the role for metadata, not&amp;nbsp;data
&lt;/h3&gt;

&lt;p&gt;The inventory lists all four secrets by name (an OAuth client secret and three Slack tokens and webhooks) along with when each was last accessed. It can’t read a single value. That’s the line I’d point to in a security&amp;nbsp;review.&lt;/p&gt;

&lt;p&gt;It took one change. ViewOnlyAccess doesn't cover a few metadata calls, and on an early test run the agent reported them as denied instead of guessing. I granted exactly those&amp;nbsp;calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DiscoveryMetadataRead"&lt;/span&gt;
  &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"s3:GetBucketLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"s3:GetBucketVersioning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"secretsmanager:ListSecrets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"cloudwatch:DescribeAlarms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"amplify:ListApps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one returns names or settings, not contents; GetSecretValue is still in the explicit deny. &lt;strong&gt;&lt;em&gt;Widen the role for metadata when the agent shows you a gap. Never widen it for&amp;nbsp;data.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This run still reported three gaps: GuardDuty, Bedrock, and QuickSight. For the first two, the agent found the account’s service-linked roles and worked out both services were in use without asking for more access. QuickSight turned out to matter a lot, and the bill is how I found&amp;nbsp;out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The savings&amp;nbsp;pass
&lt;/h2&gt;

&lt;p&gt;The inventory says what exists. The bill says what it costs. The savings come from comparing them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Where the money&amp;nbsp;goes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I asked:&lt;/strong&gt; &lt;em&gt;“Using Cost Explorer, show the last three full months of cost by service and by Region, and the month-over-month change. What are the top five cost drivers, and is anything growing faster than the&amp;nbsp;rest?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3io3xtq39hl3me7tvei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff3io3xtq39hl3me7tvei.png" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The account went from under four dollars a month to $335 in two months, and two services account for 98% of it. The agent cited the Cost Explorer request IDs for both queries, which is the evidence the steering asks for. It also rewrote a report file I hadn’t asked for; the review bar caught it with a Revert button. Steering doesn’t stop the agent from writing files in your workspace. The review gate&amp;nbsp;does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: What AWS already&amp;nbsp;computed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I asked:&lt;/strong&gt; &lt;em&gt;“Pull the current recommendations from Cost Optimization Hub and Compute Optimizer, including idle resources. Also check Savings Plans and Reserved Instance coverage and utilization. If a service isn’t enrolled, tell me instead of guessing.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both services are opt-in, and neither has anything to show the moment you turn it on.&lt;/strong&gt; Cost Optimization Hub also pulls its rightsizing and idle findings from Compute Optimizer, so you need both. Opting in is a write, so the read-only role can’t do it; I ran it as the&amp;nbsp;admin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cost-optimization-hub update-enrollment-status &lt;span class="nt"&gt;--status&lt;/span&gt; Active &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws compute-optimizer update-enrollment-status &lt;span class="nt"&gt;--status&lt;/span&gt; Active &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[Recommendations actually took about X hours to appear.] Everything else in this post works immediately.&lt;/p&gt;

&lt;p&gt;[Results: the measured recommendations, with estimated monthly savings. Did the engines flag anything the cross-check in Step 3 missed, or the other way&amp;nbsp;around?]&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The bill knew what the inventory couldn’t
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I asked:&lt;/strong&gt; &lt;em&gt;“Using the inventory and a fresh Cost Explorer pull of the last three full months, cross-check the inventory against the bill. Start with the two biggest costs: for QuickSight, how many users, which edition, and how much SPICE capacity is in use versus paid for; for Neptune, the instance size and its CPU and connection metrics since it was created. Then check the usual misses in the four allowed Regions: unattached EBS volumes, old snapshots, idle load balancers, unused Elastic IPs, NAT gateways, and log groups with no retention. Back every finding with metadata or CloudWatch metrics.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The inventory had just reported a tidy serverless app. &lt;strong&gt;No QuickSight. No Neptune.&lt;/strong&gt; Yet those two services were 98% of the&amp;nbsp;bill.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7tue05h4txz6k75086n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7tue05h4txz6k75086n.png" width="750" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 8. “The bill reveals services the inventory missed entirely.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QuickSight was invisible because of my role.&lt;/strong&gt; Every QuickSight API call was denied, so the agent read the bill instead, grouping Cost Explorer by usage&amp;nbsp;type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;USE1-Author-Pro-Enterprise-Month: one Enterprise author seat,&amp;nbsp;&lt;strong&gt;$40&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;USE1-Amazon-Q-QS-Fee: the Amazon Q in QuickSight add-on,&amp;nbsp;&lt;strong&gt;$250&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No SPICE usage types at all: no data&amp;nbsp;loaded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Neptune was invisible because it was gone.&lt;/strong&gt; DescribeDBClusters returned nothing in all four Regions. The bill showed a db.r8g.xlarge that ran about 52 hours in September and about 230 in October, roughly $210 in total, deleted both times without a snapshot. A default parameter group was the only trace left. &lt;strong&gt;An inventory only sees what exists today. The bill remembers what existed last&amp;nbsp;month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitadwmr5yk63n4go8x7f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fitadwmr5yk63n4go8x7f.png" width="750" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 9. Everything here came from usage types and hours, not from the QuickSight or Neptune&amp;nbsp;APIs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The usual misses came back clean: no unattached volumes, snapshots, load balancers, Elastic IPs, or NAT gateways. The only hygiene finding was 11 log groups set to never expire, about 2 MB, so it costs nothing&amp;nbsp;here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8n7k630q0n2iuy4jcg8n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8n7k630q0n2iuy4jcg8n.png" width="750" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 10. The cross-check: 9 minutes, 3.15 credits, and every finding handed back to a&amp;nbsp;human.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it got&amp;nbsp;wrong
&lt;/h3&gt;

&lt;p&gt;Three claims didn’t survive my review, and they’re worth more to you than the ones that&amp;nbsp;did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Its first answer was confidently wrong.&lt;/strong&gt; Before it broke the bill down by usage type, it explained the $290 like&amp;nbsp;this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcskq4x5qz4n2fdg10lzm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcskq4x5qz4n2fdg10lzm.png" width="755" height="66"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 11. Plausible, precise, and&amp;nbsp;wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The arithmetic works; the explanation doesn’t. It was one $40 seat and a $250 add-on. &lt;strong&gt;When an agent explains a number, ask it what the number is made&amp;nbsp;of.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It invented a reason.&lt;/strong&gt; Finding no CloudWatch metrics for Neptune, it suggested the cluster had been stopped, and that “stopped clusters still accrue instance charges.” That’s backwards: AWS’s &lt;a href="https://docs.aws.amazon.com/neptune/latest/userguide/manage-console-stop-start.html" rel="noopener noreferrer"&gt;Neptune documentation&lt;/a&gt; says “you aren’t charged for any DB instance hours” while a cluster is stopped. It labeled this as a guess, but it’s exactly the kind of claim that sounds right in a&amp;nbsp;report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It priced savings on something that no longer exists:&lt;/strong&gt; “$178 to $535 a month” for a cluster already deleted. The honest number is $0 saved and about $210 already spent. The real recommendation is a process one: experiments need an owner and a budget&amp;nbsp;alert.&lt;/p&gt;

&lt;p&gt;It also suggested granting quicksight:Describe* to fill the QuickSight gap. I didn't: Describe* includes calls that return dashboard and dataset definitions, which is content. Widen for metadata, never for&amp;nbsp;data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The report
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjd62ijb1a7ecico3w94s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjd62ijb1a7ecico3w94s.png" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a $335 bill, that’s about 80%, almost all from one line item nobody would have found by looking at the resources. None of it was applied by the agent. &lt;strong&gt;The agent did the digging; the judgment stayed with&amp;nbsp;me.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Proving it stayed read-only
&lt;/h2&gt;

&lt;p&gt;The last thing I asked the agent was to break its own&amp;nbsp;rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I asked:&lt;/strong&gt; &lt;em&gt;“Create an empty S3 bucket named **mcp-write-test- in us-east-1."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I picked a write whose worst case is an empty bucket, not a deleted resource. A test of your guardrails shouldn’t be able to hurt you if they fail. It refused, and when I told it to try anyway, it refused&amp;nbsp;again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgtx8rvo51xa2zm3piru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgtx8rvo51xa2zm3piru.png" width="750" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 12. “The constraint is in the workspace rules I’m operating under, not just in IAM policy enforcement.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That proves the steering works, not the role, which is why I’d already tested the role myself from the CLI. &lt;strong&gt;Test each layer on its&amp;nbsp;own.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then the check that matters more than anything the agent says about itself: CloudTrail. I pulled every event from the kiro-discovery session across all four Regions. &lt;strong&gt;445 events. Zero writes succeeded.&lt;/strong&gt; The only write attempts were my two CLI tests, both denied. One catch if you run this yourself: the AWS MCP Server logs each of its own tool calls as a non-read-only event, so filter out aws-mcp.amazonaws.com before you count&amp;nbsp;writes.&lt;/p&gt;

&lt;p&gt;The denied reads told a story of their own. Twenty-four were kms:Decrypt calls the agent never made. Their source was lambda.amazonaws.com: when the agent listed the Lambda functions, Lambda tried to decrypt their environment variables on its behalf, and the explicit deny stopped it. Environment variables are where a lot of teams still keep secrets. &lt;strong&gt;None of them reached the agent, and the deny I'd added as insurance is the only reason&amp;nbsp;why.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The records also show the condition-key gap from earlier. Calls through the AWS MCP Server carry aws-mcp.amazonaws.com as the source and user agent. The billing server's calls show my laptop's IP and Boto3, exactly like a script I'd run&amp;nbsp;myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;The agent’s description of what it did is a claim. CloudTrail is the evidence.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it&amp;nbsp;cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IAM role and AWS MCP Server:&lt;/strong&gt; no&amp;nbsp;charge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Explorer API:&lt;/strong&gt; 15 requests at about $0.01 each, so about&amp;nbsp;$0.15.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro:&lt;/strong&gt; about 8 credits for the runs in this post, and a little over 17 of the free plan’s 50 counting practice runs and two false&amp;nbsp;starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My time:&lt;/strong&gt; about 2 hours 15 minutes, start to finish, on a laptop without the AWS CLI or Terraform installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By hand, for an account this size, I’d estimate the better part of a day. The agent did it in about 25 minutes. But speed isn’t the bigger difference. A person clicking through the console would likely never see these findings. The $250 add-on doesn’t appear on any QuickSight screen; it’s one usage type, visible only if you group the bill that way. The Neptune cluster wasn’t in the console at all. The agent didn’t get tired of looking, and it checked every place it was told to, in every Region it was allowed&amp;nbsp;to.&lt;/p&gt;

&lt;p&gt;The time I didn’t save was the review, and I wouldn’t want to. Half an hour of checking found three wrong claims. That part of the job stays with a&amp;nbsp;person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you point this at production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start from ViewOnlyAccess, not ReadOnlyAccess.&lt;/strong&gt; The agent needs the shape of the environment, not its contents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the MCP server’s permission list line by line.&lt;/strong&gt; “Read-only server” and “read-only permissions” are not the same&amp;nbsp;thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember that condition keys only cover managed servers.&lt;/strong&gt; Local servers look like&amp;nbsp;you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep sessions short and the session name fixed&lt;/strong&gt;, so CloudTrail tells a clean&amp;nbsp;story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point it at the bill, not just the resources.&lt;/strong&gt; The biggest findings here never showed up in a console walkthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent separate measured from inferred&lt;/strong&gt;, and keep a human on every&amp;nbsp;change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify in CloudTrail&lt;/strong&gt; before you tell a customer it was read-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That first question, &lt;em&gt;“You gave it what?”&lt;/em&gt;, is still the right one to ask. The difference now is that I can answer it with a role, four layers, and a CloudTrail summary that says zero&amp;nbsp;writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed is what gets people interested. Read-only access is what makes it safe to say yes. Together, you get faster insights without the manual&amp;nbsp;effort.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>finops</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
