AI Agent Permissions: Least-Privilege Design, With a Worksheet
Least privilege for AI agents means each agent gets only the tools, actions, data, and volume its task needs, enforced outside the model. This post gives a six-column permission design worksheet with a filled example, and maps it to OWASP, NIST AC-6, OAuth and MCP scopes, cloud IAM, and Microsoft Entra Agent ID.
AI agent permissions should follow least privilege: each agent gets only the tools, actions, data, and volume its task needs, and the limit is enforced outside the model. Grant read before write. Scope every write to one resource. Cap volume. Keep irreversible actions behind a human, or out of the agent’s reach.
The reason is simple. The model picks the next action. When it is wrong or manipulated, the permission set is the only boundary that still holds.
What least privilege means for AI agents
NIST’s control catalog, SP 800-53 Rev. 5, states it as control AC-6: limit users, and processes acting on their behalf, to the access needed for their assigned tasks. An agent is such a process. Three enhancements matter most. AC-6(7) requires a review of privileges at a set frequency. AC-6(9) requires logging the use of privileged functions. AC-6(10) blocks non-privileged users from running privileged functions, which is the rule a prompt injection tries to break.
What changes with agents is who picks the action. OWASP’s Top 10 for LLM Applications 2025 names the risk Excessive Agency (LLM06): damaging actions taken in response to unexpected, ambiguous, or manipulated model output, whatever the cause. The 2026 edition, published in August 2026, moved it up to LLM03.
OWASP splits the root cause three ways, and each one maps to a fix:
- Excessive functionality. The agent has tools it does not need. A document reader that can also delete documents is the OWASP example.
- Excessive permissions. A tool has more downstream access than its job requires, such as a read-only tool on a database login that can insert, update, and delete.
- Excessive autonomy. High-impact actions run without independent approval.
This post covers the first two. The third is gate design, covered in where to put human-in-the-loop gates.
Why do AI agents end up over-permissioned?
Three mechanisms push agents toward broad access, and none of them needs a careless engineer.
Vendor grants are coarse. Stripe’s docs say a standard secret key has full access to every API resource, which is why Stripe recommends restricted keys for any key given to an AI agent. Even good scopes are blunt. In Google’s Gmail scope list, gmail.compose covers managing drafts and sending email. There is no Gmail scope for “write drafts but never send.” If you want a draft-only agent, the rule has to live in your own tool code.
Clients ask for everything when nobody tells them otherwise. The MCP authorization spec (revision 2026-07-28) tells clients to request the scopes in the server’s 401 challenge. If the challenge names none, the client falls back to every scope in scopes_supported. A server that lists its full catalog there causes clients to request every advertised scope. The authorization server may still grant a subset, but you are now relying on it to trim.
Humans click “Always Allow.” Invariant Labs showed in May 2025 how a malicious issue on a public GitHub repo could steer an agent into leaking private repo data into a public pull request. The agent could reach public and private repos, and many users had set tool confirmations to “Always Allow.” Invariant’s suggested mitigation included limiting the agent to one repository per session.
Multi-agent systems add a fourth path: the OWASP Top 10 for Agentic Applications 2026 lists un-scoped privilege inheritance under ASI03 (Identity and Privilege Abuse), where a manager agent passes its full access to a narrow worker. For the identity side (who the agent is, and whose token it holds), see AI agent identity and non-human identity.
The four layers of an agent permission model
My recommendation is to design agent permissions in four layers. Each layer has a question and a decision rule.
Layer 1: Tool exposure. Which tools can the agent call at all? If a task never needs a tool, the tool is not in the agent’s manifest. OWASP’s agentic list asks for per-tool “least-privilege profiles” covering scopes, maximum rate, and egress allowlists. Rule: no general-purpose tools (shell, arbitrary URL fetch, raw SQL) when a narrow tool can do the job.
Layer 2: Action scope. Read or write, on which resource type? Separate read tools from write tools, even when they hit the same API. Rule: a tool that writes gets its own credential, so a read-only path can never write by accident. Stripe’s model makes the point: Write on a resource includes Read, so a write key is always a read key too.
Layer 3: Data scope. Which records? A support agent works on the ticket in front of it, not the whole helpdesk. MCP’s 2026-07-28 revision has no protocol sessions, so servers track work with handles such as a workflow ID. Its security guidance says a handle is not authentication and should be bound to the user from the verified token. Rule: bind every record ID the agent touches to the current task, and reject IDs from outside it.
Layer 4: Volume. How much per call, per hour, per day? Correct actions turn dangerous at loop speed. Rule: every write tool has a per-call cap, a per-hour cap, and a per-day cap. A burst of denials at the cap is an alert, not noise.
Approval gates sit on top of all four, and the tool, not the model, decides when to ask. Filters versus hard constraints are covered in guardrails versus hard constraints.
The AI agent permission design worksheet
Fill one row per tool, before the agent goes live. Six columns:
- Tool. The exact function name the model sees.
- Read/write. Read, write, or not granted. Delete counts as write.
- Scope. The literal grant: OAuth scope, API key permission, IAM action, plus the record filter your tool code adds.
- Approval needed. None, or the exact condition that triggers a human.
- Blast radius. The worst result of one bad call, and of one bad hour, at the caps.
- Test. At least one call that must fail, run on every deploy.
Rate blast radius with these rules:
| Rating | Rule | What to do |
|---|---|---|
| Low | Read-only, non-sensitive data, or a reversible internal write | Grant |
| Medium | Reads sensitive data, or writes that one person can undo | Grant with record scoping and logging |
| High | Moves money, writes many records, or reaches people outside the company | Grant only with caps and an approval condition |
| Critical | Irreversible deletes, permission changes, production infrastructure | Do not grant. A human does it. |
Filled example: a Tier 1 support refund agent (hypothetical)
Say an online store wants an agent that reads Tier 1 tickets, checks Stripe charges, issues small refunds, and drafts replies.
| Tool | Read/write | Scope | Approval needed | Blast radius | Test |
|---|---|---|---|---|---|
search_kb | Read | Published help articles only | None | Low | Fetch a draft article ID; must be denied |
get_ticket | Read | Tickets in the tier1 queue, current ticket’s customer only | None | Medium (customer PII) | Fetch a ticket from the legal queue; must be denied |
add_internal_note | Write | Internal note on the current ticket only | None | Low | Post a public reply through this tool; must be denied |
draft_reply | Write | Creates a draft in the helpdesk; no send capability | Human sends every reply | Medium | Call send; tool does not exist |
get_charge | Read | Stripe restricted key: read on charges and customers, None on everything else | None | Medium | List payouts; Stripe must reject it |
create_refund | Write | Separate restricted key with the minimal refund permission, confirmed in a sandbox. Tool code limits it to charges on the current ticket’s customer. No approval needed up to $100 per refund, 20 per rolling hour, $1,000 per rolling 24 hours. Absolute cap $500 per refund | Refund from $100.01 to $500, or a second refund for the same customer within 30 days: a named approver must approve in the helpdesk before the tool runs. Over $500: the tool refuses and a human refunds in Stripe | High | Refund $101; must wait and run only after approval is recorded. Refund $501, even if approved; must be refused. Refund another customer’s charge; must be denied |
update_customer, delete_customer | Not granted | None | Not applicable | Critical | Call either; tool is not in the manifest |
Two things stand out. First, the vendor grant is rarely the whole scope. Stripe’s restricted keys set None, Read, or Write per resource, and Write includes Read. They cannot say “under $100, for this customer,” so that rule lives in the tool wrapper. Stripe’s docs also do not list the minimal grant for creating refunds. Use the method they do document: build the key in a sandbox, read its request logs for permission errors, and add only what fails.
Second, the caps are what make High survivable, and they only work as a set. Here $100 is the auto-approval threshold, not a ceiling. Refunds above it go through the approved path, and $500 is the absolute ceiling the tool never exceeds. Check the unapproved math. The hourly cap alone allows 20 x $100 = $2,000 in an hour, and a loop running a full day could reach 24 x $2,000 = $48,000. The $1,000 rolling 24-hour cap overrides both: no 24-hour window, and so no single hour, can exceed $1,000 of unapproved refunds. Make the window rolling, not calendar-day. With a calendar-day reset, a loop could spend $1,000 at 11:59 p.m. and $1,000 more after midnight, $2,000 within minutes.
How do you scope OAuth and MCP permissions for an agent?
Name the resource. RFC 8707 adds a resource parameter to OAuth requests. It lets the authorization server audience-restrict the token to that resource and downscope it to what the resource can process. The MCP spec makes this mandatory for clients: the resource parameter must be in both the authorization request and the token request, and must carry the MCP server’s canonical URI. Servers must reject tokens not issued for them, and must not pass a client’s token through to another API.
Start small, then step up. MCP builds on the OAuth 2.1 draft. Its scope rules are the most agent-specific guidance in any standard I know of:
- Servers should name the needed scope in the
WWW-Authenticatechallenge, for examplescope="files:read". - At runtime, a missing scope returns
403witherror="insufficient_scope"and the scopes this operation needs. - The client then re-authorizes for the union of the scopes it previously requested and the scopes in the new challenge. That is the step-up flow.
- The security guidance says
scopes_supportedshould be the minimal set for basic use, such as amcp:tools-basicscope for low-risk discovery and reads.
The same guidance lists common mistakes, including wildcard scopes like * or full-access and treating scopes in a token as enough without server-side checks. That last one matters most. Your server still decides whether this call, on this record, is allowed.
Know where OAuth stops. MCP authorization is optional. HTTP servers should follow the spec. Local STDIO servers should not, and take credentials from the environment instead, so scope that environment key as tightly as a token. Token lifetime and rotation are identity questions, covered in the identity post and in my MCP security guide.
Where the grants live: cloud IAM and Entra Agent ID
The worksheet’s Scope column has to land in a real policy engine.
AWS. Session policies let you pass a policy when the agent assumes its role. The session’s permissions are generally the intersection of the role’s policy and the session policy. That is task scoping: one role, narrowed per task at assume time, under a permissions boundary that caps the role. One documented exception: a resource-based policy that names the session ARN itself as principal grants access the session policy and boundary do not limit. Check resource policies for direct session grants. To find what a role actually uses, IAM Access Analyzer policy generation reads up to 90 days of CloudTrail and drafts a policy from the actions it saw. Caveats: it does not see action-level activity for data events such as S3 object reads. It does not include iam:PassRole. And AWS says not to use it for auditing.
Microsoft Entra. Microsoft now lists Entra Agent ID as generally available, though some pieces, such as the blueprint creation wizard, are still marked preview. The Agent ID best practices match the worksheet: client credentials with only the required app permissions for autonomous agents, the on-behalf-of flow for interactive ones, and no app permissions where delegated permissions would do. Disabling a blueprint blocks all its agent identities at once, a useful kill switch.
My recommendation: keep the worksheet as the source of truth and generate the IAM or Entra configuration from it. Diff the two on every change.
How to test that least privilege actually holds
A permission model nobody tests is a guess. Three checks catch drift.
Negative tests in CI. Every worksheet row has a call that must fail. Run those calls on each deploy, against a test tenant with the real policy attached. If a must-fail call succeeds, the build fails.
Denial and elevation logs. AC-6(9) asks you to log privileged functions. For agents, log denials as well as successes, and log every step-up request with the scopes asked for and granted. My read: a spike in denials means the task changed or something is steering the agent. Both need a person. My post on what to log for an agent audit trail has the field list.
Scheduled review against usage. AC-6(7) wants a review at a set frequency. Compare each grant to what the agent used since the last review (Access Analyzer on AWS, request logs on a Stripe restricted key). Any grant with zero use is a candidate for removal. Re-run it whenever the model, prompt, or tool list changes.
Bottom Line
Least privilege for agents is four layers: tools, actions, records, and volume.
The vendor’s scope is rarely the full answer. Your tool code narrows it to the record and the amount.
Fill the worksheet before launch. Rate blast radius. Do not grant anything Critical.
Then prove it with must-fail tests on every deploy.
FAQ
Does least privilege stop prompt injection?
No. It limits what a successful injection can do. OWASP’s Excessive Agency entry covers damage from manipulated output whatever the cause, and its fixes are fewer tools, narrower permissions, and approval for high-impact actions. For defenses on the input side, see prompt injection in AI agents.
What is the difference between least privilege and least agency?
Least privilege limits what an identity can access. Least agency, a term in OWASP’s agentic list, also limits what the agent can do with that access: which tools, at what rate, to which destinations. You need both.
Can an MCP server require a scope only for one risky tool?
Yes. Under the 2026-07-28 spec, the server returns 403 with error="insufficient_scope" and the scope that operation needs, such as files:write. The client then runs step-up authorization for the union of its previously requested scopes and the challenged ones. Clients should cap retries so a denied scope does not cause an endless re-authorization loop.
How often should AI agent permissions be reviewed?
NIST AC-6(7) leaves the frequency to the organization. Microsoft’s Agent ID guidance suggests sponsor attestation every 6 to 12 months and a quarterly check for orphaned or idle agents. I would also review after every model, prompt, or tool change.
Can I give an agent read-only email access but let it draft replies?
Not with Gmail scopes alone. gmail.readonly reads, but gmail.compose covers drafts and sending, and gmail.modify adds sending too. Give the model a read tool on gmail.readonly and a draft tool whose service holds gmail.compose but exposes only a create-draft function. The scope could send. Your code does not.
Three Ways to Work With TMA
Need an agent built? We deploy production AI agents in your infrastructure. Working pilot. Real data. Measurable ROI. → Schedule Demo
Want to co-build a product? We’re not a dev agency. We’re co-builders. Shared cost. Shared upside. → Partner with Us
Want to join the Guild? Ship pilots, earn bounties, share profit. Community + equity + path to exit. → Become an AI Architect
Need this implemented?
We design and deploy enterprise AI agents in your environment with measurable ROI and production guardrails.
About the Author
Chase Dillingham
Founder & CEO, TrainMyAgent
Chase Dillingham is the founder and CEO of TrainMyAgent. He spent 15+ years as an enterprise architect deploying production systems. Now he builds AI agent platforms that deliver measurable ROI.
Connect with Chase Dillingham on LinkedIn