Skip to content
roxy

Sandbox containment

An untrusted workload runs where its only route out is roxy. It sends its traffic to roxy's proxy port and trusts roxy's CA. The network does the containing: the proxy settings only tell a well-behaved client where roxy is (threat model).

A workload inside a sealed sandbox network whose only exit is roxy's proxy port 3128. Requests go from the workload to roxy and on to the allowed upstreams, api.anthropic.com and github.com, with the real key added; responses come back the same way. roxy holds the CA and the secrets, and the workload fetches roxy-ca.pem from port 3130. A direct route out of the sandbox is blocked at the boundary: direct TCP, UDP and DNS have no route.

Steps

  1. Take away the workload's route out. Put it in a network namespace, VM or container network where roxy is the only host it can reach. Block everything else, including DNS. roxy resolves names itself, so the workload needs no DNS.

  2. Give roxy a route out, and give the workload a route to roxy's proxy port. Let the workload reach the CA port too if it should fetch the CA itself.

    listeners:
      - name: proxy
        mode: http_proxy
        bind: 0.0.0.0:3128
    ca_server:
      bind: 0.0.0.0:3130             # serves /roxy-ca.pem
  3. Point the workload at roxy and make it trust roxy's CA (CA distribution):

    export HTTP_PROXY=http://<proxy> HTTPS_PROXY=http://<proxy>
    export http_proxy=$HTTP_PROXY https_proxy=$HTTPS_PROXY   # curl reads only the lower-case http_proxy
    export SSL_CERT_FILE=/path/roxy-ca.pem         # OpenSSL-based tools, Python ssl, Go
    export REQUESTS_CA_BUNDLE=/path/roxy-ca.pem    # Python requests
    export NODE_EXTRA_CA_CERTS=/path/roxy-ca.pem   # Node
    export CURL_CA_BUNDLE=/path/roxy-ca.pem        # curl

    Adding the certificate to the system trust store also works. Do not put external hosts in NO_PROXY.

  4. Optionally, shut roxy for good when the workload misbehaves. A quarantine refuses every request until an operator lifts it. It can be tripped by a rule, or by a run of malformed requests:

    listeners:
      - name: proxy
        bind: 0.0.0.0:3128
        quarantine_on:
          parse_errors: { count: 20, window: 1m }
    rules:
      - id: exfil
        when: host == "api.anthropic.com" and body.text contains "BEGIN PRIVATE KEY"
        then: quarantine

    Run roxy with --state-dir so the quarantine survives a restart.

With Docker Compose

The workload is only on an internal: true network, which has no route out. roxy is on that network and on one with a route out:

services:
  roxy:
    image: ghcr.io/roxy-proxy/roxy:latest
    read_only: true
    cap_drop: [ALL]
    security_opt: ["no-new-privileges:true"]
    volumes: [./roxy.yaml:/etc/roxy/roxy.yaml:ro, roxy-ca:/var/lib/roxy/ca]
    networks: [sandbox, egress]
  agent:
    image: your-workload
    networks: [sandbox]              # no route out except through roxy
    environment:
      HTTPS_PROXY: http://roxy:3128
      https_proxy: http://roxy:3128
      NO_PROXY: roxy                 # roxy's CA endpoint, reached directly
      no_proxy: roxy
networks:
  sandbox:
    internal: true                   # no route out: the containment
  egress: {}
volumes:
  roxy-ca: {}

The workload fetches the CA from http://roxy:3130/roxy-ca.pem. A client that ignores the proxy settings gets nowhere, because there is no route out.

Other services on the Compose network have private addresses, which roxy refuses by default. A rule that lets the workload reach one needs then: { allow: { private_ok: true } } (address floor).