Skip to content

ADR-0005: Deny-all egress with named grant profiles

  • Status: Proposed
  • Date: 2026-08-06
  • Implementation: shipped — src/engine/egress.ts:453 serveGrantedRequest, :219 buildGrant, :285/:314 apply/revoke; profiles at src/engine/profiles.ts:441; floor posture at src/sandbox-do.ts:147. Tests: engine/egress.test.ts, engine/profiles.test.ts, engine/exec-fence.test.ts.

fractalbot’s egress engine (its ADR-0005; src/egress.ts, src/exec.ts) is the reviewed implementation: deny-all class state arming interception, request-level policy (method/path/body asserted against inputs no model authored), redirect re-policing with redirect:"manual", an admitted-set == handled-set invariant, kill-before-revoke. It is portable (zero Cloudflare imports) but must live where the containers live — interception is a property of the container-owning worker and cannot be layered on from outside. Its grant vocabulary, however, is public-read-one-repo only, and it is authored per host. flare-dispatch’s ~20 runs need egress the vocabulary cannot express (npm/pypi registries, Playwright browser downloads, api.cloudflare.com, dynamic e2e targets), and a grant derived from dispatch inputs would be attacker-influenceable — a hostile payload steering the allowlist defeats deny-all. Denials today are undiagnosable from outside: unlisted hosts die as bodyless 520s before any handler runs.

  • The substrate hosts the engine; deny-all is the floor for every workload, CI included.
  • Consumers declare network needs as named grant profiles maintained and security-reviewed in the substrate — public-repo-read, js-install, rust-install, browser-fetch, cf-api, github-api-read — composed per run/recipe. Grants derive exclusively from definitions frozen in reviewed code; a dispatch payload may select among pre-authored grants, never define one. Dynamic targets declare a target schema (allowed host pattern + scheme) in the definition; an input host outside the pattern fails the dispatch, not the policy.
  • Every denial — platform 520s and handler 403s — is recorded as a per-execution denial event {host, method, path, reason, count}, retrievable with the execution’s artifacts, never surfaced into the container (oracle resistance).
  • Rollout is a three-position per-run flag: legacy → report (legacy posture, would-be denials recorded — the grant-authoring loop) → enforce; a run graduates only after a clean report window.

A container request under report or enforce meets the platform gate first, then the grant’s handler:

Egress request evaluation Flowchart, top to bottom. 11 nodes, 12 edges. container request HTTP or HTTPS, intercepted → host denied, or outside the grant's allowlist? host denied, or outside the grant's allowlist? → 520 at the platform proxy captured as a denial event [yes]; → grant bound to this container? [no] grant bound to this container? → 403 from the handler denial recorded, body names the rule only [no]; → rollout position [yes] rollout position → forwarded untouched would-be denial recorded [report]; → protocol, method, path and body match a rule? [enforce] protocol, method, path and body match a rule? → 403 from the handler denial recorded, body names the rule only [no]; → fetch with redirect: manual this host's credential injected [yes] fetch with redirect: manual this host's credential injected → 3xx with Location? 3xx with Location? → protocol, method, path and body match a rule? [yes, re-policed]; → response to the container set-cookie stripped [no] yes no no yes report enforce no yes yes, re-policed no container requestHTTP or HTTPS, intercepted host denied, or outsidethe grant's allowlist? 520 at the platformproxycaptured as a denialevent grant bound tothis container? 403 from the handlerdenial recorded, bodynames the rule only rollout position forwarded untouchedwould-be denial recorded protocol, method, pathand body match a rule? fetch with redirect:manualthis host's credentialinjected 3xx with Location? response to thecontainerset-cookie stripped
Egress request evaluation
Diagram source
flowchart TB
accTitle: Egress request evaluation
req["**container request**<br/>HTTP or HTTPS, intercepted"] --> gate{"host denied, or outside<br/>the grant's allowlist?"}
gate -->|yes| p520["**520** at the platform proxy<br/>captured as a denial event"]
gate -->|no| bound{"grant bound to<br/>this container?"}
bound -->|no| h403["**403** from the handler<br/>denial recorded, body names the rule only"]
bound -->|yes| pos{"rollout position"}
pos -->|report| fwd["**forwarded untouched**<br/>would-be denial recorded"]
pos -->|enforce| rule{"protocol, method, path<br/>and body match a rule?"}
rule -->|no| h403
rule -->|yes| send["**fetch** with `redirect: manual`<br/>this host's credential injected"]
send --> redir{"3xx with Location?"}
redir -->|yes, re-policed| rule
redir -->|no| resp["response to the container<br/>`set-cookie` stripped"]
class p520,h403 danger
class fwd warn
class resp ok
  • One audited egress surface; flare-dispatch’s secrets-in-env posture ends (ADR-0006 carries the credential half).
  • Accepted residuals, inherited and documented: DNS exfiltration uncovered; double-forked children survive killAllProcesses; git-upload-pack is a bounded exfiltration sink.
  • Whether BYOC operators may author custom profiles, and behind what review gate, is deliberately open — it becomes the primary trust boundary if the substrate is ever consumed outside FractalBox.