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.

yaml
# 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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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?
  6. 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.

ControlSurvives full model compromise?Why
Orchestrator-enforced tool allowlistYesEnforced by application code the attacker does not control
Argument schema validationYesDeterministic check on the request before execution
Per-tool, per-user least-privilege credentialsYesThe authority simply is not there to misuse
Contextual output encoding at every sinkYesThe sink enforces the split regardless of what was generated
Egress and recipient allowlists at gateway or proxyYesEnforced in the network path
Human confirmation on irreversible actionsPartlyHolds if the human is shown the real action and the gate is unavoidable
System-prompt rules forbidding dangerous behaviourNoLives inside the compromised component
Fine-tuned refusal behaviourNoLives inside the compromised component
Guardrail classifier on inputs or outputsNoProbabilistic 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?

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.

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.