Chapters
Try It For Free
July 13, 2026

How We Secured AI Worker Agents in Harness
| Harness Blog

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: model + tools + secrets, no further questions The hardened shape: four independent layers, stacked on purpose
  • Real API keys sit in plain text in the agent's own environment
  • A full shell, compiler, and package manager, ready to use
  • Open network access – any host, any port, no questions asked
  • One user. Anything the agent can see, an attacker can see too
  1. The image ships stripped of every tool an exploit would need
  2. Three locked-down users split the work inside one container
  3. Secrets live with a broker the agent has no path to read
  4. All network traffic is forced through one allow-listed proxy
⚠ One bad prompt → the keys, the data, and a way out, all at once ✓ Same bad prompt → contained, at every single step

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:

Three categories: Isolation, Identity & Authorization, Behavioral Controls

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.

# If it could… Answered by
01 Use in-container tooling to build or stage an exploit Image hardening
02 Read another process's secrets, memory, or files Process isolation
03 Harvest credentials from its own environment Secret isolation
04 Phone home or exfiltrate data over the network Network isolation

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.

The generic base image's toolbox stripped down to the hardened Harness image

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.

One hardened container holding three separate unprivileged users: the agent, the broker, and the egress proxy

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:

The agent sends a request with a placeholder credential to the broker; the broker injects the real key only for the matching host

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.

The agent's traffic is forced through an egress proxy with an allow-list and deny-all default

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.

709
environment variables dumped by the original CVSS 9.0 exploit, full of live database passwords and service tokens
33
variables returned when we replayed the exact same step against our hardened image, and none of them were usable credentials

Every other move in that chain failed the same way: each one stopped cold by a different layer.

# Attack step Result
01 Read the broker's secret files directly, bypassing the placeholder swap entirely Denied · permissions
02 Replay a captured placeholder against the attacker's own host Denied · wrong host
03 Reach the attacker's callback host directly, off the proxy – eight different ways 0 of 8 got out
04 Pivot to the cloud metadata endpoint to mint fresh credentials (the classic SSRF) Denied · off allow-list
05 Reach any host that isn't explicitly approved, hoping the default is open Denied · default is deny

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.

Four isolation layers shown as slices of Swiss cheese; two attack attempts are blocked before reaching the prize

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.

 

Shivansh Srivastava

Shivansh Srivastava is a Software Engineer on the AI Platform team at Harness, working on the security architecture behind Autonomous Worker Agents.

Shubham Jindal

Shubham Jindal is a Director of AI Software Engineering at Harness, where he leads the development of intelligent systems that enhance developer productivity and software delivery. With a strong foundation in distributed systems and web performance.

Similar Blogs

Harness AI
Harness Platform