Where the secrets go: least privilege for AI tool calls
Continuous integration pipelines went through this a decade ago. A build job used to run with a long-lived token that could do anything in the repository, and one compromised dependency could use it. The fix was not a smarter build job. It was to give each job a short-lived token with only the permissions its task needs, declared in the pipeline file where a reviewer can see them. AI pipelines are at the same point now, with one extra twist: the thing deciding what to do with the credential may be a model reading text an attacker wrote. This guide covers the three places a credential can sit in an AI system, what the model can do with it in each, and how to write a pipeline so that every step has exactly the authority it needs.
What CI pipelines learned
GitHub’s own hardening guide for Actions is the shortest statement of the principle. On the token every job receives: "make sure that the GITHUB_TOKEN is granted the minimum required permissions." The recommended default is read-only, and then "the permissions can then be increased, as required, for individual jobs within the workflow file." For cloud access it goes further, recommending OpenID Connect "to create short-lived, well-scoped access tokens for your workflow runs" so that you can "stop storing these credentials as long-lived secrets."
Three ideas are packed in there, and all three carry over to AI pipelines. Authority is declared per step, in the file, where a reviewer reads it. The default is nothing, and each step asks for more. And a credential is minted for the run, scoped to the task, rather than stored and shared.
Three places a credential can sit
In the process the model steers
The common agent setup: the tools are functions in the same process as the loop, the process has the API keys in its environment, and the model chooses which function to call with what arguments. The model never reads the key, but it does not need to. It steers the process that holds it, and its output is the process’s control flow. OWASP lists the failure mode as excessive agency, with three root causes: excessive functionality (the tools expose more operations than the task needs), excessive permissions (the credential can do more than the task needs) and excessive autonomy (consequential actions run without a check). All three are the default in this setup, because the key is the one the developer had.
Behind a tool server
A protocol boundary between the model and the credential is an improvement: the model asks a server to do something, and the server holds the key. The Model Context Protocol, the most used such boundary, publishes its own security guidance, and it reads like a list of ways the improvement is lost. Token passthrough, where a server forwards a token it did not validate, is forbidden outright: "MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server." It warns against "wildcard or omnibus scopes" and recommends a minimal initial scope with elevation per operation. The boundary helps, but the model still decides which tool to call and when, so the server is defending against a caller that may be reading an attacker’s text.
Outside the run entirely
The third arrangement is the CI one. The pipeline document declares, per step, the one operation that step may perform and the one endpoint it may call. The platform signs each call so the endpoint can verify who sent it and for which step. The endpoint holds its own credentials and decides, per operation, what it will do. The model has no authority at all: its output is data that becomes the arguments of a call it did not choose. Reviewing the pipeline’s authority means reading the document, the same way reviewing a CI pipeline’s authority means reading its permissions blocks.
What signing the call buys
A signed call is a webhook in reverse. The endpoint and the platform share a secret, the platform computes a keyed hash over the request body, and the endpoint verifies it before acting. Done properly, the signed body names the operation, the run and the step, so the endpoint can authorise per operation: this operation may insert a row, that one may only search. The operation is authenticated along with the data, so an injected output cannot rename the operation it is carried by.
- A stable call identifier per attempt lets the endpoint recognise a retry and answer it the same way, which is what makes an idempotent handler possible.
- A single sending address, rather than calls from wherever a machine happened to be, lets the endpoint allowlist one source and log every call from it.
- A non-idempotent operation should be made once, ever. If the sender is lost mid-call, the right outcome is a failed step reported to you, not a second attempt that doubles a write.
- The signing secret is shown once at registration and stored only by the endpoint and the platform. A secret that can be read back from an API can be read back by an attacker.
What is still on you
- A shared secret per endpoint is coarser than a credential per operation. The per-operation check is yours to write in the handler, using the operation name the signature covers.
- The endpoint is reachable from the internet, because a platform calls it. Require HTTPS, verify the signature before doing anything else, and allowlist the sender’s address if it publishes one.
- Do not log the request body if it carries customer data. The same rule the platform follows for prompts applies to your receiver.
- Never put a credential in a prompt or in an item. A model will repeat what it was given, which is why OWASP lists sensitive information disclosure and system prompt leakage as risks of their own. The whole point of the arrangement above is that the model never needs one.
Tool calls on Otium
Otium’s tool steps are the third arrangement. A customer registers an endpoint once, with an HTTPS URL and a signing secret shown once; a workflow names one endpoint and each tool step names one operation. Every call is a signed POST whose body names the operation, the run and the step, with the same fields repeated in headers for routing, so a handler dispatches on an operation that was authenticated with the data. The worker API signs the request at the moment the call is leased, so the runner that sends it holds one signature over one body and never the key. The control plane, which holds the database and the key store, never dials a customer URL. A step not declared idempotent is called once, and a lost runner fails it rather than repeating it. Calls leave from one address, shown to signed-in customers for allowlisting.
Model steps on Otium hold nothing: a prompt in, text out, validated against the schema the step declared. A step that waits for your system or a person parks the run; the resume is an authenticated API call with a single-use token.
Frequently asked questions
Is a tool server (MCP) not enough on its own?
It moves the credential behind a boundary, which is better than leaving it in the model’s process. But the model still chooses which tool to call, so the server must defend against a caller that may be reading an attacker’s text, and the protocol’s own guidance lists the ways that goes wrong. Declaring the call in a document removes the choice rather than guarding it.
How is this different from a plain webhook?
A webhook is the platform telling you something happened. A signed tool call is the platform asking your endpoint to do one declared thing with the model’s output as the arguments, and waiting for the answer. The signing is the same shape; the direction and the authority are reversed.
What if a step genuinely needs to do several things?
Make it several steps, each with one operation. The cost is a few more lines in the document. The benefit is that a reviewer can see each write on its own, and the one that must not repeat can be marked so.
Related guides
An injected instruction can only do what the system around the model allows. Where the three ingredients of an attack live in an agent loop and a fixed graph.
The questions a security reviewer should ask any AI inference vendor, what a good answer looks like, and the three things no vendor can honestly promise.
A model file can run code when it loads, a repository can change under you, and weights can carry a trigger. What stops each attack, and what nothing stops yet.
Most useful AI work is several model calls in a row. The common patterns, where chained jobs lose time and money, and how to design steps that hold up.
Schemas, validation, retries and escalation: how to get JSON from a language model that your code can trust, and what to do with the answers that fail.