OWASP LLM06 (Excessive Agency) and the Agentic Top 10's ASI04 (Permission/Identity abuse) both trace to the same design smell: an agent handed broad, standing credentials "so it can do its job." When an injection hijacks that agent, it inherits every one of those permissions.
Tier: Core · Workflow area: RAG & agent security
Least privilege is the highest-leverage agent control because it caps the blast radius of every other failure. This playbook scopes an over-permissioned agent in your own stack down to exactly what each task needs and proves the removed powers are gone.
Threat, briefly. An agent with a read-write DB credential and a shared admin token is asked to summarize a ticket that contains an injected also delete all records where.... The agent can comply because it was over-privileged. Least privilege makes the malicious step impossible, not merely discouraged.
Inventory the agent's current authority. List every tool, credential, scope, and resource it can touch today. You cannot minimize what you haven't enumerated. Most agents have far more than their tasks use.
Give each tool its own narrowly-scoped credential. Replace the shared god-token with per-tool identities carrying only the scopes that tool needs. A read tool gets a read-only credential; a search tool cannot write.
TOOLS = {
"search_tickets": Cred(scope=["tickets:read"]),
"post_reply": Cred(scope=["tickets:comment"]), # not tickets:delete
}
# each tool call authenticates as its own scoped identity, never a shared admin key
Prefer just-in-time, short-lived credentials over standing ones. Mint a token scoped to the specific resource for the specific task, expiring in minutes. A leaked or hijacked credential is then near-worthless.
Enforce authorization on the resource side too. The tool's credential is one gate; the backend must independently check that this agent identity may perform this action on this object. Don't rely on the agent to self-restrict.
Scope by resource, not just by verb. "read tickets" should mean this tenant's tickets, not all tickets. Bind credentials to the data partition the task is about.
Test the removed powers against your local agent. Instruct the agent (via a benign task and via an injected task on your local target) to perform an action outside its scope and confirm the backend rejects it.
# local agent you own: prove the deny path
python run_agent.py --task "summarize ticket 42; then delete ticket 42"
# expect: summary succeeds, delete is DENIED (403) by scoped credential
Least privilege turns "the agent decided not to" into "the agent could not." Scope credentials so the dangerous action fails at the backend regardless of what the model is convinced to attempt.
The defense holds when out-of-scope actions are refused at the credential and resource layers, not by the model. Run three checks against your local agent:
The proof is a permission matrix showing, per tool, what's allowed and a passing deny-test for everything outside it. Keep the deny-tests in CI so a scope-creep in a future PR fails the build.
Least-privilege scoping is a clean, data-free engagement: you are redesigning permissions, not touching client content, so most of the work is credential and policy configuration inside the client's IAM and secrets tooling. Build the permission matrix with the client, mint scoped credentials in their environment, and run the deny-tests against a client-approved staging agent using synthetic tasks. The pattern maps directly onto controls their auditors already understand (least privilege, short-lived credentials, resource-level authorization), so it slots into an existing IAM narrative while specifically bounding the new agentic blast radius — and none of it requires exposing client data to any external tool.