← Back to Blog

Minimum Privilege for AI Agents: Why OAuth Scopes Aren't Enough

Minimum privilege scope reduction

The principle of least privilege is well-established in human IAM. For AI agents, the same principle needs a fundamentally different implementation — one that accounts for the fact that agents request access programmatically, run continuously, and have no natural attention limit on what they ask for.

What least privilege means in traditional IAM

In traditional identity and access management, least privilege means granting users and service accounts only the permissions they need to do their job. In practice, this is implemented through role-based access control (RBAC): you define roles with specific permission sets, assign users to roles, and periodically audit whether the role still matches the user's actual responsibilities.

This model works reasonably well for humans and service accounts with stable, predictable access patterns. A human analyst who needs read access to financial reports gets a read-only role. A deployment pipeline that needs to push container images gets a specific IAM role bound to that action. The access pattern is known in advance and encodes into the role definition.

AI agents break this model in two ways. First, their access patterns are determined at runtime by the LLM, not by a developer writing a role definition. The agent decides what tools to call based on the task at hand — which means the scope of access it needs can vary significantly from run to run. Second, agents can run hundreds of tasks per day, making manual audit of access patterns impractical. By the time a human reviews what an agent has been doing, weeks of credential-using behavior have already happened.

The coarseness problem with OAuth scopes

OAuth scopes are the mechanism providers use to express what an application can access. They are designed for human-authorized applications where a user sees a consent screen listing what the app wants. The granularity is calibrated to what is comprehensible to a human in 10 seconds: "This app wants to read your calendar" or "This app wants to post to your GitHub repositories."

For AI agents, this granularity is often wrong in both directions. Sometimes it is too coarse: GitHub's repo scope grants access to all repositories when the agent only needs one. Google's https://www.googleapis.com/auth/calendar scope grants read and write access to all calendars when the agent only needs to read one specific calendar. The scope system cannot express the precision the agent actually requires.

Other times the scope is too granular: an agent that needs to perform several operations across a Google Workspace environment ends up requesting a large list of individual scopes, and the complexity of managing that list correctly becomes its own attack surface — especially when the scope list gets copy-pasted between agents that have different access requirements.

Static scopes in a dynamic world

The deeper problem is that OAuth scopes are static. They are defined at authorization time and remain fixed for the lifetime of the token. But an AI agent's actual access needs vary by task. An agent that handles both read-only reporting tasks and write-heavy data manipulation tasks needs different scopes at different times — but OAuth's model requires you to decide at authorization time which scopes to request, typically leading to requesting everything the agent might ever need.

This is the "request everything upfront" failure mode. It is not malicious — developers do it because it avoids authorization errors mid-task. But the result is tokens that carry far more privilege than any individual task requires. If that token is compromised, the attacker gets all of it.

A better model issues tokens with task-scoped access: the scope covers exactly what the specific task requires, the token expires when the task completes, and the next task gets a fresh token with whatever scopes that task needs. This is the minimum-privilege ideal, but standard OAuth does not support it natively. You need a layer in front of the OAuth provider that can downscope tokens on a per-task basis.

The downscoping pattern

Downscoping is the practice of issuing a child token with a subset of the scopes of a parent token. Several OAuth providers support this natively — Google's OAuth 2.0 token endpoint accepts a downscope parameter that lets you narrow the scope of an existing token without creating a new authorization flow. AWS similarly supports session policies that restrict what an assumed IAM role can do, even within the permissions the role nominally has.

For providers that do not support downscoping natively, a proxy layer can enforce scope restrictions at the call level: you issue a full-scope token to the proxy, and the proxy enforces which API endpoints the agent is allowed to call with it. The effect is equivalent to downscoping — the agent cannot exceed the allowed call set — but it requires the proxy to understand the provider's API surface well enough to map scopes to endpoints.

Alter uses both mechanisms depending on the provider. For providers that support native downscoping (Google, AWS), we issue appropriately scoped tokens directly. For providers that do not (GitHub, Slack), we enforce access at the proxy layer, blocking requests to endpoints outside the policy-defined set before they reach the provider.

Policy-as-code for agent credentials

The practical implementation of minimum privilege for AI agents requires moving scope definitions from per-token decisions to policy definitions that govern a class of agents. Instead of "this agent gets these scopes," the model becomes "agents in role X get a maximum scope of Y, which can be further restricted to Z for task type T."

In YAML, an Alter policy for a code review agent might look like this:

agent_policy:
  id: code-review-agent
  integrations:
    github:
      max_scope: [repo:read, pr:read, issues:read]
      deny_scope: [repo:write, admin:org, delete_repo]
    slack:
      max_scope: [channels:read, chat:write]
      deny_scope: [admin, channels:manage]
  token_ttl: 3600  # 1 hour
  per_task_downscope: true

The max_scope field is the ceiling — even if the agent requests write access, the policy caps it at read. The deny_scope field is an explicit block — if a future version of the agent somehow requests admin access, it is refused regardless of what the max_scope says. The per_task_downscope flag tells Alter to issue tokens scoped to the specific task context, not the maximum allowed scope.

Behavioral minimum privilege: what the agent actually uses

Writing correct max_scope policies requires knowing what the agent actually needs. For new agents, this is guesswork — you write a policy based on what the agent is designed to do and adjust it after observing actual behavior. But for agents that have been running for a while, there is a better source: the audit log.

Alter's policy suggestion engine analyzes the audit log for each agent and identifies the gap between the scopes tokens carried and the scopes the agent actually used. An agent that has been running for 90 days with repo:write but has only ever called read endpoints is a candidate for a policy update that removes repo:write. The suggestion engine surfaces these gaps weekly and lets operators approve the narrower policy with one click.

This is behavioral minimum privilege: the policy converges on the minimum needed based on observed behavior rather than developer estimates. It is harder to implement than static YAML policies, but it is the only approach that adapts as the agent's behavior changes over time.

The break-glass problem

Any minimum-privilege system needs a break-glass mechanism: what happens when the agent legitimately needs to exceed its normal scope for an unusual task? If the policy is too rigid, agents fail on legitimate edge cases. If the policy is too permissive, you are back to the default-grant-everything problem.

The right model is escalation with approval: the agent requests elevated access, a human operator approves or denies, the elevated access is granted for a specific time window, and the escalation is logged. This preserves minimum privilege as the default while allowing legitimate exceptions with an audit trail. Alter implements this via a webhook that fires when an agent requests scope beyond its policy ceiling — the operator's Slack channel or PagerDuty instance receives the escalation request and can approve it from the notification.

Where scope enforcement breaks down

Scope enforcement at the token level only works for API calls that go through the OAuth provider's access control. If an agent has write access to a GitHub repository and uses that access to write a file that subsequently triggers a GitHub Actions workflow, the scope enforcement stopped at the direct API call but the downstream effect — the Actions run — is not constrained by the agent's scope. Side-channel effects are outside the scope model entirely.

This is a fundamental limit. Minimum privilege at the credential layer reduces the direct blast radius of a compromised or misbehaving agent, but it does not eliminate all vectors. The combination of scope enforcement, behavioral monitoring, and task-level sandboxing is the complete picture. Scope enforcement is necessary but not sufficient. It is the first layer, not the last.