IAM decides whether the Lambda role can call S3 or Bedrock. It does not decide whether tenant Acme’s agent may call shell_exec on repo payments-api at 2am. That is application authorization. Teams bury it in if (ALLOWLIST.includes(tool)) until the list drifts and a prompt injects a tool name you forgot to remove. AWS Verified Permissions (AVP) + Cedar give you deny-by-default policies, IsAuthorized from the agent runtime, and an audit trail that IAM alone cannot provide.
⚡ TL;DR: Authorize each tool call with Cedar via Verified Permissions (
IsAuthorized), scoping principal (tenant/user), action (tool name), and resource (repo/env). Keep IAM for AWS API blast radius (IAM Condition Keys for Agent Runtimes); keep AppConfig for emergency kills (AppConfig kill switches); keep schemas strict (tool schemas). Do not replace IAM with Cedar — layer them.
IAM vs Cedar vs allowlists
| Layer | Question it answers |
|---|---|
| IAM (role + condition keys) | May this runtime call s3:PutObject on tagged buckets? |
| Cedar / AVP | May this principal invoke tool deploy_prod on resource repo:payments? |
| AppConfig flags | Is deploy_prod globally/incident-disabled right now? |
| Hardcoded allowlist | Whatever someone committed last Tuesday |
// ❌ Hardcoded allowlist — drifts, no tenant scope, no audit
const TOOLS = new Set(["read_file", "grep", "apply_patch", "shell_exec"]);
export function authorize(tool: string) {
if (!TOOLS.has(tool)) throw new Error("denied");
}
✅ Express intent in Cedar; evaluate on every tool hop.
Cedar policy shape for agent tools
Model entities explicitly:
- Principal:
User::"{sub}"orTenant::"{tenantId}" - Action:
Action::"invokeTool"with contexttoolName - Resource:
Repo::"{repoId}"orEnvironment::"prod"
// permit read-only tools for any authenticated tenant member
permit (
principal in Tenant::"*",
action == Action::"invokeTool",
resource
)
when {
context.toolName like "read_*" ||
context.toolName == "grep" ||
context.toolName == "list_files"
};
// deny shell in production unless role is breakglass
forbid (
principal,
action == Action::"invokeTool",
resource == Environment::"prod"
)
when {
context.toolName == "shell_exec" &&
!(principal in Role::"breakglass")
};
// permit apply_patch only on repos the principal owns
permit (
principal,
action == Action::"invokeTool",
resource
)
when {
context.toolName == "apply_patch" &&
resource in principal.ownedRepos
};
Store policies in an AVP policy store. Version them in git; deploy via CI. Prefer forbid for irreversible tools so a missing permit cannot accidentally allow deploy_prod.
IsAuthorized from the agent runtime
Call AVP before side effects — after schema validation, before Redis idempotency write.
import {
VerifiedPermissionsClient,
IsAuthorizedCommand,
} from "@aws-sdk/client-verifiedpermissions";
const avp = new VerifiedPermissionsClient({});
const POLICY_STORE_ID = process.env.AVP_POLICY_STORE_ID!;
export async function authorizeToolCall(input: {
tenantId: string;
userSub: string;
toolName: string;
resourceType: "Repo" | "Environment";
resourceId: string;
ownedRepos?: string[];
}): Promise<{ allowed: boolean; reasons?: string[] }> {
const entities = {
entityList: [
{
identifier: { entityType: "User", entityId: input.userSub },
attributes: {
tenantId: { string: input.tenantId },
},
parents: [{ entityType: "Tenant", entityId: input.tenantId }],
},
{
identifier: {
entityType: input.resourceType,
entityId: input.resourceId,
},
},
],
};
const out = await avp.send(
new IsAuthorizedCommand({
policyStoreId: POLICY_STORE_ID,
principal: { entityType: "User", entityId: input.userSub },
action: { actionType: "Action", actionId: "invokeTool" },
resource: {
entityType: input.resourceType,
entityId: input.resourceId,
},
context: {
contextMap: {
toolName: { string: input.toolName },
},
},
entities,
})
);
return {
allowed: out.decision === "ALLOW",
reasons: (out.determiningPolicies ?? []).map((p) => p.policyId ?? ""),
};
}
// ✅ Gate in the tool runner
export async function handler(event: {
tenantId: string;
userSub: string;
toolName: string;
repoId: string;
args: unknown;
}) {
const authz = await authorizeToolCall({
tenantId: event.tenantId,
userSub: event.userSub,
toolName: event.toolName,
resourceType: "Repo",
resourceId: event.repoId,
});
if (!authz.allowed) {
return { ok: false, error: "cedar_denied", policies: authz.reasons };
}
if (!(await isToolEnabled(event.toolName))) {
return { ok: false, error: "appconfig_disabled" };
}
return executeTool(event);
}
❌ Calling AVP only at session start and caching “all tools allowed” for an hour misses mid-session policy updates and role revocation. Cache negative briefly if you must; re-check irreversible tools every time.
Batch authz and latency
IsAuthorized adds a network hop. Mitigations:
- Use AVP authorization caching where available / short TTL local memo keyed by
(principal, action, resource, toolName) - Batch independent tool calls only after a single broader check when Cedar permits it (rare — prefer per-tool)
- Place the policy store in the same region as agent Lambdas
- Fail closed on
AccessDeniedException/ throttling — same posture as AppConfig
Combine with Bedrock ApplyGuardrail for content filters: Cedar answers may they call this tool; Guardrails answer is this argument/result safe.
Vs hardcoded allowlists and IAM-only
IAM condition keys (blast radius by tag) constrain what the execution role can touch in AWS. Cedar constrains what the user/tenant requested inside your product. You need both:
- IAM: tool Lambda role can only write
arn:aws:s3:::agent-artifacts-${env}/*withaws:ResourceTag/tenant=${tenant} - Cedar: user may only
apply_patchon repos they own - AppConfig: kill
apply_patchglobally during an incident
Skipping Cedar and stuffing tenant rules into IAM leads to role explosion. Skipping IAM and trusting Cedar alone leaves a compromised runtime with broad AWS power.
Operational checklist
- [ ] Define Cedar entity model: User/Tenant, Action::invokeTool, Repo/Environment
- [ ] Forbid irreversible tools by default; permit explicitly
- [ ] Call
IsAuthorizedper tool invoke; fail closed on AVP errors - [ ] Layer IAM condition keys + AppConfig kills + Cedar
- [ ] Version policies in git; review forbids as carefully as IAM
- [ ] Log determining policy IDs (not secrets) for audit
- [ ] Load-test IsAuthorized p99 under Map fan-out concurrency
Related reading
- IAM Condition Keys for Agent Runtimes: Limit Blast Radius by Tag
- AppConfig Feature Flags: Kill Switches for Agent Tools Without Redeploy
- AI Coding Agent Tool Schemas: Strict JSON Contracts That Survive Retries
- Bedrock ApplyGuardrail API: Pre/Post Filters for Tool I/O in Coding Agents
Last updated on September 20, 2026
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- LLM context windows: Tokens, Context Windows, and Why Models Forget Mid-Task
- Zero-Copy Node Streams: Pipe Large S3 Objects Without Buffering
- SQL Joins Explained: INNER, LEFT, RIGHT, FULL, CROSS, and Self Joins
- PostgreSQL Performance Tuning: The Configuration Changes That Actually Matter
Newly added
- API Gateway + WAF: Rate-Limit Public Coding Agent Endpoints
- AWS Verified Permissions: Cedar Policies That Authorize Agent Tools
- ADOT OpenTelemetry: Trace Multi-Hop Agent Tool Calls Across Lambda
- ElastiCache Redis: Scratchpads and Tool-Result Cache for Multi-Turn Coding Agents
- Step Functions: Orchestrate Multi-Step Coding Agent Graphs Without Recursive Chaos
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.