Back to Notes
ArticleTechnologyAug 10, 2026

Take the Keys Away From Your Agent

Event
The Innovation Lab
Overview

A process that runs model-authored code should hold no credential worth stealing and have no route to anywhere it could send one. Builds that from the bottom up: what TCP, HTTP and TLS actually are, how certificate authorities decide who you believe, and what a proxy can and cannot see. Ends with an interactive walkthrough of one request through every process that touches it, which you can rerun on a flat network to watch the whole thing become irrelevant.

Technologies
SecurityNetworkingTLSCertificate AuthoritiesDockerAgents
Take the Keys Away From Your Agent preview
Introduction

Take the Keys Away From Your Agent

An agent that writes and runs code needs two things: a credential for whatever API it calls, and a network. The usual way to give it both is one container, the key in an environment variable, an ordinary bridged network.

That process takes its instructions partly from things it reads. A web page, a file, the output of a tool. So untrusted input, a live credential, and a route to every host on the internet all sit in one place.

You cannot fix that at the model layer, because it means asking the component whose behaviour is in question to enforce its own limits. The controls that hold are the ones it has no way to address.

This builds them from the bottom up: what TCP, HTTP and TLS actually are, how certificates decide who you believe, and what a proxy can and cannot see. The last section is all of it working on a single request, which you can step through a hop at a time.

Layer one

What is actually on the wire

Three protocols do the work, and they stack. TCP moves an ordered stream of bytes between two ports and has no idea what they mean. HTTP gives those bytes a shape: a request line, headers, a blank line, a body. TLS sits between the two, proving the server’s identity and encrypting everything after.

Click each band below to inspect it, then switch TLS off.

encapsulation cutawayhttps · one request, three layers
the HTTP payload is sealed inside TLS — unreadable in transit

TCPthe pipe

A reliable, ordered stream of bytes between a port here and a port there. That is the whole job. It guarantees delivery and order, or a visible failure — and it has no opinion whatsoever about what the bytes mean.

Ciphertext. The observer still learns the addresses, the ports, the size of every packet and its timing. Encryption hides contents. It never hides that you spoke.

tap on the wire

IP 10.0.4.7.51314 > 203.0.113.9.443: Flags [P.], length 307
  0x0000:  1703 0301 2e9a 4fe2 710c bb38 d56a 9104
  0x0010:  c722 ef1b 883d 5ea0 76f4 12cd 398b e657
  0x0020:  2a90 dd14 6fb3 41c8 057e 92ab 3fdf 5085
  0x0030:  57ba 0a77 fa1c 0927 1dd0 8a6e 7764 e1af
  0x0040:  f47d b8fc 4229 0b50 2e97 c678 9e84 1494
  0x0050:  fe89 adf5 6cc6 b190 8265 aa4b 376f 8f24
  0x0060:  e15d f40f ca6d f1a9 2b45 6ec0 9267 a84a
  0x0070:  9358 47c5 9c26 5d20 e8bf 92cd 3c93 411d
  0x0080:  ba7a f224 ecaf b2d6 a460 eb83 2196 6367
  0x0090:  5016 86eb 9e1c dafb 784e 485d f251 22c1
  0x00a0:  e797 5be1 c715 8109 9b7d 1b3f 8d8b 36cc
  0x00b0:  ac97 4eaf 6f4c ec9f 5ad7 2970 ffbd 15a2
  0x00c0:  4aae 80bd 5c8b 55ff 1546 0e70 8da9 2fad
  0x00d0:  2856 1f12 7be4 3cb5 4d04 30bf 7f01 6e5e
  0x00e0:  4023 a876 1c0b a081 2687 8f35 23e9 57ae
  0x00f0:  b427 b76e 0e26 c5d0 65bf c891 4629 bae7
  0x0100:  32fe dcc0 3a50 ecca 8b60 d6ef e626 4b4e
  0x0110:  937d 79a7 ae7a 72ed b08a c8ca 2b02 debb
  0x0120:  1f76 8d6e 4ca1 a790 ff93 9cd0 ace5 74ab
  0x0130:  607d 91

The HTTP panel is identical in both modes. HTTP does not change when you wrap it. Only who can read it in transit changes.

That is what the rest of this depends on. TLS makes a request private from the network, not from the endpoints. Whoever completes the handshake reads the plaintext, credential included, and can rewrite it before passing it on.

Layer two

Who decides what you believe

TLS proves the server’s identity with a certificate: a name and a public key, signed by somebody else. That signature is checked against a certificate authority, whose own certificate is checked against another, until the chain reaches a self-signed root. Then one question is left. Is this root in my trust store?

Run the walk. Then run it again with a root your own deployment installed.

chain of custodyidle

Every https:// request you have ever made without thinking.

◇ trust store — awaiting a root it recognises
CN=Example Root CA X1root
issued by
itself
valid for
20 years
comes from
Shipped in your operating system's trust store. You did not choose it — your OS vendor did, years ago, on your behalf.
CN=Example Trust CA G3intermediate
issued by
Example Root CA X1
valid for
5 years
comes from
Sent alongside the leaf, to bridge the gap to a root.
CN=provider.exampleleaf
issued by
Example Trust CA G3
valid for
90 days
comes from
Presented by whoever answered on that address, during the handshake.

Both chains verify, and the client cannot tell them apart. The checks are identical and both pass honestly. What differs happened before any of the cryptography: somebody decided which roots this machine believes.

So the boundary is not in the maths. It is in who can read the root’s private key. Publish the certificate where the agent can see it, and keep the key on a volume the agent cannot mount. When two containers run as the same user, file permissions are a note rather than a wall.

Layer three

Standing in the middle, two ways

A proxy is any process that makes a request on someone else’s behalf. The distinction that matters is not forward versus reverse. It is whether the proxy terminates the TLS.

A CONNECT tunnel opens a raw pipe and then relays bytes it cannot read. A terminating proxy answers the handshake itself and sees everything. Send one request through both.

proxy comparatortwo lanes, same request
CONNECT tunnelan opaque pipe
agent
relay in
relay out
destination

One connection, straight through. The same amber session runs from the agent all the way to the destination — the relay is just pipe.

Terminate and re-originatetwo sessions, spliced
agent
TLS ends here
new TLS out
destination

Two connections. The agent's amber session ends at the proxy; a separate teal one continues outward, carrying a credential the agent never held.

Most security writing treats a proxy reading your plaintext as the attack. That assumes the client is you and the middlebox belongs to somebody else.

Invert it. Here the client runs code a model wrote, and the middlebox is yours. Now the tunnel is the dangerous option, because a blind relay can only carry an authenticated request if the client already holds the credential.

The two ideas

What the layers are for

Those three layers are the material. Two ideas turn them into a boundary.

The agent has no route out

Not a firewall rule, which can be misordered or shadowed by an earlier entry. An absent route. Its network has no gateway, and no host address on the bridge either, so the kernel has nowhere to send the packet and the call fails before DNS resolves. That holds for TCP, UDP and IPv6 without enumerating them.

Closing both paths takes two separate settings. Marking the network internal stops traffic being routed through the host to somewhere else. It does nothing about reaching the host itself, which is a different path, and anything you have bound to all interfaces is still sitting there. Removing the host’s address from the bridge is the second setting, and skipping it leaves a deployment that looks locked down and is not.

Authority and capability live in different processes

One process decides whether a request may go at all. It owns the certificate authority and the approval list, and it holds no credential and has no way to reach the internet. A second process holds the credential and the only route out, and decides nothing: it acts on requests the first one already approved, and cannot originate one.

Split that way, a bug in the approval logic still sends nothing, and a compromise of the sending process cannot invent a request to send. Neither half is worth much alone, which is the property the last section is built to show.

All of it at once

One turn, end to end

One request through every process that touches it. Run it, or step a hop at a time and watch the Authorization header in the panel on the right.

one turn, end to endisolated · step 1 of 13

Run a model turn. The job the whole deployment exists to do.

POST provider.example/v1/messages — The agent builds a requestthe container · no credentials, no route outinterception · decidesegress · holdsone containeragentmodel-driven · untrustedholds: nothingsidecarthe only way outholds: nothingproxymitm · terminates + approvesholds: the CA root keyrunnersends · decides nothingholds: vault access + egressvaultsqlite · read by the runner aloneholds: the real secretsprovider.exampleoutside your boundaryholds: nothing of yours

step 1 · agent

The agent builds a request

It asks for a credential and receives the literal string __apiToken__. There is no real one anywhere in this container to find, so there is nothing for a compromised agent to steal here.

the request, right now

POST /v1/messages HTTP/1.1
Host: provider.example
Authorization: Bearer __apiToken__

readable right now by: the agent

Track where the real credential exists. It appears in the runner at the substitution and in the outbound session after it. The agent’s copy of that header reads __apiToken__ from the first step to the last. Those two never overlap.

Then switch to flat network. Everything past the container disappears, the request goes straight to the destination, and the credential is back in the agent’s environment where anything it runs can read it. All four scenarios succeed, including the two that should not. That configuration is the default, and it is what you have until you decide otherwise.

Limits

What this does not do

Not a defence against your own operators

Anyone who can shell into the runner reads the vault, and anyone who can shell into the proxy mints certificates. This sits between the agent and the secrets, not between an administrator and them.

The runner is still the biggest single prize

Vault access and egress do sit together there. Its defence is that it is small enough to read in one sitting, runs nothing the untrusted side influences, and cannot originate a request. Add a plugin system to it and the boundary is gone.

It does not stop misuse of legitimate access

The agent cannot steal the key, but it can spend it, on as many perfectly approved requests as it likes. Rate limits and budget ceilings are a separate control that this makes easier to enforce rather than unnecessary.

None of these primitives are new. TLS termination, private certificate authorities, credential-injecting proxies and isolated network segments have been ordinary practice anywhere with an auditor for decades.

What is new is the reason to reach for them. We have started running processes whose instructions are written partly by whatever they happened to read, and then handing those processes a credential and a network.

Questions?

Got a question about something I wrote? Send me a note.