Least privilege for AI agents means every agent runs with the smallest set of tools, data scopes and credentials its current task requires — and loses them when the task ends. It is the same principle that governs human IAM, applied to a workload that can be rewritten by a line of untrusted text. This guide shows how to get there in five steps: entitlement mapping, role design, enforcement points, secrets isolation and audit.

Most teams skip straight to the system prompt: "You may only use the read-only tools." That is a suggestion, not a control. A prompt injection in a retrieved document, a poisoned tool description, or a hallucinated tool call can override it. Least privilege only becomes real when the permission decision happens outside the model's context, in code that the model cannot rewrite.

Why AI agents break classic IAM

Traditional IAM assumes a human principal with stable intent and a session measured in hours. Agents violate every assumption:

  • Intent is untrusted input. The agent's plan is generated from user text, retrieved documents, tool outputs and memory — all attacker-reachable.
  • Permissions are inherited by accident. An agent launched by an admin often inherits admin rights for every sub-task, including the ones triggered by a malicious email.
  • Tool calls compose. Two individually harmless tools — "read CRM" and "send email" — combine into a data exfiltration path. Classic IAM reviews each permission in isolation.
  • Sessions are long and multi-tenant. One agent process may act for hundreds of users in parallel, so a shared service account destroys attribution.

The threat model is covered in depth in the AI agent security hub; this article focuses on the permission design itself.

Step 1 — Build an entitlement map

An agent entitlement map is an inventory that answers, for every agent: what can it reach, on whose behalf, through which tool, and with what consequence? Do this before writing policy. The map is a table (or YAML file) with one row per entitlement:

agent: support-triage owner: customer-support runs_as: calling_user entitlements: - tool: zendesk.search_tickets scope: [own_queue] action: read reversible: true - tool: zendesk.update_ticket scope: [own_queue] action: write reversible: true - tool: email.send scope: [customer_on_ticket] action: write reversible: false # requires human approval - tool: crm.read_contact scope: [account_of_ticket] action: read pii: true # redact fields unless needed - tool: shell.exec action: none # explicitly denied

Three fields do most of the security work: scope (which records, not just which API), reversible (whether a human must confirm), and pii (whether output needs masking). An entitlement map with "tool: all, scope: all" is not a map — it is a finding.

Step 2 — Turn entitlements into capability masks

Assign each capability a bit and each role a mask. Bits are coarse on purpose: they are cheap to check on every token and every tool call. Fine-grained scope (row-level, tenant-level) is enforced by the tool gateway using the request context, not by the model.

CAP_READ_TICKETS = 1 << 0 CAP_WRITE_TICKETS = 1 << 1 CAP_SEND_EMAIL = 1 << 2 CAP_READ_CRM = 1 << 3 CAP_ACCESS_PII = 1 << 4 CAP_EXEC_SHELL = 1 << 5 ROLES = { "support_agent": CAP_READ_TICKETS | CAP_WRITE_TICKETS | CAP_READ_CRM, "support_lead": CAP_READ_TICKETS | CAP_WRITE_TICKETS | CAP_READ_CRM | CAP_SEND_EMAIL, "auditor": CAP_READ_TICKETS | CAP_READ_CRM | CAP_ACCESS_PII, }

The effective mask is the intersection of three things: the calling user's own permissions, the agent's role, and the task's temporary elevation. Intersection — not union. If a support agent asks an admin-launched workflow to run, the workflow does not become admin. This single rule blocks most privilege-escalation paths, including the "confused deputy" pattern where a low-privilege user tricks a high-privilege agent into acting for them.

Step 3 — Enforce at three independent layers

Defense in depth is not three copies of the same check. Use three different mechanisms at three different moments:

  1. Before generation (token/logits level). If the mask lacks CAP_EXEC_SHELL, ban the trigger phrases that start a shell call. The model cannot emit shell.exec( at all, so the tool call never exists. This is the approach used by ReskSafety and reskSecure.
  2. At the tool gateway. Re-check the mask and the scope against the actual arguments: is this ticket in own_queue? is this recipient the customer on the ticket? Argument-level checks catch the calls that were syntactically allowed but semantically out of scope.
  3. After execution. Verify the result, redact PII that the mask does not permit, and log the decision with the mask, the arguments and the policy version.

Only layer 1 is invisible to the attacker; layers 2 and 3 are the safety net when a tool is misconfigured or a new tool ships without review. A practical implementation walkthrough is in Tool Permissions for AI Agents, and the bitmask mechanics are detailed in Capability Bitmasks: Real RBAC for LLM Applications.

Step 4 — Keep secrets out of the context window

An agent that can read its own credentials can leak them, log them, or paste them into a tool argument. Rules that hold up in production:

  • No long-lived keys in prompts, memory or tool results. Credentials live in a broker; the agent receives a short-lived, audience-restricted token.
  • One token per tool and per task. A token for zendesk must be rejected by email.send.
  • Never let the agent choose the audience. The gateway maps tool → audience; a model-supplied URL or scope is untrusted input.
  • Redact on read, not on write. If the agent does not need a secret to finish the task, it should never appear in the tool response.

Encrypting conversation state is a separate concern, covered in Encrypting AI Conversations with ReskCrypt.

Step 5 — Audit, detect drift, review on a schedule

Permissions decay. A tool gets a new parameter, an agent inherits a broader role during an incident, a "temporary" exception becomes permanent. Treat the entitlement map as code:

  • Version it in Git and require review for every added bit — the same PR process as application code.
  • Diff the effective mask of each agent in CI against the approved map, and fail the build on unexpected widening.
  • Log every decision (allow, deny, require-approval) with agent ID, caller, tool, scope, policy version and latency. ReskPoints streams agent events for exactly this purpose.
  • Alert on patterns: a spike in denied calls, a new tool used for the first time, PII read without CAP_ACCESS_PII, or the same agent calling tools across unrelated tenants.

A minimal enforcement example

The check itself should be boring and impossible to bypass from the prompt:

def authorize_tool_call(caller, agent, call): mask = caller.mask & agent.mask & call.task_elevation required = TOOL_CAPABILITIES[call.tool] if mask & required != required: audit.deny(caller, agent, call, reason="missing_capability") return Deny("This action is not permitted for your role.") if not scope_allows(call.tool, call.args, caller, agent): audit.deny(caller, agent, call, reason="out_of_scope") return Deny("The requested resource is outside this task's scope.") if TOOL_REVERSIBILITY[call.tool] == "irreversible" and not call.human_approval: return RequireApproval(call) return Allow()

Note what is missing: there is no branch that reads the model's explanation, confidence or claimed role. The model proposes; the gateway disposes.

The five failure modes we see most

1. Prompt-level only. "You must not use the delete tool" is one injection away from being ignored. Enforce below the prompt.

2. Shared god-mode service account. Every agent acts as the same superuser, so logs cannot attribute actions and one compromise is total. Use per-caller identity.

3. Union instead of intersection. The agent inherits the broadest permission in the chain. Always intersect.

4. Scope-free permissions. "Read CRM" without a record constraint is full-database access. Scope every entitlement to the task.

5. No expiry. Elevations granted for an incident never get revoked. Give every elevation a TTL and an owner.

FAQ

How do I give AI agents least-privilege access? Map every entitlement to a tool, scope and consequence; derive a per-agent capability mask from the intersection of caller, agent and task; enforce the mask before generation and again at the tool gateway; isolate credentials; and audit every decision.

What is AI agent entitlement mapping? An inventory of everything an agent can reach — tool, data scope, action, reversibility and PII sensitivity — used as the source of truth for policy and reviews.

How do I enforce least privilege for LLM tool execution? Do it in code, not in the system prompt: block disallowed tool trigger phrases during generation, validate arguments and scope at the gateway, and require human approval for irreversible actions.

How do I prevent AI agent privilege escalation? Intersect permissions rather than union them, bind the agent to the calling user, reject model-supplied scopes or audiences, expire elevations, and alert on permission drift.

Should each agent have its own identity? Yes. An agent should act as the user who launched it, with a mask equal to or narrower than that user's, plus its own workload identity for audit. Never a shared superuser.

Next step

Start with one agent and one week: build the entitlement map, delete every bit you cannot justify, then wire the three enforcement layers. You will usually find two or three tools that no one can explain — which is exactly the point.

For the architecture behind this model, read the AI agent security hub. To see masks and tool gating in action, try the hosted demo at demo.resk.fr.