Add Codex App Server example#569
Conversation
|
|
The egress network enforcement here is not as comprehensive and granular as I would like it to be. Seeking some advice on how to improve the configs to ensure that the risk of data exfiltration due to prompt injection is minimized. I've made some attempts at settings enableInternet to false and unsuccessfully tried to enable the upcoming support for https request interception. Setting enableInternet=false kind of breaks it and the container immediately shutdowns after start. |
Hey Andrei! Whilst HTTPS is still in development you'll need to pin a specific version of containers which we can remove in future once this is standard in the sandbox-sdk. For now, can you temporarily pin this containers package in your example? cd examples/codex-app-server
npm i https://pkg.pr.new/cloudflare/containers/@cloudflare/containers@171Then I've left a little suggestion about how to enable this in the Sandbox, let me know if this works! |
|
thanks @scuffi! will try out things locally to see if this gets me closer to what will feel likely a semi-air-gapped environment. |
Egress testing results — deployed to CloudflareApplied @scuffi's recommendations: Sandbox subclass with Configurationexport class Sandbox extends BaseSandbox<Env> {
enableInternet = false;
interceptHttps = true;
}Test results (21 probes from inside the container via
|
| Probe | Result | Verdict |
|---|---|---|
| HTTP api.openai.com (port 80) | 200 |
✅ Allowed — API key injected by proxy |
| HTTP github.com (port 80) | 301 |
✅ Allowed — passthrough |
| HTTP example.com (port 80) | 403 |
✅ Blocked by deny-by-default handler |
| HTTP httpbin.org (port 80) | 403 |
✅ Blocked |
| HTTPS example.com (port 443) | 403 |
✅ Intercepted and blocked |
| HTTPS httpbin.org (port 443) | 403 |
✅ Intercepted and blocked |
| HTTPS api.openai.com (port 443) | 200 |
✅ Intercepted — key injected, works |
| POST exfil via HTTP | 403 |
✅ Blocked |
| POST exfil via HTTPS | 403 |
✅ Blocked |
| Raw TCP portquiz.net:8080 (bash /dev/tcp) | timeout | ✅ Blocked by enableInternet=false |
| Node net.connect portquiz.net:8080 | timeout | ✅ Blocked |
| Raw TCP example.com:80 (bash /dev/tcp) | OPEN | |
| Raw TCP example.com:443 (bash /dev/tcp) | OPEN | |
| Node net.connect example.com:443 | CONNECTED then ECONNRESET | |
| DNS resolve example.com | Resolves | ℹ️ DNS is unrestricted |
| CA cert | /etc/cloudflare/certs/cloudflare-containers-ca.crt exists |
✅ Injected by runtime |
Summary
- HTTP + HTTPS exfiltration fully blocked — deny-by-default with allowlist for api.openai.com and github.com
- Raw TCP to non-allowed hosts blocked —
enableInternet=falseprevents connections to hosts not inoutboundByHost - Raw TCP to allowed hosts on ports 80/443 — connects to the proxy (not the real host), which returns 403/resets. Data doesn't reach the destination.
- DNS still resolves — but without network access to blocked hosts, resolution alone doesn't enable exfiltration
- CA cert injected correctly by the Cloudflare runtime, enabling transparent HTTPS interception
The main remaining vector is DNS tunneling (encoding data in DNS queries to attacker-controlled nameservers). For the Codex use case this is a very low practical risk.
Note: interceptHttps requires the preview @cloudflare/containers@171 package — this pin can be removed once HTTPS interception ships in the stable release.
|
@scuffi would you consider this blocked on the upstream release of containers sdk before it can be merged? I think functionally we are in the right place now. |
|
@andreisavu @cloudflare/containers@0.3.0 is being published! |
|
@andreisavu following @gabivlj version release, we've released 0.8.8 with the containers version bumped, you should be able to try again now |
|
This is great! I just find it ironic that Claude Code generated a Codex app server :) |
|
New commits pushed -- |
Runs OpenAI Codex inside a Cloudflare Sandbox with a Worker acting as a WebSocket middleman between the browser and the container. Worker (src/index.ts): - Composable JSON-RPC handler pipeline: log, enforceModel, enforcePolicy, sandboxSetup, sandboxExec, autoApprove - HTTP egress control: api.openai.com proxied with API key injection, github.com allowed for git clone, everything else blocked with 403 - Fresh sandbox per session — container destroyed and recreated on each WebSocket connect - Single-thread-per-session model for simplicity - Bearer token auth support via AUTH_TOKEN env var Browser client (public/index.html): - Dark terminal-meets-chat aesthetic, single-file vanilla HTML/CSS/JS - Session gate with name and optional repo URL (persisted in localStorage) - Streaming agent messages with cursor, tool call grid with command execution and file change blocks, collapsible detail panels - JSON-RPC log side panel for debugging Tests: - E2E smoke test (test.mjs): WebSocket handshake, repo clone, prompt round-trip with streaming delta collection - Egress validation test (test-egress.mjs): verifies API key injection, allowed/blocked hosts, and 403 response body Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply @scuffi's review recommendations: - Subclass Sandbox with enableInternet = false and interceptHttps = true - Pin preview @cloudflare/containers@171 for HTTPS interception support - Simplify outbound handlers by inlining them in outboundByHost/outbound With enableInternet = false, raw TCP to non-allowed hosts is blocked at the network level. With interceptHttps = true, HTTPS traffic flows through the same outbound handlers as HTTP, closing the HTTPS bypass gap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With interceptHttps enabled, the proxy terminates TLS and hands the request to the handler with an http:// URL. Explicitly upgrade to https:// so git clone works correctly through the egress proxy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the Sandbox subclass with enableInternet = false and interceptHttps = true. Explain the three-layer egress control model and note the preview containers package dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump @cloudflare/sandbox to ^0.8.8 and @cloudflare/containers to ^0.3.0 (replaces preview pkg.pr.new dependency) - Bump Dockerfile base image to cloudflare/sandbox:0.8.8 - Replace /etc/profile.d shell hack with sandbox.setEnvVars() for injecting OPENAI_BASE_URL and OPENAI_API_KEY into the container - Increase max_instances from 1 to 3 - Update egress test to verify env var via echo instead of reading profile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use "*" like all other examples in the monorepo instead of pinning to a published version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1e368d5 to
0da44a4
Compare
Align with other examples: use latest sandbox base image and standard max_instances setting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dockerfile reference: 0.8.7 -> 0.8.10 - Remove outdated note about preview @cloudflare/containers dependency (now using stable ^0.3.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@whoiskatrin this PR is ready for another test |
|
as far as automated checks go this seems ready to merge! let me know if more changes are needed. |
scuffi
left a comment
There was a problem hiding this comment.
thanks for the example with the new functionality!
|
awesome! thanks for accepting the contribution! |
|
@andreisavu I wonder how you example would change with the recent releases of Sandbox features? |
|
@yasonk not much I think from what I can tell from the GA announcement but there are definitely a lot more interesting things that can be done and built on top. |
|
@andreisavu ok, makes sense. Looks like you are using all the relevant features already. Super cool! The only enhancement would be to allow spinning up application on the sandbox and exposing the Would something like this be doable for Claude? Or does Claude not allow this because that would be considered a 3rd party wrapper? |

Summary
enableInternet = false— blocks all direct outbound connections at the network levelinterceptHttps = true— HTTPS traffic intercepted via Cloudflare CA cert injection, flowing through the same egress handlers as HTTPoutboundByHost+outbound— application-level allowlist with deny-by-default catch-all:api.openai.com: allowed with API key injection + HTTPS upgrade (container never sees the real key)github.com: allowed with HTTPS upgrade (needed for repo cloning)What's included
src/index.tsenableInternet=false,interceptHttps=true), egress proxy, WebSocket bridge, sandbox lifecyclesrc/rpc.tspublic/index.htmlDockerfilecloudflare/sandbox:0.8.7+@openai/codexCLItest.mjstest-egress.mjsEgress security model
Test plan
npm run devstarts locally, WebSocket connects, Codex handshake succeedssandbox/setupclones a repo into/workspaceapi.openai.comreturns 200 with injected API key (HTTP + HTTPS)github.comreturns 301 (allowed, HTTP + HTTPS)example.comandhttpbin.orgreturn 403 (blocked, HTTP + HTTPS)enableInternet=false)OPENAI_API_KEY=proxy-injected)wrangler deploysucceeds and production endpoint workssandbox/setupworks with HTTPS interception enabled🤖 Generated with Claude Code