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. The unfair advantage is IAM condition keys — especially tag-based and Bedrock-specific keys — so the runtime role physically cannot touch resources outside the tenant cell, even if the agent hallucinates a path.
⚡ TL;DR: Stamp every agent-touchable resource with
TenantId/AgentScopetags. Bind the runtime role withaws:ResourceTag/aws:RequestTag/aws:PrincipalTagconditions. Prefer least-privilege Bedrock actions over*. Prove deny paths with chaos tests. Pair with Cell-Based Architecture on AWS, Chaos for IAM Boundaries, and Cursor Background Agents CI Gates.
The failure mode: app guards without IAM guards
// ❌ 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 }));
}
If the model invents 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 |
{
"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"
}
}
}
]
}
✅ Stamp the session role (or assumed role per run) with 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.
{
"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"]
}
}
}
For Agents / Knowledge Bases, scope 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 need ExistingObjectTag. Writes need request tags so agents cannot create untagged exfil buckets/objects.
{
"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"
}
}
}
// ✅ 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}`,
})
);
❌ Allowing 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.
// 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;
}
}
Also bind code signing so only CI-built agent runtimes run in prod — Lambda Code Signing.
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:
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 same TenantId 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).
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
Most viewed
Newly added
- CloudWatch EMF for LLM Cost: Per-Tenant Token Metrics That Survive Sampling
- SQS FIFO + Lambda: Ordered Agent Job Queues Without Double-Applies
- Bedrock Converse toolConfig: Idempotent Tool Results Under Retries
- IAM Condition Keys for Agent Runtimes: Limit Blast Radius by Tag
- Lambda Response Streaming: Keep AI Coding Gateways Under Client Timeouts
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.