Module 7 · 7 min read
Allowlisting Tools and Scoping Credentials
The two architectural controls with the most leverage in an agent design, and the compromise test that tells you which of your controls are real.
If you take one design principle from this course, take this one: the security posture of an agent is determined by what is reachable in a session, not by how the model is instructed. Two controls implement that principle. The first decides which tools exist for this task. The second decides what each tool's credential can do. Both are enforced by ordinary code in the orchestration layer, which is why both survive the model being completely subverted.
Start with tool exposure. Consider a platform that lets the model call any registered tool by name, and a task that only needs read access to a ticketing system. The right posture is to expose to that session only the tools the task requires, enforced by the orchestrator outside the model. The mounted tool set becomes a property of the task rather than of the platform, and a payload that asks for the payments tool gets a refusal from code, not a judgement call from a model.
Now credentials. The common anti-pattern is a single broad-privilege service account shared by every tool the agent can reach, often justified by operational convenience. Under that design, any successful injection inherits the union of everything the agent could ever do. The change that most reduces blast radius is to issue each tool its own credential, scoped to the minimum operations and resources that tool needs, and ideally bounded to the acting user's own rights so the agent can never do anything the human on whose behalf it acts could not do themselves. A hijacked summarisation tool then cannot write to billing, because its credential has no billing scope to misuse.
# Session-scoped tool registry: mounted per task, enforced by the orchestrator
session:
task: "answer support questions from the ticket system"
mounted_tools: [tickets.search, tickets.read] # nothing else exists here
denied_by_default: true
tools:
tickets.search:
credential: svc-tickets-read # read scope only
bind_to_acting_user: true # cannot exceed the user's own rights
resources: ["queue:support/*"]
tickets.read:
credential: svc-tickets-read
bind_to_acting_user: true
mail.send:
credential: svc-mail-send
mounted_in: [outbound_workflow] # never in sessions reading untrusted text
requires_confirmation: true
recipient_allowlist: internal_directory # enforced at the mail gateway- Start from the task, not the platformWrite down what this session must accomplish and the minimum set of operations that accomplishes it. The mounted tool set is a property of the task; a platform-wide registry is an inventory, not a permission model.
- Mount only those tools, deny by defaultThe orchestrator exposes the chosen tools and nothing else. A payload asking for a tool that is not mounted receives a refusal from code, with no model judgement involved at any point.
- Give each tool its own credentialScope each credential to the minimum operations and the minimum resources that tool needs. A hijacked summariser then has no billing scope to misuse, because the authority is simply not present.
- Bind authority to the acting userWhere the platform supports it, act with the user's own rights so the agent can never do anything the human on whose behalf it acts could not do themselves. This caps the blast radius at one person's access.
- Validate arguments, then authorise themSchema validation checks shape. Follow it with an authorisation check on the actual values: is this record in scope for this user, is this recipient permitted, is this quantity within policy?
- Gate what cannot be undoneFor the residue of irreversible actions, require human confirmation at the point of execution with the real effect displayed. Everything else executes within policy without interrupting anyone.
The compromise test
Here is a review method that cuts through most architecture arguments. Assume an attacker achieves full control of the model's outputs: whatever text serves their purposes, the model emits. Now walk your control list and ask, for each one, whether it still functions. The controls that survive are the ones you actually have.
| Control | Survives full model compromise? | Why |
|---|---|---|
| Orchestrator-enforced tool allowlist | Yes | Enforced by application code the attacker does not control |
| Argument schema validation | Yes | Deterministic check on the request before execution |
| Per-tool, per-user least-privilege credentials | Yes | The authority simply is not there to misuse |
| Contextual output encoding at every sink | Yes | The sink enforces the split regardless of what was generated |
| Egress and recipient allowlists at gateway or proxy | Yes | Enforced in the network path |
| Human confirmation on irreversible actions | Partly | Holds if the human is shown the real action and the gate is unavoidable |
| System-prompt rules forbidding dangerous behaviour | No | Lives inside the compromised component |
| Fine-tuned refusal behaviour | No | Lives inside the compromised component |
| Guardrail classifier on inputs or outputs | No | Probabilistic and evadable; treat as detection |
Check yourself
An orchestrator validates every tool call against a strict schema and rejects anything malformed. An injection nonetheless causes the agent to archive an entire quarter of records that should have been retained. What did schema validation fail to provide?
Schema validation answers whether the arguments are well formed. It does not answer whether this actor, in this session, is permitted to act on those particular resources at that scale. Shape checking has to be followed by an authorisation decision on the actual values, and by scoping the credential so that the operation is not available in the first place.
Try it first
A platform team resists per-tool credentials because managing twelve scoped identities is operationally awkward compared with one service account. Before reading on: make the cost of the shared account concrete rather than principled.
Under a shared account, the authority available to any successful injection is the union of everything any tool can do, no matter which tool was legitimately in use. So an injection arriving in a summarisation task, where the only intended operation was reading text, can write to whatever the finance tool can write to, delete whatever the archive tool can delete, and send wherever the mail tool can send. The blast radius of every entry point becomes the blast radius of the whole system, and it grows silently every time someone adds a tool, because the new capability is immediately reachable from every existing session. Per-tool scoping converts that into a bounded loss: a compromised summariser reads text it was already reading. Bounding to the acting user's rights bounds it further, to what one person could have done manually. The operational cost is real; it is paid once, in configuration, against an exposure that otherwise compounds with every feature added.
One structural pattern follows naturally and is worth designing towards: separate the sessions that read untrusted content from the sessions that can act. A component that ingests and summarises hostile material runs with no tools and no private data, and its output is carried across to the acting session as clearly labelled untrusted content that can inform an answer but cannot authorise an action. This does not solve injection, and the boundary is only as good as the discipline applied to what crosses it, but it converts an open-ended agent problem into a narrow, reviewable interface.