Zero Trust for AI Agents: A Builder's Guide for SMBs

You gave an AI agent a GitHub token, a Gmail connection, and an invoicing API key so it could "just handle things." Now it runs unattended, thousands of times faster than you could, with credentials that never expire. If any one of those calls goes wrong, from a prompt injection, a bad tool description, or a plain model mistake, nobody is watching in time to stop it.
This post is the practical version of what security vendors call "zero trust at agent speed." No enterprise budget required.
What zero trust actually means for an agent
Zero trust means no user, device, or system is trusted automatically, and every action is verified before it runs, not just at login. For agents, that shifts the question from "did this session authenticate?" to "should this specific action, right now, be allowed?"
The formal reference is NIST SP 800-207, published in August 2020. NIST's announcement describes it as providing an abstract definition of zero trust architecture, general deployment models, use cases, and a high-level roadmap for implementing it. The core ideas are least privilege and per-request access decisions on the assumption that the network is already compromised.
None of that is new. What changed is who is making the requests. Andre Durand, CEO and founder of Ping Identity, argues in a sponsored VentureBeat piece (published July 16, 2026, so read it with that in mind) that enterprises should treat zero trust as an immediate requirement for agents rather than a long-term goal. His illustration: a human compromise might unfold over minutes, hours, or days, while at agentic speed a thousand actions could happen in five minutes. That's his illustrative claim, not measured data, but the direction is hard to argue with. An agent that holds a standing credential and gets manipulated can do damage faster than any alert can reach you.
If you run a five-person business, you don't need a zero trust platform. You need five habits applied consistently. The rest of this post covers them.
The two dials: how much access, for how long
Durand frames agent permissions as two variables: the surface area of access you grant, and how long that access stays valid. Zero trust narrows both at once and keeps revalidating. His shorthand is "just enough, just in time."
Here's what that looks like against the way most small teams wire up agents today:
| Typical setup | "Just enough, just in time" | |
|---|---|---|
| Scope | Full-access API key or personal access token | Narrow scopes for the one task (e.g. read-only on one resource) |
| Lifetime | Long-lived key pasted into an env file, never rotated | Short-lived token issued per task or per session |
| Identity | The agent uses your login or a shared service account | The agent has its own identity |
| Revocation | Rotate the key and hope you found every copy | Revoke one agent's token without touching anything else |
| Audit trail | Logs show "you" did it | Logs show which agent did what, on whose behalf |
The left column is convenient and works fine until it doesn't. The right column takes extra setup per integration, and how much depends on the tool. But it changes what a failure looks like: instead of "the agent had the keys to everything," it's "the agent could only read one folder for ten minutes."
A concrete test you can run today: list every credential your agents hold. For each one, answer two questions. What is the narrowest permission that still lets the task work? When does this credential stop working if I do nothing? If the second answer is "never," that's your first fix.
Give every agent its own identity
Durand is blunt on this: each agent should have its own identity and should not impersonate the human. It acts on the human's behalf through explicitly delegated authority. Cloned human logins and shared service accounts are, in his words, the practice that doesn't work.
The reason is practical, not philosophical. When an agent borrows your login:
- You can't tell your actions from the agent's in the logs.
- You can't revoke the agent without locking yourself out.
- Anything you can do, it can do, including things the task never needed.
When an agent has its own identity with delegated authority, you get the opposite. The audit trail separates "Lazar asked agent X to do Y" from "Lazar did Y." Revoking the agent is one switch. And the agent's permissions can be a strict subset of yours.
For a solopreneur, this can be simple:
- Create a dedicated service identity per agent (or per workflow), not one shared "automation" account.
- In SaaS tools that support it, use OAuth apps or API tokens scoped to that identity rather than your personal token.
- Name them so the logs are readable:
agent-invoice-drafts,agent-lead-triage, notautomation-2. - Keep a one-page inventory: agent name, what it can touch, who owns it, when its credentials expire.
That inventory sounds boring. It's also the thing you'll wish you had the day something looks off.
Where to put the checkpoint: gateways in front of MCP
If your agents talk to tools through the Model Context Protocol, the MCP server boundary is a natural choke point. The VentureBeat article names API gateways and the agent gateway in front of MCP servers as practical places to inspect an agent's request and apply policy before granting access. It also calls moving agents off shared secrets and long-lived standing access an urgent priority.
One thing to be clear about: MCP does not enforce security for you. According to the MCP authorization spec (version 2026-07-28), authorization is OPTIONAL. HTTP-based transports SHOULD conform to it, and STDIO implementations SHOULD NOT follow it and should take credentials from the environment instead. The spec gives you good primitives, but nothing forces you to use them.
Where you do use them, here's what the current spec requires or recommends:
- OAuth 2.1 roles. A protected MCP server acts as an OAuth 2.1 resource server, and the MCP client acts as an OAuth 2.1 client. Authorization servers MUST implement OAuth 2.1.
- Audience binding. Servers must validate that access tokens were issued specifically for them (RFC 8707) and must not accept or pass through tokens meant for other services. Clients must send the
resourceparameter in authorization and token requests. Tokens must not go in the URI query string. - Scope challenges. Servers SHOULD declare required scopes in the
WWW-Authenticateheader. Clients treat the challenged scopes as authoritative. An insufficient-scope error returns HTTP 403, letting the client step up to additional permissions at runtime instead of starting with everything. - Issuer validation. Per the changelog, clients MUST validate a present
issparameter (RFC 9207) before redeeming the authorization code. Client credentials are bound to the issuing authorization server. Dynamic Client Registration (RFC 7591) is deprecated in favor of Client ID Metadata Documents, though it still works for backward compatibility.
The step-up behavior is worth dwelling on, because it's zero trust in miniature. An agent starts with a narrow token. When it needs more, the server says so with a 403 and a scope challenge, and the agent has to go get that permission rather than having been handed it up front.
One caveat: the spec doesn't define what your scopes are. That's up to whoever builds the server. "Scoped tokens" only helps if someone designed scopes worth having, so a scope called all is the same as no scope at all.
Authorize the action, not just the session
Durand's example of per-action authorization is an agent committing code to a repository. Instead of granting a standing permission to write to GitHub, the request is checked against context and policy at the moment it happens.
You can build a small version of this without any vendor. Put a policy check between the agent's decision and the tool call:
from dataclasses import dataclass
@dataclass
class ToolCall:
agent_id: str
tool: str
action: str
target: str
params: dict
# Rules live in one place, not scattered across prompts.
POLICY = {
("agent-invoice-drafts", "invoicing", "create_draft"): "allow",
("agent-invoice-drafts", "invoicing", "send"): "require_human",
("agent-invoice-drafts", "invoicing", "delete"): "deny",
("agent-lead-triage", "crm", "read"): "allow",
("agent-lead-triage", "crm", "update_stage"): "allow",
("agent-lead-triage", "email", "send_external"): "require_human",
}
def decide(call: ToolCall) -> str:
# Default-deny: anything not listed is refused.
return POLICY.get((call.agent_id, call.tool, call.action), "deny")
def execute(call: ToolCall, approve_fn, run_fn):
verdict = decide(call)
log(call, verdict) # always log, including denials
if verdict == "deny":
raise PermissionError(f"{call.agent_id} may not {call.tool}.{call.action}")
if verdict == "require_human" and not approve_fn(call):
raise PermissionError("Human approval declined")
return run_fn(call)
Three design choices matter here:
- Default-deny. An action that isn't in the table is refused. New capabilities have to be added deliberately.
- Policy outside the prompt. Rules written in a system prompt are suggestions the model may ignore or be talked out of. Rules enforced in code are not negotiable by the model.
- Log denials, not just successes. A burst of denied calls is often your first signal that something is steering the agent somewhere it shouldn't go.
This is a sketch, not a product. In production you'd want the policy in config you can review and version, context-aware conditions (amount thresholds, recipient domain, time of day), and tamper-resistant logs. But even this crude version beats a standing token.
Don't rely on the model to police itself
There's a tempting shortcut: tell the agent in its prompt to be careful, and trust that it will be. Durand addresses why that isn't enough. If generative AI follows instructions 97% of the time, he says, that may be fine for advice, but for a decision about who gets let in, 97% is not good enough. (Again, 97% is his illustrative number, not a benchmark.)
Do the math on a small example with round numbers. Suppose an agent makes 200 tool calls a day and a model-level guardrail is right 97% of the time. That leaves roughly 6 calls a day that slip past, which is about 180 a month. Most are harmless. But you don't get to choose which ones, and you don't want the dangerous ones to depend on a coin that lands your way 97% of the time. (This is an illustration with made-up round numbers, not data from any study.)
This is also why OWASP ranks the risk where it does. In the OWASP Top 10 for LLM Applications 2026, Excessive Agency is listed as LLM03:2026, up from LLM06 in the 2025 list. It arises when an application gives an agent more functions, permissions, or autonomy than the task requires. (The publication date of the 2026 list is reported inconsistently across sources, so check OWASP's own site if you need to cite it.) The fix is structural: reduce what the agent can do, rather than hoping it chooses well.
The practical rule: use the model for judgment calls where being occasionally wrong is cheap, and use deterministic code for anything where being wrong is expensive.
Human gates and a kill switch
For higher-risk decisions, the article recommends combining automated review with clear human accountability. It also mentions a kill switch that can halt an agent when a sequence of actions crosses a risk threshold.
You don't need to approve everything; that defeats the point of automating. Gate by consequence:
| Action type | Example | Gate |
|---|---|---|
| Read-only, internal | Summarize inbox, read CRM records | Auto-allow, log |
| Reversible write | Create a draft invoice, tag a lead | Auto-allow with narrow scope, log |
| Hard to reverse | Send email to a customer, publish content | Human approval |
| Money or access | Issue refund, change permissions, delete data | Human approval, ideally with a second signal |
A kill switch for a small setup can be as plain as a counter and a flag:
import time
from collections import deque
class CircuitBreaker:
def __init__(self, max_actions=50, window_seconds=300, max_denials=5):
self.events = deque()
self.denials = deque()
self.max_actions = max_actions
self.window = window_seconds
self.max_denials = max_denials
self.halted = False
def record(self, denied=False):
now = time.time()
self.events.append(now)
if denied:
self.denials.append(now)
for q in (self.events, self.denials):
while q and now - q[0] > self.window:
q.popleft()
if len(self.events) > self.max_actions or len(self.denials) >= self.max_denials:
self.halted = True # require a human to reset
def check(self):
if self.halted:
raise RuntimeError("Agent halted: risk threshold crossed. Human reset required.")
The thresholds above are placeholders; tune them to what normal looks like for your workflow. The point is that a runaway sequence of actions, or a pile of denials, stops the agent automatically and waits for a person. Per Durand's framing, that threshold-based halt is what makes zero trust work at machine speed: continuous revalidation can't depend on you noticing something in real time.
A one-weekend hardening checklist
If you only do one thing this week, work through this list in order:
- Inventory. List every agent, every tool it touches, and every credential it holds.
- Kill standing secrets. Replace long-lived, all-powerful keys with scoped, expiring tokens wherever the tool supports it. Where it doesn't, store the secret in a vault, not a file in the repo.
- Split identities. One identity per agent or workflow. Stop using your own login for automation.
- Narrow scopes. For each credential, apply the narrowest permission that still works. Re-test.
- Add a policy layer. Default-deny table in code between the model and the tools.
- Gate the irreversible. Human approval for anything that sends, spends, deletes, or changes access.
- Add a breaker. Rate and denial thresholds that halt the agent.
- Check MCP specifics. If you run MCP servers over HTTP, confirm they validate token audience, don't pass tokens through, and challenge for scopes. Remember that STDIO servers pull credentials from the environment, so keep that environment minimal.
- Review the logs weekly. Look at denials first.
Skip the vendor demos until you've done this.
Where I'd start
My own view, as someone who builds AI automations for small teams: give each agent its own identity, scope tokens to the task, and route anything irreversible through a human gate by default. Scope design is a decision you make per integration, since the MCP spec itself leaves scope definitions to the implementer.
If you already have agents running with broad keys, the order of work is the same: inventory what exists, tighten scopes, add the gates, and keep the workflow itself working. BizFlowAI is where I publish practical write-ups like this on AI automation for small teams.
Work with BizFlowAI
If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.
Request a free discovery call — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.
More guides like this on the BizFlowAI blog.
Frequently asked questions
What does zero trust mean for AI agents?
Zero trust means no user, device, or system is trusted automatically, and every action is verified before it runs. For AI agents, this shifts the question from 'did this session authenticate?' to 'should this specific action be allowed right now?' Agents act far faster than humans, so a manipulated agent with standing credentials can cause damage before any alert reaches you. The practical answer is narrow permissions, short-lived credentials, and per-action checks.
How do I give an AI agent its own identity instead of using my login?
Create a dedicated service identity for each agent or workflow, rather than reusing your personal token or a shared automation account. Use OAuth apps or scoped API tokens tied to that identity, and name them clearly, such as agent-invoice-drafts, so logs are readable. This lets you tell your actions apart from the agent's, revoke one agent without locking yourself out, and limit its permissions to a strict subset of yours.
Does the Model Context Protocol enforce security automatically?
No. In the MCP authorization spec (version 2026-07-28), authorization is optional. HTTP-based transports should conform to it, while STDIO implementations should instead take credentials from the environment. Where used, it builds on OAuth 2.1, requires audience-bound tokens, and supports scope challenges. Whether your setup is actually secure depends on whether your server implements these controls and defines meaningful scopes.
What is 'just enough, just in time' access for AI agents?
It is a principle that narrows two variables at once: how much access an agent has and how long that access lasts. Instead of a full-access API key that never expires, the agent gets narrow scopes for one task and a short-lived token issued per task or session. If something goes wrong, the failure is limited to something like reading one folder for ten minutes rather than losing control of everything.
How can a small business authorize AI agent actions without an enterprise platform?
Put a simple policy check between the agent's decision and the tool call. Define rules in one place that map each agent, tool, and action to allow, deny, or require human approval. For example, let an invoicing agent create drafts, require a human to approve sending, and deny deletion. Pair this with a one-page inventory of each agent's access, owner, and credential expiry.