A coding agent with a broad task role will eventually do something you did not intend: s3:ListAllMyBuckets from a “read file” tool, kms:Decrypt on the wrong key, or iam:PassRole into a privileged sandbox. CloudTrail event history in the console is fine for one incident; it collapses when you need “every AccessDenied and unexpected allow for role/agent-tool-* across seven days and three accounts.” CloudTrail Lake is the unfair advantage — managed SQL over immutable management (and optionally data) events so you hunt IAM abuse like a query, not a CSV export. Pair with KMS Decrypt Grants and Verified Permissions for prevention — this post is the detection query layer.
⚡ TL;DR: Create an organization or account-level CloudTrail Lake event data store for management events. SQL-filter on
userIdentity.sessionContext.sessionIssuer.userName/ ARNs matching agent roles,errorCode = AccessDenied, and sensitiveeventNameallowlists. Alert via EventBridge or scheduled Athena-style reviews. Related: KMS grants, IAM condition keys for agents, Logs Insights forensics.
What CloudTrail Lake is (and is not)
CloudTrail Lake stores events in event data stores you query with SQL-like syntax (not classic Athena DDL on your own S3). You pay for ingestion + retention + queries. It is not a replacement for application logs (Logs Insights) and not Cedar authorization (Verified Permissions). Use Lake when the question is: “Which principal called which AWS API?”
| Question | Tool | Why |
|---|---|---|
| Which tool timed out for tenant X? | Logs Insights / ADOT | App-level fields |
| Did tool Y call KMS Decrypt on key Z? | CloudTrail Lake | AWS API audit |
| Should tool Y be allowed to call Decrypt? | Cedar / IAM | Policy decision |
| Is spend anomalous? | Budgets / EMF | Cost, not IAM |
Stand up an event data store for agent forensics
Prefer an organization trail feeding Lake if you run multi-account agent sandboxes (dev/staging/prod + per-tenant accounts). For a single account MVP:
# ✅ Create management-event data store (CLI sketch — adjust retention)
aws cloudtrail create-event-data-store \
--name agent-iam-forensics \
--retention-period 90 \
--multi-region-enabled \
--organization-enabled \
--advanced-event-selectors '[
{
"Name": "ManagementEventsAll",
"FieldSelectors": [
{"Field": "eventCategory", "Equals": ["Management"]}
]
}
]'
# ❌ Relying only on console "Event history" 90-day UI for weekly reviews
# You will miss cross-account patterns and cannot join on role name at scale
Optionally add data events for S3 object-level or Lambda invoke if your threat model includes exfiltration via GetObject on the wrong prefix — data events cost more; start with management events + tight IAM, then expand.
SQL queries that catch agent IAM abuse
CloudTrail Lake query language resembles Athena. Filter on agent role name patterns you already enforce with IAM condition keys / tags.
-- AccessDenied volume by agent role (last 7 days)
SELECT
userIdentity.sessionContext.sessionIssuer.userName AS role_name,
eventName,
errorCode,
count(*) AS n
FROM alias_agent_iam_forensics
WHERE eventTime >= DATE_ADD('day', -7, CURRENT_TIMESTAMP)
AND userIdentity.sessionContext.sessionIssuer.userName LIKE 'agent-tool-%'
AND errorCode = 'AccessDenied'
GROUP BY 1, 2, 3
ORDER BY n DESC
LIMIT 100;
-- Unexpected allows: sensitive APIs that should never appear for read-only tools
SELECT
eventTime,
userIdentity.sessionContext.sessionIssuer.userName AS role_name,
eventName,
sourceIPAddress,
userAgent,
requestParameters
FROM alias_agent_iam_forensics
WHERE eventTime >= DATE_ADD('day', -1, CURRENT_TIMESTAMP)
AND userIdentity.sessionContext.sessionIssuer.userName LIKE 'agent-tool-%'
AND errorCode IS NULL
AND eventName IN (
'DeleteBucket', 'PutBucketPolicy', 'CreateUser', 'AttachRolePolicy',
'PutRolePolicy', 'CreateAccessKey', 'PassRole'
)
ORDER BY eventTime DESC;
-- KMS Decrypt callers for agent roles (pair with grant audits)
SELECT
eventTime,
userIdentity.arn,
eventName,
json_extract_scalar(requestParameters, '$.keyId') AS key_id,
errorCode
FROM alias_agent_iam_forensics
WHERE eventTime >= DATE_ADD('day', -3, CURRENT_TIMESTAMP)
AND eventSource = 'kms.amazonaws.com'
AND eventName IN ('Decrypt', 'GenerateDataKey')
AND userIdentity.sessionContext.sessionIssuer.userName LIKE 'agent-%'
ORDER BY eventTime DESC
LIMIT 200;
Save these as Lake saved queries. When PassRole or AttachRolePolicy appears for an agent role, treat it as a severity — agents should not mutate IAM.
Wire alerts without a spreadsheet ritual
- Scheduled review: EventBridge Scheduler (overnight batches pattern) invokes a Lambda that runs Lake
StartQuery/GetQueryResults, posts Slack if sensitiveeventNamecount > 0. - Real-time complement: CloudTrail → EventBridge rules on
DeleteBucket,AttachRolePolicyfor faster paging; Lake for historical join/aggregate. - Correlate: Join Lake
requestId/ time windows with ADOT traces and toolsession_idfrom app logs when investigating a single abuse window.
// ✅ Sketch: start Lake query from Lambda (pseudo)
import {
CloudTrailClient,
StartQueryCommand,
GetQueryResultsCommand,
} from "@aws-sdk/client-cloudtrail";
const ct = new CloudTrailClient({});
export async function runDeniedQuery(eventDataStoreId: string) {
const start = await ct.send(
new StartQueryCommand({
QueryStatement: `
SELECT eventName, count(*) n
FROM ${eventDataStoreId}
WHERE errorCode = 'AccessDenied'
AND eventTime >= DATE_ADD('day', -1, CURRENT_TIMESTAMP)
AND userIdentity.sessionContext.sessionIssuer.userName LIKE 'agent-tool-%'
GROUP BY eventName ORDER BY n DESC LIMIT 20
`,
})
);
// poll GetQueryResults with start.QueryId — omit busy-wait details
return start.QueryId;
}
Hardening loop: from query hit to policy fix
| Lake finding | Likely root cause | Fix |
|---|---|---|
| Burst AccessDenied on S3 | Tool schema allows broad URI | Tighten tool args + IAM resource ARNs |
| Decrypt on unexpected keyId | Shared role too wide | Per-tool KMS grants |
| PassRole / AttachRolePolicy | Compromised session or bug | Kill role; investigate; deny iam:* on tool roles |
| ConsoleLogin as agent role | Should never happen | Alert + remove console trust |
Prevention still wins: Cedar for tool authorization, IAM path/tag conditions for runtime, AppConfig kill switches when a tool goes sideways. Lake tells you when prevention failed.
Production checklist
- [ ] Event data store covers all accounts where agent roles live (org store preferred).
- [ ] Agent roles follow a naming/tag convention queryable in SQL.
- [ ] Saved queries for AccessDenied, sensitive allows, KMS Decrypt.
- [ ] Daily/hourly Lambda or Scheduler job; not “someone opens the console.”
- [ ] Runbooks map findings to IAM/Cedar/KMS grant changes.
- [ ] Data events enabled only where threat model justifies cost.
- [ ] Retention ≥ your incident response SLA (90 days is a common start).
CloudTrail Lake will not stop a bad IAM policy — it stops you from hunting agent abuse with screenshots and spreadsheets while the role is still live.
Sample investigation playbook (30 minutes)
When Slack fires on sensitive eventName or a spike in AccessDenied:
- Scope the principal — copy
userIdentity.arnandsessionIssuer.userNamefrom the Lake row. Confirm it matches an agent tool role, not a human break-glass user. - Pull the window — re-query ±15 minutes around
eventTimefor that role; look for precedingAssumeRoleand following data-plane hints. - Map to tool — join with app logs on approximate time +
sourceIPAddress/ VPC endpoint; your tool Lambda should logtool_nameandsession_id(Logs Insights). - Contain — AppConfig kill switch for that tool; optionally detach inline policy or deny with SCPs if org-level.
- Fix forward — tighten IAM resource ARNs, add Cedar permit constraints, rotate if credentials leaked.
- Write the finding — one paragraph in the runbook: query used, root cause, PR links.
Do not start by downloading 50k events to Excel. Start with the saved query that already groups by eventName.
Cost and retention notes
Lake pricing is ingestion + storage + queries. Management events for a busy multi-tenant agent platform are usually modest compared to Bedrock token spend — but data events on high-churn S3 prefixes can dominate. Start management-only; add S3 data events for the artifact bucket prefix only. Set retention to match compliance (90/365); shorter retention saves money but blinds postmortems. Cap automated query fan-out so a buggy Scheduler job cannot run unbounded SQL every minute.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Distributed Locks Reality Check: When Redis Redlock Is the Wrong Tool
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
- Python asyncio vs Threading: The Benchmark That Changes How You Think About Concurrency
- LLM context windows: Tokens, Context Windows, and Why Models Forget Mid-Task
Newly added
- AWS Budgets + Cost Anomaly Detection: Cap Runaway Coding-Agent Spend
- OpenSearch Serverless: Semantic Scratch Memory for Multi-Turn Coding Agents
- ECR + Lambda Container Images: Heavyweight Coding Tools Without Zip Limits
- CloudTrail Lake: Query Agent IAM Abuses Without Spreadsheets
- DynamoDB Transactions: Atomic Tool-Ledger Writes for Coding Agents
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.