An AI agent just read my AWS account and told me a bucket was open to the internet, SSH was exposed to 0.0.0.0/0, GuardDuty was off, and I was burning $3.65 a month on an Elastic IP attached to nothing.
It did all of that in about two minutes. And here is the part that counts: it physically could not have changed anything even if it tried.
That last sentence is the whole point of this build. Most "give the AI access to my cloud" ideas die on one fear: what if it deletes something, or a bad prompt tricks it into running a destructive command? We remove that fear at the permission layer, not with a polite instruction. The agent runs on a read-only IAM identity. Every write call it could imagine gets rejected by AWS before it happens.
This is a walkthrough of building that agent from scratch. It is one JSON file and one Markdown checklist. By the end you will have a working AWS auditor you can point at your own account, and you will understand every field that makes it work.
The full code is on GitHub: github.com/simplynadaf/aws-auditor-agent.
Who this is for
You use Kiro Crew or the Amazon Q Developer CLI. You know your way around AWS enough to have an account with a few things running. You have heard "AI agent" a hundred times and you want to see what one is built from, without a framework, without a vector database, without 400 lines of Python.
If you can edit a JSON file and write a checklist in Markdown, you can build this.
Table of contents
- What an agent is made of (the 6 pieces)
- The safety foundation: read-only IAM
- Writing the agent config
- The skill: your audit checklist
- Wiring the AWS tools with MCP
- Running it, and what it produced
- Making it yours
- Why build this when Prowler and Trusted Advisor exist?
- What the docs do not tell you
1. What an agent is made of
Strip away the hype and an agent is one JSON file. The filename minus .json is the agent's name. The file describes a chat session: which model to use, what tools it can call, what it is allowed to do without asking you, what extra powers it plugs in, and what knowledge it carries.
Six pieces. That is the entire mental model.
| Piece | Field | Plain meaning |
|---|---|---|
| Identity |
name, description
|
What it is called |
| The brain | model |
Which LLM answers |
| Instructions | prompt |
Its personality and rules |
| What it can do | tools |
The toolbox |
| What runs without asking | allowedTools |
Pre-signed permission slips |
| Extra powers | mcpServers |
Plug in tool servers |
| Its knowledge | resources |
Attach skills and files |
The one distinction that trips up every beginner is tools versus allowedTools.
tools answers "what CAN this agent use?" If a tool is not listed, it does not exist for the agent.
allowedTools answers "what runs WITHOUT stopping to ask me?" A tool that is in tools but not in allowedTools still works, it just prompts you for approval each time it fires.
Toolbox versus permission slips. Keep that image and the rest is easy.
2. The safety foundation: read-only IAM
Before any config, we build the guardrail. This step is not optional and it is the reason the whole thing is trustworthy.
We attach two AWS-managed policies to the identity the agent uses:
arn:aws:iam::aws:policy/SecurityAuditarn:aws:iam::aws:policy/job-function/ViewOnlyAccess
SecurityAudit is the policy AWS designed for exactly this job: reading security-relevant configuration across services. ViewOnlyAccess fills the cost gaps, so the agent can see Elastic IPs, volumes, snapshots, and load balancers.
Underneath, both policies are Get*, List*, and Describe* only. There is no Create, no Delete, no Put, no Modify anywhere in them.
aws iam create-user --user-name aws-auditor
aws iam attach-user-policy --user-name aws-auditor \
--policy-arn arn:aws:iam::aws:policy/SecurityAudit
aws iam attach-user-policy --user-name aws-auditor \
--policy-arn arn:aws:iam::aws:policy/job-function/ViewOnlyAccess
aws iam create-access-key --user-name aws-auditor
# then paste the keys into: aws configure --profile aws-auditor
Why go through this instead of just telling the model "please do not change anything"?
Because a prompt is a suggestion and IAM is a wall. If the model hallucinates a fix, IAM blocks it. If someone slips a "now delete that bucket" instruction into a file the agent reads, IAM blocks it. The blast radius is zero by construction. You are separating the act of detecting problems from the act of fixing them, which is a security best practice on its own.
The agent has read-only glasses, not a wrench.
3. Writing the agent config
Here is the complete file. Save it as ~/.kiro/agents/aws-auditor.json.
{
"$schema": "https://raw.githubusercontent.com/aws/amazon-q-developer-cli/refs/heads/main/schemas/agent-v1.json",
"name": "aws-auditor",
"description": "Read-only agent that audits an AWS account for security risks and wasted spend, and reports prioritized, cited findings. Never changes anything.",
"model": "auto",
"mcpServers": {
"security": {
"command": "uvx",
"args": ["awslabs.well-architected-security-mcp-server@latest"],
"env": { "AWS_PROFILE": "default", "AWS_REGION": "us-east-1", "FASTMCP_LOG_LEVEL": "ERROR" }
},
"cloudtrail": {
"command": "uvx",
"args": ["awslabs.cloudtrail-mcp-server@latest"],
"env": { "AWS_PROFILE": "default", "AWS_REGION": "us-east-1", "FASTMCP_LOG_LEVEL": "ERROR" }
},
"pricing": {
"command": "uvx",
"args": ["awslabs.aws-pricing-mcp-server@latest"],
"env": { "AWS_PROFILE": "default", "AWS_REGION": "us-east-1", "FASTMCP_LOG_LEVEL": "ERROR" }
},
"awsdocs": {
"command": "uvx",
"args": ["awslabs.aws-documentation-mcp-server@latest"],
"env": { "FASTMCP_LOG_LEVEL": "ERROR" }
}
},
"tools": ["fs_read", "use_aws", "@security", "@cloudtrail", "@pricing", "@awsdocs"],
"allowedTools": ["fs_read", "use_aws", "@security", "@cloudtrail", "@pricing", "@awsdocs"],
"toolsSettings": {
"use_aws": {
"allowedServices": ["iam", "s3", "s3api", "ec2", "rds", "guardduty", "accessanalyzer", "securityhub", "elbv2", "elasticloadbalancing", "cloudwatch", "sts"]
}
},
"resources": ["skill://~/.kiro/skills/aws-audit/SKILL.md"],
"prompt": "You are AWS Auditor, a read-only cloud security and cost reviewer. You have ONLY read permissions (SecurityAudit + ViewOnlyAccess). You cannot and must not attempt to change anything, and you must not recommend that you run a fix yourself. Run the checks defined in your aws-audit skill using the available tools. Cite ONLY real resources you actually observed in tool output (real IDs, real names). NEVER invent a finding, a resource, or a number. If a check could not run, put it in the Gaps section as 'not verified' rather than implying it passed. Produce the report exactly in the format your skill defines. For every cost finding include an estimated monthly dollar impact computed from real pricing data, and state the assumption you used. Recommend fixes; never run them.",
"welcomeMessage": "AWS Auditor here (read-only). Point me at a region and I will report what is risky and what is wasteful, with real evidence. I cannot change anything."
}
Walk through the fields that carry weight.
model is set to auto, which lets the CLI pick the model. You can pin one (run /model in a session to see valid IDs) but auto is the sensible default.
prompt is where the honesty rules live. Read it closely. "Cite ONLY real resources you actually observed." "NEVER invent a finding, a resource, or a number." "If a check could not run, put it in the Gaps section as not verified rather than implying it passed." Those three lines are what separate a useful audit from a confident-sounding hallucination.
toolsSettings.use_aws.allowedServices is a second wall on top of IAM. Even though IAM already blocks writes, this restricts the use_aws tool to a specific list of services it can even attempt to call. Two independent limits: the tool can only reach these services, and IAM only permits reads inside them. Defense in depth.
Notice tools and allowedTools are identical here. That means the agent runs the audit end to end without stopping to ask permission for each read. That is a deliberate trade-off: convenience for a demo, and it is safe precisely because every one of those tools is read-only. If you were doing anything with write access, you would keep the risky tools out of allowedTools so they prompt you.
4. The skill: your audit checklist
The agent config is the wiring. The skill is the brain of the audit. It is a plain Markdown file at ~/.kiro/skills/aws-audit/SKILL.md, attached through the resources field, and the agent reads it on every run.
This is the file you will edit most. It holds the checks, the severity model, the framework mapping, and the exact output format.
The security checks, each backed by a specific read-only API:
| Check | Read-only API | Severity |
|---|---|---|
| Public S3 bucket |
s3:GetPublicAccessBlock, s3:GetBucketPolicyStatus
|
CRITICAL |
| Root account MFA off | iam:GetAccountSummary |
CRITICAL |
| Security group open to 0.0.0.0/0 on 22 / 3389 / DB | ec2:DescribeSecurityGroups |
HIGH |
| Unencrypted EBS or RDS |
ec2:DescribeVolumes, rds:DescribeDBInstances
|
HIGH |
| IAM users without MFA |
iam:ListUsers, iam:ListMFADevices
|
HIGH |
| GuardDuty disabled | guardduty:ListDetectors |
HIGH |
| Access Analyzer disabled | accessanalyzer:ListAnalyzers |
MEDIUM |
| Security Hub standards incomplete | securityhub:GetEnabledStandards |
MEDIUM |
The cost checks, each of which must carry a real dollar figure:
| Check | Read-only API |
|---|---|
| Unattached EBS volume |
ec2:DescribeVolumes (state = available) |
| Unassociated Elastic IP | ec2:DescribeAddresses |
| Old or orphaned snapshot | ec2:DescribeSnapshots |
| gp2 volume that should be gp3 |
ec2:DescribeVolumes (VolumeType = gp2) |
| Idle load balancer |
elbv2:DescribeLoadBalancers plus target health |
The skill tells the agent how to think about severity (a traffic-light model), which frameworks to cite (Well-Architected Security Pillar for structure, CIS AWS Foundations Benchmark for authority, Trusted Advisor for the cost category), and the precise report shape: executive summary, a Top 3 list, findings grouped by Security and Cost, passing checks, and an honest gaps section.
One design choice worth calling out. The skill ships with a small "demo scope" block that tells the agent to only report resources tagged demo-auditor=true. That keeps a demo repeatable and stops it from surfacing anything real. To audit your whole account, you delete that one block. That is the entire difference between "show me the demo" and "audit everything."
5. Wiring the AWS tools with MCP
The agent needs eyes. MCP (Model Context Protocol) servers are how it sees AWS. Each server in the mcpServers block is a small program that exposes a set of tools, and you reference all of a server's tools with an @ prefix, like @pricing.
This build uses four:
-
@security(Well-Architected security MCP): checks GuardDuty, Security Hub, Access Analyzer. -
@cloudtrail: queries account activity. -
@pricing: pulls live rates from the AWS Price List API, which is how cost findings get real numbers instead of guesses. -
@awsdocs: reads AWS documentation when the agent needs to confirm a detail.
They run through uvx, so you need uv installed (pip install uv). The first time you launch the agent, uvx fetches each server. No manual install step, no Docker.
The general-purpose use_aws tool covers everything else with direct read-only API calls. Between use_aws and the four MCP servers, the agent can reach every check in the skill.
6. Running it, and what it produced
Install the two files, then start the agent:
mkdir -p ~/.kiro/skills/aws-audit ~/.kiro/agents
cp skill/SKILL.md ~/.kiro/skills/aws-audit/SKILL.md
cp agent/aws-auditor.json ~/.kiro/agents/aws-auditor.json
kirocrew chat --agent aws-auditor
On the Amazon Q Developer CLI the command is q chat --agent aws-auditor, and you put the agent in ~/.aws/amazonq/cli-agents/ with the resources entry changed from skill:// to a file:// path.
Then ask it plainly:
Audit us-east-1. Actually call the tools and produce the report.
I ran this against five resources I created on purpose to be broken (the repo has a script for that, plus a teardown script that removes 100% of them). Here is the real output, trimmed. Nothing here is edited for effect.
# AWS Audit - us-east-1, 2026-09-02
## Executive summary
Scoped to the 5 resources tagged demo-auditor=true, this audit found 8 findings:
1 CRITICAL, 3 HIGH, 2 MEDIUM, and 3 cost items. The single most urgent problem is a
publicly readable S3 bucket (demo-auditor-public-5403) that lets anyone on the
internet download its objects.
## Top 3 - do these now
1. Public S3 bucket demo-auditor-public-5403 - bucket policy grants s3:GetObject to
everyone (Principal: *). Enable Public Access Block and remove the public policy.
2. Security group sg-055250cbcc6f3b37b - SSH port 22 is open to 0.0.0.0/0. Restrict
to a known admin IP or use SSM Session Manager.
3. GuardDuty is disabled in us-east-1 - no threat detection is running.
The cost section is where the pricing MCP earns its place:
### [COST] Unassociated Elastic IP - ~$3.65/month
- Resource: eipalloc-000992c9956cfaaa5 (public IP 35.173.72.149, no association)
- Estimated impact: $0.005/hr (USE1-PublicIPv4:IdleAddress, us-east-1) x 730 hrs
= $3.65/mo. Assumption: idle for a full month, on-demand.
- Fix: Release the Elastic IP if not needed, or associate it with a running resource.
Every resource ID is real. Every rate came from the live Price List API. The agent computed the numbers, it did not make them up.
The most convincing part was not a finding, though. It was the honesty. Three moments stood out:
The agent found a snapshot created that same day. The checklist looks for "old" snapshots. Instead of forcing it into the finding, the agent flagged it for completeness and refused to call a fresh snapshot old.
Root MFA was on. It reported that as a PASS. A tool that only ever finds problems just confirms its own bias. Reporting passes is how you know it looked.
And the gaps section listed what it did not check and why: IAM per-user MFA, RDS encryption, and idle load balancers were out of scope for the demo, so it said "not verified" rather than implying those passed. "Not verified" is not "passed." That line in the prompt did real work.
The whole run cost a few cents in demo resources and about two minutes of wall time.
7. Making it yours
The SKILL.md file is yours to own. It is a checklist, not code.
Add checks your team cares about. Each row names the read-only API that backs it, so extending it is a matter of adding a row and a line of guidance. Change the severities to match your risk appetite. Rewrite the report format if your manager wants it a certain way. Delete the demo-scope block to audit the entire region.
A few natural next steps once it works:
Point it at more regions. The demo is us-east-1 only. Loop the region in the prompt or run it per region.
Schedule it. A read-only agent that runs every morning and mails you a diff of new findings is a genuinely useful thing, and it cannot break anything overnight because it cannot write.
Widen the checklist toward a framework you report against, like the full CIS benchmark, one row at a time.
Why build this when Prowler and Trusted Advisor exist?
Fair question. There are mature tools in this space, and you should know when to reach for them instead.
Prowler is the heavyweight: 600+ checks, mapped to CIS, PCI, HIPAA, and more. If you need exhaustive compliance coverage for an audit, use Prowler. The trade-off is that 600 findings with no narrative is a wall of text. It tells you everything and prioritizes nothing.
ScoutSuite is read-only like our agent and produces a nice HTML report. It is excellent for a point-in-time config review. It does no cost analysis and it is static, not conversational. You cannot ask it a follow-up.
Trusted Advisor has the best native cost checks (idle load balancers, unassociated Elastic IPs, underutilized EBS). The catch: the full cost category requires a Business or Enterprise Support plan (AWS docs). On a Basic plan you do not get them, which is exactly why an agent computing the same things from raw Describe calls is useful to a beginner.
Security Hub is the central dashboard, but it must be configured. Our demo run caught the trap live: the standards were subscribed but reported NO_AVAILABLE_CONFIGURATION_RECORDER. Without an AWS Config recorder, most controls cannot evaluate (AWS docs). The dashboard was on, the checks were off. A human skims a green dashboard and moves on. The agent read the actual status and flagged it.
So here is the decision framework:
| Reach for | When |
|---|---|
| Prowler | You need exhaustive, framework-mapped compliance evidence for an audit |
| ScoutSuite | You want a thorough static config snapshot, security only |
| Trusted Advisor | You are on Business/Enterprise Support and want native cost checks |
| This agent | You want security AND cost in one plain-language, prioritized report you can converse with, on any support plan, provably read-only |
The edge of the custom agent is not raw coverage. It is clarity, ruthless prioritization (a Top 3, not 600 rows), security and cost in one voice, detecting the "configured but inert" trap, and a provable read-only guarantee you can hand to a nervous manager.
When NOT to use it: if you need certified compliance evidence, if you want continuous automated remediation (this agent only reads), or if your org already runs Prowler in CI and just needs the raw findings. This is a fast, human-friendly first look, not a compliance system of record.
What the docs do not tell you
Four things I hit building this that are not in any single doc:
The use_aws service allowlist is a real second wall, and it is easy to forget s3api. S3 read calls split between s3 and s3api depending on the operation. Leave s3api out of allowedServices and the public-bucket check silently cannot run. Both belong in the list.
"Not verified" has to be forced in the prompt or the model will paper over gaps. Without the explicit "put it in the Gaps section as not verified" instruction, models tend to imply a skipped check passed. That single sentence changed the behavior in testing.
Idle Elastic IP pricing hides behind a specific usage type. The rate is not under a generic "EIP" filter. It is USE1-PublicIPv4:IdleAddress in us-east-1 (since the Feb 2024 public IPv4 charge). If your cost math comes back empty, you are querying the wrong usage type.
Clean up your demo resources. If you use the demo scripts, the teardown deletes by recorded ID and then sweeps by tag, in this order: snapshot, Elastic IP, volume, security group, bucket. Run it right after, or you keep paying the few cents a month the audit just flagged. The irony writes itself.
The takeaway
An agent is not a mystery. It is a JSON file that names a model, lists some tools, plugs in a few MCP servers, and points at a Markdown checklist. The engineering that makes it trustworthy is not in the model at all. It is in the IAM boundary that makes destructive action impossible, and in a prompt that forbids inventing anything.
Build the guardrail first. Then the agent can be as capable as you like, because the worst it can do is tell you the truth about your account.
Grab the two files, attach the two read-only policies, and run it against your own account: github.com/simplynadaf/aws-auditor-agent.
What would you add to the checklist first, security or cost? I am curious which one bites people more in practice.
Follow me for more on AWS architecture, DevOps, and AI Infrastructure:
Portfolio | LinkedIn | Dev.to | YouTube | Email | AWS Builder Center | X
Top comments (2)
Putting the hard safety boundary in IAM rather than the prompt is exactly right. One operational check I’d add is emitting the resolved account/role ARN at startup and binding the config explicitly to
aws-auditor; the example creates that profile but showsAWS_PROFILE: default, which could make a clean read-only design inspect the wrong account. Do you also use an explicit deny boundary or SCP for mutation APIs as defense in depth?Yes thats a very good catch. The resolved account id + role arn should definitely be surfaced at startup and the profile should be explicitly bound to the intended aws-auditor configuration rather than relying on default.
For the current demo im relying primarily on least privilege readonly iam as the hard boundary I havent added an explicit scp/deny boundary for mutation apis yet.
That would be a useful next layer of defense in depth especially for production environments.