Skip to content

Isolate an agent's network access

Run an agent in a sandbox that does not hold credentials and reaches only the destinations you list. Keycard issues a credential for each allowed request, as the user the sandbox belongs to.

An agent that runs unattended on your infrastructure still needs to act as someone, but it should not reach anything you did not intend. Put a proxy between the agent and the network: the agent does not hold credentials, and the proxy decides where each request may go and adds a credential to each request it allows.

A background agent runs in a remote workspace, and it needs to push a branch and open a pull request as the engineer who asked for the change. The usual way to do that is to put a token in the workspace.

Everything in the workspace can then read that token: the agent, every package it installs, and any instruction it picks up from a web page or an issue comment. With open network access, any of them can send the token anywhere, and the token works wherever it is copied until it expires or someone revokes it.

A provider refresh token is worse, because it renews itself and a copy keeps working long after the task ends.

Open network access is a problem even when the workspace does not hold a token, because an agent that can reach any host can send source code, customer data, or the contents of its environment to it.

flowchart LR
    T[GitHub token in env] --> A[Agent in workspace]
    A --> G[api.github.com]
    A -.->|"<span style='color:#dc2626'>fak:fa-danger</span> copied by an install script"| X[Attacker host]
    A -.->|"<span style='color:#dc2626'>fak:fa-danger</span> cloud metadata"| M[169.254.169.254]
    linkStyle 2,3 stroke:#dc2626,stroke-width:2px
    style X stroke:#dc2626,stroke-width:2px
    style M stroke:#dc2626,stroke-width:2px
  • Agents run on infrastructure you control, such as a Kubernetes cluster
  • An agent performs background tasks on behalf of a user, non-interactively
  • The workspace runs code you do not fully trust: dependencies, generated commands, content from the internet
  • You need to limit outbound traffic to a list of destinations and keep a record of each connection

Suppose an engineer asks for a change in a chat message. A workspace starts on your cluster, where an agent edits the code, runs the tests, pushes a branch, and opens a pull request while the engineer works on something else. The pull request should still be attributed to the engineer, and the workspace should not hold a secret. You also want a record of every connection the agent made and of every credential Keycard issued for it.

  • The agent runs on a developer’s own machine. Start with Govern a coding agent.
  • The agent reaches its tools through MCP servers. Put a Gateway in the path and decide per tool, as Allow one tool, deny the next describes. The two combine, since an MCP server URL can be an endpoint the proxy allows.

The sandbox is a Kubernetes pod where the agent runs as an unprivileged user, without a Keycard secret, a provider token, or a ServiceAccount token of its own.

The egress proxy runs in the same pod as a sidecar. An init container installs firewall rules that send all of the agent’s outbound traffic through the proxy and drop everything else. The proxy allows a request only when its host, port, and path match an endpoint in its configuration. When that endpoint requires a credential, the proxy asks Keycard for one and writes it into the request.

Keycard decides whether to issue each credential. The proxy authenticates with the pod’s Kubernetes ServiceAccount token, which the cluster signs, and each user has their own ServiceAccount. Keycard policy permits that ServiceAccount to act for that one user, and only on the Resources the proxy’s Application depends on.

flowchart LR
    subgraph Pod[Sandbox pod]
        A[Agent] -->|"every outbound connection"| P[Egress proxy]
    end
    P -->|"ServiceAccount token + user"| K[Keycard]
    K -->|"credential for that user"| P
    P -->|"request with credential"| G[api.github.com]
    P -.-x|"not listed: refused"| X[Any other host]
    linkStyle 4 stroke:#dc2626,stroke-width:2px
    style X stroke:#dc2626,stroke-width:2px,stroke-dasharray:4 3
  1. The agent sends a request to api.github.com with a placeholder token or none.
  2. The firewall rules send the connection to the proxy.
  3. The proxy finds api.github.com in its endpoint list; it refuses any host, port, or path the list does not name, and records why.
  4. The proxy asks Keycard for a GitHub credential with an impersonation token exchange, authenticating with the pod’s ServiceAccount token and naming the user the sandbox belongs to.
  5. Keycard checks the token’s signature, issuer, and subject, then evaluates policy. If policy permits it, Keycard returns the user’s GitHub token from the grant it holds for that user.
  6. The proxy writes the token over the placeholder and sends the request, and the token does not reach the agent, the proxy’s logs, or its audit records.

The proxy also answers the agent’s DNS lookups. A name that is not an endpoint host gets NXDOMAIN. This prevents agents from creating covert channels through DNS.

The workspace does not hold a credential: the agent’s container mounts only the proxy’s CA certificate, the proxy container alone mounts the ServiceAccount token, and user grants stay in Keycard.

One sandbox acts for one user. Each user’s ServiceAccount is named after their Keycard User ID, and Keycard policy permits that ServiceAccount’s token from your cluster’s issuer to act for that user only. The User ID is in the proxy’s configuration, which the agent cannot read or change; if a sandbox is configured with another user’s ID, Keycard refuses the exchange with access_denied.

The agent works while the user is away. Once the user authorizes the brokered Resources, Keycard renews the provider grant and the proxy gets a new credential for each request, so the user does not need a browser or a laptop and the sandbox does not store a token.

Network access is a list you wrote. The proxy allows a request only when an endpoint covers its host, port, and path prefix, and it refuses everything else: UDP (including QUIC), cleartext HTTP, the cloud metadata address, cluster addresses, and any name not on the list.

Revocation takes effect on the next request. The proxy does not cache credentials; it runs a token exchange for every request that needs one, so a policy change or a revoked grant stops the next request.

Every decision has a record. The proxy writes one JSON line for each connection, request, and DNS lookup it allows or denies, with the sandbox ID and a digest of the configuration that made the decision. Keycard records each credential it issues, along with the proxy’s Application and the impersonated user.

You run Kubernetes, and the sandbox namespace runs at the privileged Pod Security level. The init container that installs the firewall rules needs NET_ADMIN and NET_RAW; it drops NET_ADMIN before the agent starts, and the proxy refuses to start if it did not.

Treat anyone who can create a sandbox as a node administrator. A sandbox can run any pod spec under any user’s ServiceAccount, so creating one is the same as acting as any enrolled user.

The agent must trust the proxy’s CA, because the proxy terminates TLS to read each request. A client that pins certificates or ships its own trust store fails until you configure it. The reference base image adds the CA to the system trust store before the agent starts, so curl, git, Python requests, and Node.js fetch trust it.

The agent can still send data to an allowed endpoint, because the proxy decides on host, port, and path rather than content. If the agent can push to GitHub, it can push anything the user’s GitHub token permits, so narrow the endpoint paths, the provider scopes, and the Resources each user may reach.

You maintain the endpoint list. A destination missing from the list fails at name lookup, so the agent sees “could not resolve host” rather than an HTTP 403, and the proxy’s audit record names the refused name.

Each credentialed request is one token exchange, so a busy agent sends many exchanges to Keycard.

No. The proxy writes the credential into the request after the agent sends it, over any value the agent sent at that position, and never logs it. The agent’s container does not mount the ServiceAccount token or the proxy’s configuration.

Not through Keycard. The User ID is in the proxy’s configuration, which only the proxy mounts, and if the configuration names another user, Keycard compares the ServiceAccount in the token with the user and refuses. The pod cannot forge a token for another ServiceAccount, because the cluster signs it.

What if the agent follows a malicious instruction?

Section titled “What if the agent follows a malicious instruction?”

It can reach only the endpoints you listed, with the access the user’s credential grants there. The proxy refuses the metadata service, other pods, and any other host, and it records every attempt, including the refused ones.

Remove the user’s Policies from the active Policy Set; in the reference deployment, removing the user from the values file and running its setup tool again does this for you. Keycard refuses the next exchange, and the proxy denies the request with exchange_denied. A credential the proxy already sent upstream stays valid at the provider until it expires, but the agent never held a copy of it.

How do we tie a pull request to the request that caused it?

Section titled “How do we tie a pull request to the request that caused it?”

Join the records from GitHub, Keycard, and the proxy with your own. GitHub attributes the pull request to the user, Keycard’s credentials:issue event names the Application, the user, and the ServiceAccount, and the proxy’s record names the sandbox, the method, the host, and the path. Keycard does not know your task IDs, so your orchestrator must record which task ran in which sandbox.

The reference deployment runs on Kubernetes, but the proxy works in any sandbox where you can make it the only route out of the workload: a container, a virtual machine, or another sandbox runtime whose network you control.

The reference deployment uses firewall rules to send traffic to the proxy, so the agent does not need proxy settings. The proxy also accepts CONNECT requests, which lets a client that honors the HTTPS_PROXY environment variable use it without firewall rules in the sandbox. The client chooses whether to honor that variable, though, so the sandbox must still block every other route out.

On Kubernetes, the proxy authenticates with the token of the ServiceAccount that acts for the user. Outside Kubernetes, use another workload identity mechanism, such as the platforms in Authenticate deployed workloads.

  • A CI job that runs third-party code and must publish an artifact as the person who triggered it
  • A data analysis agent that may read one warehouse and must not reach any other host
  • An evaluation harness that runs model-generated code
  • A support agent that must reach a ticketing API as the customer’s account owner