All writing

Just-in-time access shrinks an agent's window, not its reach

Short-lived credentials limit how long an AI agent holds access, not what it can do. Put hard limits in code first, then shorten the tokens.

Picture a support agent that can refund customers up to R500. An attacker opens a ticket with instructions buried in it, the agent reads them, and it refunds R50,000.

The vendor pitch for this failure is just-in-time access: no standing privileges, a fresh token for every call, expiring in seconds. Now run the same ticket again. The agent asks for a token scoped to “issue refunds”, gets one that lives for 30 seconds, and issues the R50,000 refund in the first two.

Just-in-time credentials shrink how long an agent holds access, not what it can do with it. For an agent, the control that matters is a hard limit enforced outside the model: the refund API that rejects anything over R500. JIT versus standing access comes second. It’s worth doing, but after the limit, not instead of it.

This is for the people wiring agents into business systems and the people signing off on them, at firms where the security team might be one person. It picks up where the least-autonomy post left off: once you’ve decided a step needs an agent, what actually fences it in.

A 30-second token still issues the refund

Security people have two ways to limit the damage a credential can do. One limits it in time: the token expires, so a thief gets a short window. The other limits it in scope: the token, or the system behind it, only allows certain actions with certain values.

Prompt injection doesn’t steal the credential. It borrows the agent, which already holds the credential legitimately, at the moment it’s using it. However short the token’s life, the attacker’s instruction runs inside it. Only a scope limit changes the outcome.

Control Limits Stops the injected R50,000 refund?
Token expires in seconds Time No. The refund happens inside the window
No standing privileges, token issued per task Time No. The task is “issue a refund”
Token scoped to the refund tool only Which tool No. It’s the right tool
Refund API rejects amounts over R500 Value Yes
Refunds only to the card that paid Recipient Limits it to the original customer
At most five refunds an hour per agent Rate Caps the total loss

OWASP’s LLM06 Excessive Agency entry says it directly: implement authorisation in downstream systems rather than relying on an LLM to decide whether an action is allowed. The R500 cap in the prompt is a request. The R500 cap in the refund API is a control.

A policy engine sees the request, never the intent

The next layer of the pitch is “intent-based” authorisation: a policy engine that checks whether the agent’s action matches what it meant to do. It sounds like the answer to injection. It isn’t, because there is no intent to check.

A policy engine sees a request: the caller, the endpoint, the parameters. When a product says it evaluates intent, the intent is text the model wrote, usually a field like reason: "customer requested refund for damaged goods". The model wrote that after reading the attacker’s ticket. It’s attacker-controllable text, the same as everything else the model produces, and an injected ticket can tell the model what reason to give.

What a policy engine can check reliably is the request itself:

  • Parameters. The amount is under R500. The date range is under 90 days. The record count is under 50.
  • Recipients. The email goes to an address already on the client’s file. The payment goes to an account on the allowlist.
  • Rates. No more than a set number of these actions per hour, per agent and per customer.
  • Pairing. The refund matches an order that exists, for no more than its value.

All four are deterministic. The same request gives the same answer every time, and an attacker can’t talk them round.

A second model reads the same poisoned ticket

Another common design splits the agent into roles: a planner, an inspector that reviews the plan, and an executor that holds the credentials. It helps only when the split removes something the attacker needs.

If the inspector is another language model, it reads the same ticket the planner read, instructions and all. Models follow instructions in content, and they can’t reliably tell yours from an attacker’s. An injection that fooled the planner can carry a second paragraph addressed to the inspector. Two models in series aren’t two controls. They’re one control that runs twice. The inspector has to be code.

The split that works is the one that takes a leg off the lethal trifecta. Simon Willison described it in 2023 as the dual LLM pattern: a quarantined model reads the untrusted content and has no tools, and a privileged model holds the tools and never sees that content directly. The component that reads the customer’s ticket should hold no credentials at all. It passes structured fields (an order number, a category, an amount) to code, and code decides what happens next.

An agent never holds more than the person it works for

Most business agents act for someone: the customer on the chat, the adviser who asked for a summary, the claims clerk working a queue. The service-account habit is to give the agent one broad token that can reach every customer’s records, and then trust the model to only look at the right one.

Turn that round. The agent gets a token derived from the user it’s acting for, and never more than that user could do. Usually it gets less, because the agent needs read access to one client’s file, not the adviser’s full book. OWASP’s LLM06 guidance says the same: actions taken for a user should run on downstream systems in that user’s context.

OAuth has a standard for this. RFC 8693, OAuth 2.0 Token Exchange (January 2020) defines how a service swaps one token for another, including delegation. With delegation the agent keeps its own identity, separate from the user’s, and the token’s act claim records that the agent is acting for the user. An injected instruction to read another client’s file then fails at the API, because the token doesn’t reach it. The log shows both names: which agent did it, and for whom.

Approval gates work only when they’re rare

Every design in this area ends with a human approval step for risky actions, and OWASP recommends one for high-impact actions. I agree, with one condition: it has to fire rarely.

An approval prompt that appears forty times a day trains the approver to click yes. The nearest analogy I know is MFA push fatigue, where enough prompts turn a security check into noise and a tired user eventually accepts one. An agent’s approval queue drifts the same way, and nobody has to attack it for that to happen.

Keep approvals for what can’t be undone or is unusual: a payment over the cap, a first-time recipient, deleting records, anything a client will see before anyone else does. Everything routine sits under a hard limit in code and needs no click. If a gate fires often enough that people stop reading it, move the limit, not the person.

Short-lived credentials still earn second place

Here’s the best argument for JIT, and it’s a good one. Agents leak credentials. A token ends up in an environment variable that gets printed to a log, a debug trace, a support bundle, or a compromised MCP server the agent talks to. A token that expired four minutes after it was issued is worth very little to whoever finds it. A standing key that works until someone rotates it is worth a great deal.

Better still is having no secret to leak. Google Cloud’s Workload Identity Federation exists to replace service account keys, which Google itself calls powerful credentials that are a risk when mismanaged, with short-lived access tokens.

Per-task tokens also fix a real audit problem. When every action runs under one shared service token, the log says the service did it. When each task gets its own token, tied to a user and a ticket, the log answers who asked for what.

So do it. JIT addresses a real threat: the stolen credential. Prompt injection is a different threat, and the agent doesn’t need to lose its credential for the attacker to use it. Order the work by which threat you’re more likely to meet this year.

A five-person FSP needs vendor limits, not a policy engine

Everything above assumes you run the agent’s code. A five-person financial services provider running one agent inside a vendor’s SaaS has no policy engine, no token broker and no developer to build either. That doesn’t leave it with nothing. The proportionate version uses what the vendor already enforces:

  • The narrowest OAuth scopes the integration offers: read-only where the agent only reads, one mailbox instead of all of them.
  • Per-tool limits on the vendor’s side: spend caps, recipient allowlists, a daily action ceiling.
  • A separate account for the agent, never a person’s login shared with it.
  • Logs that record which user the agent acted for, and someone who reads them.

A vendor that can’t offer any of these is telling you something about how its product will fail.

An agent’s credential is a service account, whatever the vendor calls it. POPIA section 19 already asks you to take appropriate, reasonable technical and organisational measures against unlawful access to personal information.Sidenote: My reading of POPIA as a practitioner, not legal advice. Your compliance officer decides how it applies to your firm.

Write the limit down before you shorten the token

Expect a blank next to at least one tool on the first pass. Start with the tool that can move money or send data out of the firm, and put a number on it.

The R50,000 refund in the opening needed a token for two seconds. A R500 cap in the refund API would have stopped it at the first request.