IAM for LLM Apps: Least Privilege When the Caller Is a Model
When the caller is a model, least privilege still applies. Give each agent tool a scoped IAM role and a session policy, not one broad set of admin credentials.

An agent is a caller you cannot fully predict, which is exactly the caller that should hold the least privilege. The instinct with a new agent is to hand it a broad role so it "just works" while you iterate. That instinct is how you end up with a language model holding credentials that can read every bucket and delete every table, driven by text an attacker can influence. Least privilege was always the rule. A non-deterministic caller makes it non-negotiable.
The mental shift is to stop thinking of the agent as your code and start thinking of it as a semi-trusted actor executing on your behalf. Your code does what you wrote. An agent does what the model decided, and the model decided based on input you do not control. The IAM boundary is what stands between "the agent made an odd choice" and "the agent made an odd choice with admin."
One role per tool, not one role per agent
The common anti-pattern is a single execution role attached to the whole agent, carrying the union of every permission any tool might need. That role is now the blast radius of every tool at once. A prompt injection that reaches the weakest tool inherits the permissions of the strongest.
Scope permissions to the tool, not the agent. The Lambda behind a lookup_order tool gets a role that can read one table and nothing else. The function behind send_email gets a role that can call SES for one verified identity. No tool carries a permission it does not use, so a compromised tool call can only do that tool's one job.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": "arn:aws:dynamodb:eu-west-1:111122223333:table/orders",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["${aws:PrincipalTag/tenant}"]
}
}
}]
}That is the entire permission for the order-lookup tool: read one item, from one table, and only rows for the caller's tenant. There is nothing else for an injected instruction to reach.
Narrow the role further at runtime with session policies
Per-tool roles are the floor. For multi-tenant systems you also want the permission scoped to the specific request. Session policies let you do that: when your backend assumes the tool's role, it passes an inline policy that intersects with the role's permissions for that one session, so a request acting for tenant A cannot touch tenant B's data even though the role technically spans the table.
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/order-lookup \
--role-session-name agent-tenant-a \
--policy '{"Version":"2012-10-17","Statement":[{
"Effect":"Allow","Action":"dynamodb:GetItem",
"Resource":"arn:aws:dynamodb:eu-west-1:111122223333:table/orders",
"Condition":{"ForAllValues:StringEquals":
{"dynamodb:LeadingKeys":["tenant-a"]}}}]}'The effective permission is the intersection of the role and the session policy, so the credentials the tool actually holds are narrowed to this request. Session tags plus a tenant condition on the role achieve the same thing declaratively. Either way, the credentials handed to the model-driven call are the smallest set that completes the current task.
Policy in front of the credentials
IAM decides what a principal can do. It does not decide whether this particular agent, on this particular turn, should be calling this tool at all. That authorization question now has a purpose-built answer on AWS: Policy in Amazon Bedrock AgentCore, generally available since earlier this month, evaluates each agent-to-tool request against rules you author in natural language that compile to Cedar, enforced at an AgentCore Gateway before the call proceeds. Read it as a layer in front of IAM, not a replacement: the gateway decides whether the tool call is allowed, and a tight IAM role decides how little damage it can do if it is.
Log the assumption, not just the API call
Because tools assume scoped roles, CloudTrail gives you a clean audit trail: which role was assumed, with what session name, for which task. Set the role-session-name to something that ties back to the agent turn and the tenant, and you can reconstruct exactly what an agent did and under whose authority. When an agent misbehaves, that trail is the difference between a bounded incident you can explain and a mystery you cannot.
The takeaway
Least privilege does not relax because the caller is clever. It tightens, because the caller is non-deterministic and influenced by untrusted input. Give each tool its own narrow role, intersect it with a session policy per request, put an authorization layer like AgentCore Policy in front, and log every role assumption. When the model makes a bad call, and it will, IAM is what decides that the bad call stays small.
Read this next
- Bedrock Guardrails Won't Save You From Prompt Injection, on why content filtering is not the boundary and IAM is.
The deep end of IAM and account hardening lives in the cloud field notes: securing your AWS account, at ercan.cloud. The hub is at ercanermis.com.
More from Ercan
Two more sites, same author, different ground.
Cloud, AWS, EKS, Terraform, platform engineering.
Field notes from production systems. EKS, IAM, Terraform at organization scale, observability, cost optimization.
Visit ercan.cloud →The hub. About, consulting, contact.
Personal hub for both writing tracks. Who I am, how the consulting works, how to reach me.
Visit ercanermis.com →