Back to writing
SecurityAgentsLLM InfrastructureArchitecture

Your Agent Has More Access Than Your Junior Developer. That's a Problem.

Jun 18, 2026 · 10 min read

Agent security, governance, and why the industry is sleepwalking into production without guardrails.


I watched an agent bypass its container and write to the host filesystem.

Nothing catastrophic happened. It didn't run a destructive script. It didn't exfiltrate data. It just reached outside the boundary it was supposed to stay inside, and wrote files where it had no business writing. A quiet reminder that the container you think is sandboxing your agent may not be doing what you think it's doing.

I've also seen an agent execute a delete statement on production data and then, in its next response, essentially say, "I probably shouldn't have done that." Which would be funny if it weren't terrifying.

These aren't hypothetical risks. These are things that happened. And they happened in systems that were designed with some level of care. The teams that aren't thinking about agent security at all? I don't want to know what's happening in their production environments.

Here's the uncomfortable truth: most agentic systems in production today have more access, fewer constraints, and less oversight than a junior developer on their first day. A new hire gets scoped permissions, code review, and limited production access. An agent gets a set of API keys and a system prompt that says "be helpful."

That needs to change. And it needs to change now, not after the first major incident.


Least privilege. No exceptions.

The foundational principle is simple and non-negotiable: an agent should never have more access than it needs to perform its specific task.

This sounds obvious. In practice, almost nobody does it properly. The default pattern I see is: give the agent credentials for the services it needs, let it figure out what to do. The credentials have broad access because it's easier to set up. The agent technically could read, write, and delete across the entire service. The team trusts that the prompt will keep it in line.

The prompt will not keep it in line. Prompts are suggestions to a probabilistic system. They are not access controls.

What I enforce instead:

No direct key access. The agent never holds raw API keys or database credentials. Most teams do something like this: hand the agent a connection string and let it talk to the database directly:

python

# This is the problem. The agent holds raw credentials.
agent = Agent(
    tools=[DatabaseTool(connection_string="postgres://admin:password@prod-db/main")],
    llm=ChatOpenAI(api_key="sk-...")
)

If that agent is compromised through prompt injection, unexpected behaviour, or a bug, it has full access to everything those credentials allow. Read, write, delete. The agent IS the admin.

What I build instead is a Tool Proxy, a lightweight service that sits between the agent and every external resource. The agent never touches credentials. It calls tools through the proxy, and the proxy holds the keys, validates the request, and executes on the agent's behalf.

The agent's tool definition looks like this:

python

# The agent's tool — no credentials anywhere in the agent's context
class DatabaseQueryTool:
    def execute(self, query_request: QueryRequest) -> QueryResult:
        response = httpx.post(
            "http://tool-proxy:8000/database/query",
            json={
                "correlation_id": self.run_context.correlation_id,
                "agent_id": self.agent_id,
                "user_id": self.run_context.user_id,
                "query": query_request.query,
                "params": query_request.params
            }
        )
        return QueryResult.parse(response.json())

The proxy service, where the actual validation and credentials live:

python

@app.post("/database/query")
async def handle_db_query(req: ProxyRequest):
    # 1. Is this agent allowed to use this tool?
    if req.agent_id not in AGENT_TOOL_PERMISSIONS["database"]:
        audit.log_unauthorized(req)
        raise HTTPException(403, "Agent not authorized")

    # 2. Does the query match allowed patterns?
    if not query_validator.is_allowed(req.query, req.agent_id):
        audit.log_blocked_query(req)
        raise HTTPException(403, "Query pattern not allowed")

    # 3. Get scoped credentials from vault
    scoped_creds = await vault.get_scoped_token(
        user_id=req.user_id,
        resource="database",
        action=query_validator.classify_action(req.query)
    )

    # 4. Execute with scoped credentials
    result = await db.execute(req.query, req.params, credentials=scoped_creds)

    # 5. Scan output for sensitive data before returning
    sanitized = dlp_scanner.check(result)

    # 6. Full audit trail
    audit.record(req, sanitized)

    return sanitized

The agent knows it has a "database query" tool. It doesn't know the connection string, the password, or even what database engine is behind it. Credentials live in the proxy, pulled from a secrets manager like HashiCorp Vault or AWS Secrets Manager. If the agent is manipulated into runningDROP TABLE users, the proxy's query validator catches it before it reaches the database. The agent could never execute it directly. The same pattern works for every external service, APIs, file storage, and email. The agent gets a tool that talks to the proxy. The proxy holds the keys and enforces the rules.

Tool access is per-agent, not global. When you build a multi-agent system, it's tempting to give every agent access to every tool. Don't. Each agent gets access only to the tools it needs for its specific task. The research agent gets read access to the knowledge base. The execution agent gets write access to the output store. The critic gets read-only access to both. No agent gets tools it doesn't need. This isn't just security hygiene; it also reduces the surface area for unexpected behaviour. An agent can't misuse a tool it doesn't have.

Sensitive operations require human approval. Any destructive action, deletion, write to production systems, or external API calls that modify state go through a human-in-the-loop checkpoint. Not optional. Not "we'll add it later." From day one. I mentioned earlier that I saw an agent run a delete statement unprompted. That happened because the agent had the tool, the tool had the permission, and nobody built a gate between "agent decides to delete" and "deletion actually happens."


Your container is not a sandbox

This is the misconception I see most often: "We run our agents in Docker, so they're sandboxed."

No, they're not. Not for this purpose.

Docker provides process isolation. It was designed to package and deploy applications, not to contain adversarial or unpredictable behaviour. An agent, especially one that can generate and execute code, is doing things that Docker's isolation model wasn't built to prevent. I've seen agents reach outside their container. It's not common, but it's possible, and "not common" is not a security posture.

What you actually need is ephemeral, disposable execution environments with zero access to the host. Think microVMs, lightweight virtual machines that spin up for a single execution, have no access to the host filesystem, no network access beyond what's explicitly allowed, and get destroyed when the task is done. If the agent executes something destructive inside that environment, it destroys the disposable sandbox and nothing else. Your infrastructure doesn't care.

The key properties: ephemeral (created per execution, destroyed after), isolated (no host access, no shared filesystem, no ambient credentials), and disposable (if anything goes wrong, you lose nothing because there was nothing persistent to lose).

This matters most when agents have code execution tools. But even agents that only call APIs benefit from isolation; it limits the blast radius of any unexpected behaviour.


Don't let the same LLM plan and execute

Here's an architectural pattern I've adopted that fundamentally changes the security posture of an agentic system: separate the LLM that processes user input from the system that executes actions.

The reasoning is straightforward. An LLM that processes user input or external data is exposed to prompt injection, deliberate or accidental. A malicious user, a compromised data source, or even a poorly formatted document can influence what the LLM decides to do. If that same LLM has direct access to execute tools, a successful injection means direct execution of whatever the attacker wants.

The fix is an asymmetric architecture:

The planner LLM handles user input and reasoning. It processes the request, analyses the data, and outputs a structured plan: "I want to query this database table with these parameters" or "I want to call this API with this payload." It does not execute anything. It outputs intent.

A deterministic controller layer sits between the planner and execution. This is hard-coded middleware, not another LLM, not a flexible system, but rigid validation logic. It takes the planner's requested action, checks it against a strict schema of allowed operations, validates the parameters, and either approves or blocks the request. This layer doesn't interpret. It matches patterns. If the planner asks for something that isn't in the allowed schema, it's blocked. No exceptions, no "well, it seems reasonable."

A separate guardrail check evaluates intent. Before the approved request reaches execution, a small, heavily constrained model evaluates it specifically for malicious intent or out-of-scope logic. This is a focused check, not a general-purpose LLM; it's system-prompted to look for specific patterns: data exfiltration attempts, scope violations, anomalous access patterns.

The result: even if the planner LLM is successfully manipulated through prompt injection, the injected instruction hits a deterministic wall before it can do anything. The planner can be tricked. The controller can't, because it doesn't think. It validates.


Scope tokens to context, not to roles

Traditional access control works on roles. An agent has the "analyst" role, and the "analyst" role can read from the database. Simple.

Too simple for agents. Because an agent with the "analyst" role that processes requests for a thousand different users shouldn't have blanket read access to the entire database. It should have access scoped to the specific user, the specific request, and the specific records that are relevant to the task at hand.

Here's a concrete example. Say you're building an agentic system for a financial services company. A customer support agent handles queries from different clients. In a role-based model:

Agent role: "support_analyst"
Permissions: SELECT on client_accounts, client_transactions

That agent can read every client's account data and every transaction. Client A's agent-assisted query can technically access Client B's financial records. The only thing preventing that is the prompt saying "only look at the requesting client's data." A prompt. Protecting financial data. That should make you uncomfortable.

With context-scoped tokens, the flow changes fundamentally:

1. Client "Acme Corp" triggers a support query
2. Agent needs to read account data
3. Agent requests access from a Token Vending Machine (TVM):
   { user: "acme_corp", action: "read", resource: "client_accounts", client_id: "ACME-1042" }
4. TVM checks: Does this session belong to Acme Corp? Does Acme Corp own client_id ACME-1042?
5. TVM issues a short-lived token (TTL: 30 seconds) scoped ONLY to that client's records
6. Agent queries the database with that token
7. Database enforces the scope — even SELECT * returns only ACME-1042's data
8. Token expires. Agent can't reuse it for the next query.

The implementation relies on your database's row-level security. In PostgreSQL, this means RLS policies that filter rows based on the session's token claims. In cloud databases like Supabase, RLS is a first-class feature. The TVM is a lightweight service, essentially the same Tool Proxy pattern from above, extended with an identity check against your auth provider before issuing a scoped credential.

If the agent, through malfunction, injection, or unexpected behaviour, tries to read beyond that scope, the token itself prevents it. The security isn't in the prompt telling the agent to only read Acme Corp's data. The security is in the infrastructure, making it impossible to read anything else.

This is more work to implement than blanket role-based access. Significantly more work. But it's the difference between "we trust the agent to behave" and "it doesn't matter if the agent misbehaves, because the infrastructure constrains it."


Watch what leaves, not just what enters

Most security thinking focuses on input: what can the user send, what can the agent receive? Not enough thinking goes into output: what is the agent sending back, and to where?

An agent that's been compromised through prompt injection doesn't need to execute a destructive command to cause damage. It can exfiltrate data quietly, gradually, through its normal response channels. It reads sensitive records as part of its task, and instead of summarising them, it includes raw data in its output. Or it makes an API call to an external endpoint that wasn't part of the intended workflow.

Two controls that address this:

Output interception. Every tool response, every piece of data an agent retrieves using its scoped access, passes through an outbound check before it reaches the agent's response layer. This check looks for sensitive data patterns (PII, credentials, financial data) that shouldn't be leaving the system in raw form. Think of it as a DLP gateway specifically for agent outputs.

Volume anomaly detection. Agents have normal operating patterns. A research agent that usually pulls five to ten records per task doesn't suddenly need fifty thousand. If the requested volume exceeds the normal range for that agent and task type, the request gets blocked automatically and flagged for review. This catches both compromised agents and runaway loops that are accumulating data beyond what the task requires.


Audit everything. Trust nothing.

Every agent action needs to be logged with enough detail to reconstruct what happened after the fact:

What was the input to the agent? What the agent decided to do. What LLM calls were made, with what prompts and what responses? What tools were called, with what inputs and what outputs? What data was accessed, and what data was returned? What the critic evaluated, what the falsifier tested. What the final output was.

This is the audit trail. It serves two purposes: debugging when things go wrong (covered in the previous post in this series), and accountability when you need to answer the question "what did this agent do, and why?"

The honest caveat: audit trails are necessary but not sufficient. They give you reconstruction capability after the fact. They don't prevent the incident. The agent that ran a delete statement and then apologised- that action was logged. The log didn't stop the deletion. The human-in-the-loop checkpoint that should have been there would have.

Logging tells you what happened. Access controls, sandboxing, and human checkpoints prevent the things that shouldn't happen. You need both.


Where the industry is, and isn't

I'll be blunt: agent security is an afterthought right now. The industry is moving from the experimental phase to production, and most teams are bringing their prototype security posture with them. Which is to say, almost none.

The EU AI Act is a step in the right direction; it establishes that AI systems operating autonomously need governance, accountability, and risk management. But legislation sets the floor, not the ceiling. Most teams haven't even started thinking about how the Act applies to their agent architectures, let alone implementing the controls it implies.

What exists today: basic prompt guardrails, API key management, maybe some logging. What's missing: standardised frameworks for agent permissions, runtime isolation as a default (not an afterthought), context-aware access scoping, output monitoring, and governance tooling that understands multi-agent workflows.

The gap between "we have an agent in production" and "we have an agent in production with proper security and governance" is enormous. And it's going to get filled either by teams doing the work proactively or by the first major incident that forces the entire industry to pay attention.

I'd rather it be the first way.


The minimum security stack

If you take nothing else from this post, implement these five things before your agentic system touches production:

Least privilege per agent. Every agent gets only the tools and access it needs. No more. Review this regularly.

Ephemeral execution environments. Agents that execute code run in disposable, isolated environments with zero host access.

Deterministic validation between planning and execution. Never let the LLM that processes input directly execute actions. Put hard-coded validation in between.

Human-in-the-loop for destructive operations. Deletes, writes, external mutations, all gated. No exceptions.

Full audit trail. Every action, every tool call, every piece of data accessed. Tied to correlation IDs so you can reconstruct any run completely.

This isn't a complete security architecture. But it's the minimum viable set of controls that separates "a prototype we're testing" from "a system we can defend."


This is the fourth in a series on building and operating agentic systems in production. Previously: What "Agentic" Actually Means, Your Agent Broke at 2 AM. Now What? and Prompts Are Code. Treat Them Like It.

Found this useful? I do 1:1 sessions on AI architecture and strategy. → Book a session

// stay in the loop

If any of this was useful, there's more where that came from.

I write about agentic systems, LLM infrastructure, and what actually works in production — roughly once or twice a month. No noise, no sponsors.