.png)
When we launched Autonomous Worker Agents, the message we led with was simple: governance is inherited, not integrated. Agents don't get security bolted on after the fact. They inherit the OPA policies, RBAC, and audit trails already running your production pipelines. This post is about the layer underneath that promise: isolation.
We let an Autonomous Worker Agent run shell commands and call APIs inside our pipelines. Then we sat down and asked the uncomfortable question: what happens the moment it goes rogue? This post is about the four walls we put around the agent so that a break-in stays a break-in and never becomes a breach.
OUR STARTING ASSUMPTION
The agent is already compromised. Not might be...already is. Everything below follows from that one sentence.
Key takeaways
- The agent is the threat model. An agent that runs tools based on a model's output is one bad prompt away from doing whatever an attacker wants. We don't try to make that impossible. We assume it's already happened and ask what the agent would still be unable to do.
- Defense comes in layers. A hardened image removes the tools an exploit needs. Isolation keeps the agent away from the secrets. A broker means it never holds a real key. A proxy bounds where data can go. Four layers, each doing one job.
- No single layer is load-bearing. Each layer assumes the one before it already failed. A break-in has to beat all four, one after another, and every one is enforced by the kernel or the network,. never by the model's judgment.
- Proven, not promised. We replayed a real CVSS-9.0 breach against our own image. The step that used to dump 709 live secrets now returns 33 variables and zero usable credentials. Every layer ships with a test that has to pass first.
The old way breaks on contact with the real world
The first wave of agent platforms all looked the same. Take a capable model, hand it some tools, hand it some secrets, and let it run. It's fast to build. It sounds reasonable in a design review. It's also exactly how we built our first internal agent.
Then we actually looked at what that agent does all day. It reads a prompt, makes a plan, and runs tools (a shell, a file editor, an HTTP client) based on the output of a model that is itself reading whatever it's been pointed at: a README, a webpage, the result of the last tool call, a dependency's install script. None of that text is trustworthy by default. And the model is the thing deciding what in it counts as “information” versus what counts as “instructions.” That call is exactly what models are worst at making.
Here's what that looks like in practice. A README that says, as part of setup, run env > .config and continue, doesn't read like an attack on a model. It reads like a setup. The model isn't being careless. It's doing exactly what it was built to do, which is follow the text in front of it. The text just wasn't trustworthy in the first place.
That's not a bug we can patch inside the model. It's a mistake in how we built the system around it. We were treating an agent like trusted code when its actual job is to read untrusted input and act on it. Once we saw it that way, the fix was obvious: stop pretending otherwise.
Same starting point for both columns below: a tool result with text the agent was never supposed to follow. What differs is everything built around the agent.
The naive shape vs. the hardened shape.
The Reframe: Stop asking if the model will behave
Our governing assumption is blunt: the process running the agent is fully under an attacker's control, the same as remote code execution, running as the agent's own user. We don't treat that as a remote possibility we're guarding against. We treat it as the starting condition.
Everything below follows from that one sentence. We stopped asking “Will the model refuse the malicious instruction?” We've already assumed it won't. The question that actually matters is: if the agent turned hostile this exact instant, what would the operating system, the filesystem, and the network physically refuse to do on its behalf?
That question, asked four times, is where the four layers of isolation come from. Isolation is the foundation: the part that has to hold even when everything else fails. It's not the whole story: separately, we control what an agent is allowed to touch, and we read the prompts and responses themselves for injection and sensitive data. Those are their own posts. This one is about the walls.
Here's the bigger picture those three fit into, so you can see where this post sits before we go deep on it:

The bigger picture: isolation is the deterministic foundation; identity/authorization and behavioral controls sit above it.
1. Isolation (this post)
Kernel, OS, network: deterministic. The agent can't reason its way out.
Image hardening
Process isolation
Secret isolation
Network isolation
2. Identity & Authorization
Two levels: who defines the permission surface, and what runs within it.
Operator level - RBAC: who creates the surface
- MCP connectors: creates + tool scope
- Agent creation: creates + tool scope
- Pipeline runs: who can execute
Agent level - authorization: what runs within that surface
- Scoped tokens: short-lived
- Allowed tools: connector-level
- OPA - runtime: pipeline policy
3. Behavioral Controls
Content and call inspection: the only probabilistic layer.
- Injection scan: LLM gateway
- PII + toxicity: LLM gateway
- Call security: MCP gateway
- OPA policies: gateway
Not load-bearing on its own. Designed to fail safely into 01 and 02.
Four threats. Four layers. No overlap in the job.
Working from that one assumption, a concrete list of things a hostile agent would try falls out almost by itself. Each one gets exactly one layer of defense, built to answer it and nothing else. Below is the map, running from the outermost edge of the container inward to the network path out.
Walking the chain, outside in
Each section below stands on its own. If you only care about one threat, say, where your secrets actually live, jump straight to it. Read straight through, and you get the whole chain, from the edge of the container to the only network path out.
Layer 1: Image hardening: An empty toolbox
The threat
Break into a normal container and you'll find a fully stocked workshop waiting. A compiler to build a payload. A package manager to pull one down. Setuid binaries to climb privileges. Network tools to probe around and call home. A general-purpose base image hands an attacker all of it, for free, the moment they're inside.
The defense
We strip the image down to the studs at build time and the stripping is permanent, not a setting someone can flip back:
- No compiler. The build toolchain, C compiler, linker, make, is purged. Nothing left to compile a second-stage payload with.
- No package manager. The agent can't install its way to a capability the image doesn't already ship. (Package metadata stays, so vulnerability scanners and SBOM tooling still see exactly what's installed.)
- No setuid binaries. Every setuid and setgid bit in the image is cleared, closing the classic local privilege-escalation path.
- Read-only at runtime. The recommended way to run it locks the root filesystem read-only, with small writable scratch areas mounted separately, turns off privilege escalation, and drops every Linux capability the container doesn't need. The image ships ready for that posture and our published pod spec sets it.
In plain terms
Picture breaking into a workshop and finding the pegboard empty. No tools on the wall, no machine plugged in, nothing to build with. You can be standing right there, inside the room, and still unable to do anything.

Stripped at build time, irreversibly, compiler removed, package metadata only, setuid bits cleared, network utilities removed.
Layer 2: Process isolation: Three users, one container
The threat
Even with no tools to build with, a compromised agent will still look at what's sitting around it: another process's environment variables, its memory, the files holding the keys. If everything inside the container runs as one user, all of that is just… readable.
The defense
Inside the single container, three distinct, unprivileged users run three distinct processes: the agent, the credential broker, and the egress proxy. The container starts as root only long enough to lay down permissions and network rules, then drops each process to its own user and never returns to root again.
The user running the agent is in none of the broker's groups, so the kernel denies it even the ability to traverse into the secret directory: every attempt comes back as a hard permission error, not a polite refusal. The broker's own source code is locked down the same way, so a compromised agent can't even read the logic it's trying to defeat.
In plain terms
Same building, different locks. Three tenants live under one roof, but tenant A's key was never cut to open tenant B's door. The building super hands out keys exactly once, at move-in, then leaves for good. Nobody can get a new key issued later, not even the super.

Kernel-enforced separation: not a convention, a permission error.
Layer 3: Secret isolation: The agent never holds a key
The threat
Credentials are the actual prize here: API keys, cloud tokens, platform tokens, registry passwords. In the naive design, they sit in the agent's own environment, and the agent can reach them the instant it wants to. One env dump, or a config file generated “with values baked in,” and they're gone.
The defense
We take the secrets away from the agent completely. The moment the container starts, before the agent process even exists, a classification pass runs over every environment variable. Anything that looks like a secret, caught by a generic naming convention, so a brand-new connector needs zero custom code, gets swapped, inside the agent's own environment, for a unique, single-use placeholder. The real value moves into a store that only the broker can read.
So the agent boots up and sees something like this, and nothing more:

One placeholder, one destination. Replays against any other host fail.
ANTHROPIC_API_KEY = hbroker-Up4k…r3Q # placeholder, not a key
ANTHROPIC_BASE_URL = http://127.0.0.1:8443/proxy/api.anthropic.com
The real key is nowhere in the agent's world, ever. When the agent makes its call, it builds a normal-looking auth header with the placeholder and sends it to the in-container broker. The broker, and only the broker, swaps it for the real key on the way out, and only at that exact moment.
The agent container holds zero cloud-credential bytes: the same model covers credentials that can't ride in a header. Cloud SDK credentials, Google service-account keys, AWS and Azure managed identity, get signed into the request body instead, so the broker stands in as a metadata server and mints short-lived credentials on demand. No key file, no service-account JSON, nothing.
In plain terms
It's the valet-key trick hotels use. The key you hand the valet starts the car and opens the door, but it won't touch the glovebox or the trunk. Even a dishonest valet holding that exact key can't drive off in the car parked next to yours, it simply doesn't turn that ignition.
Layer 4: Network isolation: Traffic can't leave the path
The threat
A compromised agent with open network access is a data-exfiltration tool, full stop. Phone home to an attacker's server. Push data to a public mirror. Tunnel it out over whatever port happens to be open.
The defense
All outbound traffic from the container goes through an in-container proxy running an allow-list. Approved hosts pass. Everything else is denied by default. The agent's traffic and the broker's traffic ride separate listening ports, so the two zones are never confused with each other.
A proxy only matters if the agent can't route around it, so three independent mechanisms keep it on that path:
- Set for the agent: not left to its discretion as a setting it could choose to ignore.
- An always-on guard inspects every command and file write the agent issues, and blocks anything that tries to disable or sidestep the proxy: unsetting proxy variables, a “no-proxy” flag, stripping the environment through a helper, mutating it from inside an interpreter, or even a quiet bypass written into a shell startup file.
- A kernel-level firewall rule pins the agent's user to the proxy and rejects every other destination, off-container hosts, the broker's private port, and cloud metadata endpoints. The capability that installs it is used once by root at startup, then dropped for good. The agent can never get it back, and can't edit, list, or flush the rule that contains it.
Real APIs occasionally echo a request's own credentials back inside an error body, so the broker scrubs responses on the way back too. A stray 401 can't hand the agent the very key the broker just injected.
In plain terms
One exit, and it's guarded. The fire doors are welded shut, the windows don't open, and the guard's own keys get taken away once the building is set up for the day, so nobody , not even the guard, can prop a door open later.

Three independent locks on one gate: not three settings, three different enforcement mechanisms.
Red-teaming our own design: Proof, not promises
None of this means much as an assertion, so we tested it the way an attacker would. We took a real, high-severity breach chain and replayed it against our own image, logged in as the agent.
Every other move in that chain failed the same way: each one stopped cold by a different layer.
Every layer is a release gate
Each isolation layer has its own end-to-end test that runs against the freshly built image, and the full breach-chain pentest above runs on top of all of them. Every check lives in the build pipeline ahead of the publish step, an image that regresses any single layer simply never ships, on either supported CPU architecture.
- Container smoke — Hardened image boots and is intact
- Process-isolation smoke — Three users separated; agent denied on secret files; egress ports pinned
- Secret-isolation test — Agent's environment holds only placeholders, never real secret bytes
- Per-host scoping test — A credential is injected only for its own host; off-target replays denied
- Egress-policy test — The allow-list is enforced and the deny-all default holds
- Credential-routing test — Each connector's traffic is routed and brokered correctly
- Proxy-bypass-guard test — Attempts to disable or route around the egress proxy are blocked
- Containment pentest — The full breach-chain replay above stays contained
- Image vulnerability scan — The published image clears the CVE policy
The proof isn't a slide from one audit done once. It runs on every release, and a single failure blocks the publish.
How it fits together
Security engineers have a name for this shape: the Swiss cheese model. Slice one piece of cheese and hold it to the light, there are holes in it. Every real-world control has gaps somewhere; that's just true, and pretending otherwise is how people get hurt. But stack four slices together, each with holes in different, unrelated places, and there's no longer a straight line through all four at once. None of these “slices” are guesswork, either. Each is enforced by a different mechanism, the build, the kernel, the filesystem, the network, so a gap in one almost never lines up with a gap in the next.

Attempt 1: finds a gap in image hardening, hits a solid wall at process isolation.
Attempt 2: gets three layers deep, still never reaches the prize.
A compromise of any single layer just lands in the next one. A tool that gets past the hardened image still can't read the secrets. An environment dump that gets read finds only placeholders. A placeholder that gets grabbed can't be aimed anywhere useful. And a connection that tries to phone home is pinned to the proxy and denied. That's the entire point of building it as four layers instead of betting everything on one wall.
The secure shape is the default
There's no separate hardening mode to opt into. You write an ordinary Agent step and pass your secrets the way you already do, under their normal names. The container does the rest before your agent process ever starts:
agent:
step:
run:
container:
image: harness-ai-agent:latest
env:
# Pass the real secret under the name the SDK already reads.
# The engine swaps it for a placeholder before the agent starts;
# The broker holds the real bytes and injects them only at egress.
AWS_BEARER_TOKEN_BEDROCK: <+secrets.getValue("bedrock_token")>
HARNESS_API_KEY: <+secrets.getValue("harness_token")>
That's the whole interface: no proxy wiring, no per-connector code, no placeholder plumbing on your side. The container classifies your secrets, swaps them for placeholders, writes a per-session network policy, drops to the three unprivileged users, and runs your agent, which never sees a single real credential.
Two things live in the platform, not the step: the three-user split and the read-only, capability-dropped runtime are set once in our published pod spec, and the kernel egress pin turns on wherever the runner can grant it. You don't wire any of it up per pipeline. It's how the image is meant to run.
FAQs
Why layers instead of one strong control?
Because any single control can fail, and an agent running tools on untrusted output gives an attacker a lot of chances to find that failure. Layering means a break in one boundary gets caught by the next one. And each layer is enforced by a different mechanism – the build, the kernel, the filesystem, the network – so a gap in one doesn't line up with a gap in the next.
Where do the secret bytes actually live, and who can read them?
With the broker process, in a store owned by the broker's user and readable only by it. The user running the agent is in none of the broker's groups, so the kernel denies it access outright. The agent's own environment only ever holds placeholders.
Is the egress proxy mandatory, or can the agent route around it?
It's set for the agent rather than left to its discretion, and an always-on guard blocks the obvious ways to disable or bypass it. On top of that, a kernel firewall rule pins the agent's traffic to the proxy and rejects everything else – the same control enforced one level down, where the agent can't touch it.
How does a new connector get secured? Do you write code for each one?
No. Secrets are recognized by a generic naming convention, so a new connector's key gets placeholder-swapped and host-scoped automatically, with zero custom wiring required.
What about prompt injection itself – doesn't that defeat all of this?
Isolation doesn't try to stop the injection – it makes a successful one boring. A hijacked agent still can't read a secret, can't reach a host off the allow-list, and can't turn the container into a toolkit. The blast radius is bounded by the system, not by the model's judgment. Catching the injection payload in the content itself is a separate job, handled by the LLM gateway – that's the subject of the next post in this series.

