Skip to content

Design proxy-based egress controls for agent runner containers #517

Description

@eloylp

Problem

Once agent runs execute in fresh runner containers, we have a cleaner place to enforce network boundaries. Today a compromised prompt, dependency install, or tool invocation can generally reach whatever the runner container network can reach.

We want operators to be able to say things like:

  • deny all internet access for a given agent,
  • allow only GitHub and Anthropic for a reviewer agent,
  • allow GitHub plus OpenAI for a Codex-backed agent,
  • keep a permissive global default while locking down sensitive agents,
  • log denied egress attempts for security review.

Prompt guardrails are useful, but this must be runtime-enforced. An agent prompt can be manipulated; the network policy should not depend on the model cooperating.

Product Direction

Implement egress policy for runner containers using a controlled egress proxy path, not prompt-only rules and not host-wide firewall mutation.

Policy should exist at two levels:

  1. Global default policy, applied to every run unless overridden.
  2. Per-agent policy, used to narrow or override the global default for that specific agent.

A per-workspace policy may be useful later, but it is not required for the first version unless implementation naturally needs it. Keep v1 focused.

Why Proxy-Based

Hostname allowlists are the useful product shape:

allow github.com, api.github.com, api.anthropic.com
block everything else

iptables is good for IP/CIDR/port enforcement, but it is brittle for hostnames because GitHub, Anthropic, OpenAI, package registries, and CDNs use changing IPs. /etc/hosts is not a security boundary: it does not block direct IP access, does not handle wildcard domains well, and can break TLS/CDN routing.

A proxy can enforce HTTP/HTTPS target hostnames directly through normal proxy semantics, especially HTTP CONNECT for HTTPS. It also lets us log denied hostnames in a user-understandable way.

Target Runtime Shape

For a restricted run:

daemon
  -> create per-run Docker network
  -> start egress proxy container with policy for this run
  -> start runner container on that network
  -> inject HTTP_PROXY / HTTPS_PROXY / NO_PROXY into runner
  -> block or avoid direct internet access from runner
  -> runner tools reach allowed services through proxy only
  -> daemon removes runner, proxy, and network after run

The runner container must stay unprivileged. Do not give the runner CAP_NET_ADMIN or privileged mode so that agent-controlled code cannot remove or weaken its own network controls.

If low-level firewall rules are needed, they should be owned by daemon-controlled infrastructure or a trusted helper/proxy, not by the runner process itself.

Policy Model

Initial policy shape should be simple and explicit:

egress:
  mode: allow_all | deny_all | allowlist
  allow_hosts:
    - github.com
    - api.github.com
    - api.anthropic.com
  allow_ports:
    - 443

Suggested semantics:

  • allow_all: current behavior, no egress proxy restriction required.
  • deny_all: runner has no internet egress.
  • allowlist: runner may only connect to listed hostnames/ports through the proxy.

Questions to settle during implementation:

  • Whether host matching supports exact hostnames only in v1, or wildcard suffixes like *.github.com.
  • Whether allow_ports should default to 443 only.
  • Whether HTTP on port 80 should be disabled by default.
  • How to represent built-in presets like github, anthropic, openai without hiding the actual hostnames from users.

Global and Per-Agent Behavior

Global policy should be the default:

runtime:
  egress:
    mode: allowlist
    allow_hosts:
      - github.com
      - api.github.com
      - api.githubcopilot.com
      - api.anthropic.com
      - api.openai.com
    allow_ports: [443]

Agent-level policy can override or narrow it:

agents:
  - name: pr-reviewer
    egress:
      mode: allowlist
      allow_hosts:
        - github.com
        - api.github.com
        - api.githubcopilot.com
        - api.anthropic.com
      allow_ports: [443]

Implementation must define merge semantics clearly. Recommended v1:

  • If agent policy is absent, use global policy.
  • If agent policy is present, use agent policy as the complete effective policy.
  • Do not implement complex merging in v1.

Required Product Surfaces

Egress policy must be configurable through the same surfaces as other runtime/fleet settings:

  • UI:
    • Config -> Runtime for global default egress policy.
    • Agent editor for per-agent egress policy.
    • Clear status badges for allow_all, deny_all, and allowlist.
  • REST:
    • global runtime egress fields in /runtime.
    • per-agent egress fields in agent CRUD.
  • MCP:
    • expose global egress policy through runtime tools.
    • expose per-agent egress policy through agent create/update tools.
  • Import/export:
    • include global and per-agent egress policy.
  • Docs:
    • quickstart/security/configuration/runtime docs must explain the policy model and limits.

Enforcement Requirements

The policy must be enforced by runtime, not just displayed.

For deny_all:

  • Runner should have no direct external network access.
  • It may still need loopback/local access for the CLI process itself.
  • If no network is required, Docker network_mode: none may be enough.

For allowlist:

  • Runner traffic should be routed through the egress proxy.
  • Proxy allows only configured hostname/port pairs.
  • Proxy denies everything else and records a structured denial event/log.
  • Runner should not have a direct route to the internet that bypasses the proxy.

The hardest part is bypass prevention. Setting HTTP_PROXY / HTTPS_PROXY is not enough if direct egress still works, because tools or malicious code can ignore proxy env vars. The design must either isolate direct egress or explicitly document the limitation if a first implementation cannot fully block bypasses.

Observability

Denied egress attempts should be visible enough to debug and audit:

  • daemon logs should include denied hostname/port, agent, workspace, repo, run/span if available,
  • trace/runners UI should eventually surface egress denial as a run diagnostic,
  • /backends/status or runtime diagnostics should verify proxy availability when policy requires it.

Do not log secret headers, bearer tokens, request bodies, or full URLs with sensitive query strings.

Security Constraints

  • Runner containers must remain unprivileged.
  • Do not grant agent-controlled containers CAP_NET_ADMIN.
  • Do not mutate host-wide iptables rules from the runner.
  • If a helper requires Docker/socket privileges, keep that helper daemon-controlled and document the boundary.
  • Avoid relying on /etc/hosts as a security mechanism.
  • Proxy logs must redact credentials.
  • Denied egress must fail closed.

Implementation Notes

A reasonable implementation path:

  1. Add EgressPolicy domain structs.
  2. Persist global policy as part of runtime settings.
  3. Persist optional per-agent policy.
  4. Extend REST/MCP/import/export/UI surfaces.
  5. Add an internal egress runtime abstraction so workflow code does not know proxy internals.
  6. Add or build an egress proxy image/binary.
  7. At run time, compute effective policy for the agent.
  8. For allow_all, keep current bridge/network behavior.
  9. For deny_all, start runner with no external network.
  10. For allowlist, create per-run network, start proxy with policy, start runner with proxy env, prevent direct egress, clean everything up.
  11. Add fake-backed tests for policy resolution and runtime orchestration.
  12. Add at least one real Docker smoke test path behind an explicit env var/build tag if CI cannot run Docker.

The proxy can be a small Go HTTP proxy owned by this repo, or a carefully configured existing proxy if that keeps the implementation smaller. If we ship a proxy image, publish it alongside agents and agents-runner with matching tags.

Open Questions

  • Should the proxy be a separate agents-egress-proxy image or should the proxy binary be embedded in the daemon image and launched as a tiny container from the daemon image?
  • Exact hostname matching only, or wildcard suffix matching in v1?
  • Should package registry access be a built-in preset or always explicit?
  • How should local-model deployments express local network access, e.g. host.docker.internal, LAN CIDRs, or a local inference endpoint?
  • Do we need per-workspace policy before per-agent policy, or can global + agent cover v1?
  • How do we prevent bypasses robustly without overcomplicating Docker networking?

Acceptance Criteria

  • Global egress policy is persisted and configurable through UI, REST, MCP, and import/export.
  • Per-agent egress policy is persisted and configurable through UI, REST, MCP, and import/export.
  • Effective policy is computed deterministically for every run.
  • allow_all preserves current behavior.
  • deny_all blocks internet egress for runner containers.
  • allowlist permits only configured hostnames/ports through a proxy path.
  • Runner containers cannot trivially bypass the proxy through direct internet access.
  • Denied egress attempts are logged with agent/workspace/repo/run context where available, without leaking secrets.
  • Docs explain the model, limitations, Docker/network requirements, and examples for GitHub + Anthropic/OpenAI.
  • Tests cover policy validation, effective-policy resolution, REST/MCP wire shapes, and runtime orchestration.
  • A real Docker smoke test path exists for restricted egress behavior, even if it is opt-in for local/CI environments.

Relationship to Runner Split

This should build on the daemon/runner split from #511. It should not be included in the current runner split PR unless explicitly requested. The runner split gives us the per-run container boundary; this issue adds a controlled network boundary on top.

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussingIssue is still under discussion — not ready for implementation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions