13 views
An AI coding agent with bedrock:InvokeModel* and s3:* on * is a privilege escalation waiting for a prompt. Teams “fix” it with app-level path checks; attackers (or confused agents) call the SDK directly. Real IAM for AI agents uses condition keys — especially tag-based and Bedrock-specific keys — so the runtime role physically cannot touch resources outside the tenant cell.
⚡ TL;DR: IAM for AI agents starts with tagging: stamp every agent-touchable resource withApply the same least-privilege story to AI code review bot IAM, Lambda tool sandboxes, and Bedrock Agents + EventBridge workers.TenantId/AgentScope. Bind the runtime role withaws:ResourceTag/aws:RequestTag/aws:PrincipalTagconditions. Prefer least-privilege Bedrock actions over*. Prove deny paths with chaos tests — do not trust prompt instructions as the security boundary.
IAM for AI agents: why app guards without IAM fail
typescript
If the model invents // ❌ App-only guard — agent can still call S3 with stolen/confused credentials
async function readRepoFile(tenantId: string, key: string) {
if (!key.startsWith(`repos/${tenantId}/`)) {
throw new Error("path escape");
}
return s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
}
repos/other-tenant/... and your guard has a bug — or a tool bypasses the helper — IAM still allows it. Condition keys make that a hard 403.
Tag the world the agent can see
Convention that scales:| Tag key | On | Meaning |
|---|---|---|
TenantId |
S3 objects (via bucket policy + Object tagging), DynamoDB items (via ABAC on leading key), secrets, queues | Tenant boundary |
AgentScope |
Tool Lambda versions, knowledge-base resources | read / mutate / admin |
Environment |
Everything | dev / staging / prod |
DataClass |
Buckets, tables | public / customer / secrets |
json
✅ Stamp the session role (or assumed role per run) with {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlyTenantObjects",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:GetObjectTagging"],
"Resource": "arn:aws:s3:::cc-agent-repos-${Environment}/*",
"Condition": {
"StringEquals": {
"s3:ExistingObjectTag/TenantId": "${aws:PrincipalTag/TenantId}",
"aws:PrincipalTag/Environment": "${aws:ResourceTag/Environment}"
}
}
},
{
"Sid": "DenyCustomerDataCrossEnv",
"Effect": "Deny",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalTag/Environment": "${aws:ResourceTag/Environment}"
},
"StringEquals": {
"aws:ResourceTag/DataClass": "customer"
}
}
}
]
}
TenantId via sts:TagSession. ❌ Do not rely on a long-lived role shared across all tenants without principal tags.
Bedrock and agent-specific conditions
Bedrock supports condition keys that limit model IDs, guardrail usage, and more. Combine them so a coding agent cannot suddenly call an unrestricted foundation model or skip guardrails.json
For Agents / Knowledge Bases, scope {
"Sid": "ConverseOnlyApprovedModels",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:Converse",
"bedrock:ConverseStream"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/anthropic.claude-3-5-sonnet-*",
"arn:aws:bedrock:*::foundation-model/amazon.titan-embed-text-*"
],
"Condition": {
"StringEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2"]
}
}
}
bedrock:Retrieve and bedrock:InvokeAgent to resource ARNs tagged for that product cell — not *. When agents can start async work, constrain EventBridge events:PutEvents with events:detail-type and source conditions so they cannot inject arbitrary bus traffic (see Bedrock Agents Plus EventBridge).
Request tags on create — close the write path
Reads needExistingObjectTag. Writes need request tags so agents cannot create untagged exfil buckets/objects.
json
{
"Sid": "PutOnlyWithMatchingTenantTag",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::cc-agent-repos-${Environment}/*",
"Condition": {
"StringEquals": {
"s3:RequestObjectTag/TenantId": "${aws:PrincipalTag/TenantId}",
"s3:RequestObjectTag/AgentScope": "mutate"
}
}
}
typescript
❌ Allowing // ✅ Tool always forwards tags on write
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: `repos/${tenantId}/patches/${patchId}.diff`,
Body: diff,
Tagging: `TenantId=${encodeURIComponent(tenantId)}&AgentScope=mutate&Environment=${env}`,
})
);
s3:PutObject without tag conditions lets the agent create repos/evil/... without TenantId — your ABAC read path then becomes inconsistent.
Prove it with chaos and CI
App tests that mock S3 will never catch a missing condition. Add:- Policy unit tests (IAM Access Analyzer policy validation / Cedar-like fixtures, or
iam-lensstyle checks in CI). - Live deny probes from a canary role assuming the agent role with a wrong
TenantIdtag — expect AccessDenied. See Chaos for IAM Boundaries. - Agent PR gates that reject CDK/IAM diffs introducing
Action: "*"or unconditioneds3:*— Cursor Background Agents CI Gates.
typescript
Also bind code signing so only CI-built agent runtimes run in prod — Lambda Code Signing.
// canary/deny-probe.ts
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
export async function expectCrossTenantDenied() {
const sts = new STSClient({});
const assumed = await sts.send(
new AssumeRoleCommand({
RoleArn: process.env.AGENT_RUNTIME_ROLE_ARN!,
RoleSessionName: "deny-probe",
Tags: [
{ Key: "TenantId", Value: "tenant-A" },
{ Key: "Environment", Value: "prod" },
],
})
);
const s3 = new S3Client({
credentials: {
accessKeyId: assumed.Credentials!.AccessKeyId!,
secretAccessKey: assumed.Credentials!.SecretAccessKey!,
sessionToken: assumed.Credentials!.SessionToken!,
},
});
try {
await s3.send(
new GetObjectCommand({
Bucket: "cc-agent-repos-prod",
Key: "repos/tenant-B/secrets.env", // tagged TenantId=tenant-B
})
);
throw new Error("❌ cross-tenant read unexpectedly allowed");
} catch (e: any) {
if (e?.name === "AccessDenied" || e?.$metadata?.httpStatusCode === 403) {
return; // ✅
}
throw e;
}
}
Session tagging at assume time (STS)
The runtime shared role should be thin. Per request (or per agent run), assume a session role with tags:typescript
✅ import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
const sts = new STSClient({});
export async function credentialsForTenant(tenantId: string, env: string) {
const out = await sts.send(
new AssumeRoleCommand({
RoleArn: process.env.AGENT_RUNTIME_ROLE_ARN!,
RoleSessionName: `agent-${tenantId}`.slice(0, 64),
DurationSeconds: 900,
Tags: [
{ Key: "TenantId", Value: tenantId },
{ Key: "Environment", Value: env },
{ Key: "AgentScope", Value: "mutate" },
],
TransitiveTagKeys: ["TenantId", "Environment"],
})
);
return out.Credentials!;
}
TransitiveTagKeys so downstream sts:AssumeRole chains cannot drop the tenant boundary. ❌ Passing tenant only as a header to tools while the AWS credential stays untagged.
Secrets and knowledge bases under the same ABAC story
Secrets Manager and Bedrock Knowledge Bases should carry the sameTenantId tag story. An agent that can secretsmanager:GetSecretValue on * will pull prod DB creds “to debug.” Condition on secretsmanager:ResourceTag/TenantId and deny DataClass=secrets cross-env. For RAG, scope bedrock:Retrieve to KB ARNs that belong to the tenant cell — never a shared KB with mixed tenants unless document-level filtering is proven (prefer separate indexes per cell for strong isolation).
Related reading
Checklist
- [ ] Per-run / per-tenant session tags via
sts:TagSession(TenantId,Environment) - [ ] Allow statements use
aws:ResourceTag/ object tag conditions — not path-prefix hope - [ ] Put/create requires matching
RequestObjectTag/aws:RequestTag - [ ] Bedrock limited to approved model ARNs + regions; no
bedrock:*on* - [ ] Explicit Deny for cross-env customer data
- [ ] Live AccessDenied canary in CI/CD for cross-tenant probes
- [ ] Policy lint rejects unconditioned
s3:*/dynamodb:*from agent roles
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- n8n vs AI Agent: I Gave Both the Same Job (Local, Ollama)
- Local AI Video Generator: Build Your Own App (Free, Python, No GPU)
- AI Wrote Raw Binary Instead of Code. Here’s What Happened (WebAssembly)
- I Built a Browser That Only Runs Encrypted Binary (Ep 1)
- RAG From Scratch: Chunking, Embeddings, Reranking and Evaluation (Real Numbers)
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.