Egress: watch it, then close it
Every workspace on your host leaves through one proxy there, behind a firewall that fails closed and a DNS filter per organisation. (If your organisation turns on members' own computers, egress there is observed, not enforced.) It ships in observe: decide, log, allow. You promote to deny once the record shows what your team needs.
$ bin/egress-proxy summary --days 14
# tick the hosts your team needs to build the allowlist lineTick the hosts your team legitimately needs, then preview deny. Under observe an unlisted host is recorded as observe with would_be: deny and still allowed; under deny the same host is refused. Direct GitHub is refused in both, because file transfer is below both. The numbers are made up.
What it is
Egress control is three pieces on your host working together. An iptables chain sends each organisation's network to one forward proxy and drops everything else. The proxy decides by hostname against an allowlist. A DNS filter on each organisation's gateway answers only allowlisted names. In observe the proxy records what it would refuse and lets it through. In deny it refuses.
On your host only. A workspace on a member's own computer, where an operator has granted it and the organisation has turned it on, uses that computer's own network. There, internet access is observed, not enforced: the platform sees only the calls that go through its brokers (git, AI and the portal). DNS filtering is not enforced there either.
It is
- One exit per host for the workspaces it runs, with a record of every connection: host, port, workspace, member, bytes and the decision.
- Per organisation, strictest wins: an organisation can move itself to deny over a platform that observes, never the reverse.
- Fail closed: if the proxy stops, the firewall drops the traffic.
It is not
- Blocking by default. A fresh install observes, logs and allows.
- Payload inspection or DLP. It never terminates TLS; it sees the hostname and the byte counts.
- A closed box.
npm publish, some AI APIs, HTTPS uploads to allowed sites, preview links, and code shown in an attached editor or terminal are still open. See the limits.
How it works
- Network. Each organisation's workspaces sit on their own Docker network,
agentstack-t-<org>. Creating a workspace is refused unless the firewall covers that network and a DNS gateway exists. - Firewall. In
DOCKER-USER, each organisation subnet jumps to one owned chain,AGENTSTACK_EGRESS. It returns traffic bound for the proxy, DNS and the host brokers, then LOGs (prefixagentstack-egress-deny, rate-limited) and DROPs everything else. A boot unit restores it after a reboot. - DNS filter. A hardened dnsmasq on each organisation's gateway forwards allowlisted names and answers
REFUSEDto the rest. Refusals are written to theegressaudit stream. No filter means no DNS, so it fails closed too. - Proxy. Workspaces reach the internet through
egress-proxywith a per-workspace proxy credential, so every record names a workspace. It computes a decision on every connection, whatever the mode, and readsegress_enforceon every connection: a flip to deny, or back, needs no restart. - Two conditions to block. A connection is refused only when the security profile puts egress on an allowlist (
metered,guardedorstrict) and enforcement isdeny. Under theopenprofile, deny blocks nothing. - Always on. Below file transfer
both, in every mode:github.comand*.github.comare refused (GitHub goes through the git broker;codeload.github.comsource archives still work),upload.pypi.orgis refused, and copy-tool ports 22, 2222, 873, 20, 21, 989, 990 and 69 are refused before connecting, and on any other port the proxy recognises ssh, rsync and ftp by their opening bytes and refuses before any of the copy is relayed.
Hostnames, not packets. Under observe, a bare IP or a hostname that resolves to GitHub is only logged. Under deny, a bare IP matches no allowlist entry and is refused.
Defaults
| Setting | Scope | Default | Allowed values |
|---|---|---|---|
egress_enforceAct on the decision, or only record it. | platformorganisation | observe | observe · denyAn organisation may tighten to deny, never loosen. |
| Security profileDecides whether egress is on an allowlist at all. | platformorganisationrepositorymember | guarded | open · metered · guarded · strictOnly open leaves egress unlisted. |
egress_allowlistHosts a workspace may reach. A leading dot means “and every subdomain”. | platformorganisation | github.com, api.github.com, codeload.github.com, .npmjs.org, pypi.org, files.pythonhosted.org, api.anthropic.com, open-vsx.org, openvsx.eclipsecontent.org | host listAn organisation's list is added to the platform's; it cannot remove from it. |
egress_portsDestination ports a tunnel may target. | platform | 443, 80 | port list |
egress_identityRefuse connections without a workspace credential. | platform | optional | optional · required |
egress_idle_timeoutClose a silent tunnel, so its bytes get recorded. | platform | 600 s | 10–86400 |
egress_connect_timeout | platform | 15 s | 1–120 |
egress_log_only_graceHow long the console keeps reminding you it is only observing. Changes no traffic. | platform | 7 days | 0–365 (0 = no reminder) |
workspace_file_transferAt both, direct GitHub, copy ports and unfiltered DNS open up. | platformorganisation | off | off · download · upload · both |
workspace_logs_egressDevelopers see their own egress decisions in the Logs pane. | platformorganisation | on | true · falseAn organisation may turn it off, not back on past the host. |
workspace_egress_show_hostsThat feed shows the destination host. | platformorganisation | on | true · false |
docker_egressWhether a workspace's Docker containers reach the internet. | platformorganisation | off: the workspace database and each other only | off · proxyproxy gives the containers the workspace's own allowlist. |
| Firewall and DNS filterNo setting: part of the host install. Workspaces on your host only, not on members' computers. | platform | on | n/a |
Configure it
Everything is set with bin/policy on the host or in the console. The proxy reads policy on every connection, so none of these needs a restart.
On the host
# where you stand: mode, profile and what is enforced
$ bin/egress-proxy status
$ bin/egress-proxy posture --tenant acme
# would this host be allowed for acme? decides nothing, changes nothing
$ bin/egress-proxy check --host registry.example.org --tenant acme
# what the organisation reached, ranked, with a proposed list
$ bin/egress-proxy summary --days 14
$ bin/egress-proxy summary --days 14 --workspace acme/dev01 --json
# add hosts for one organisation (added to the platform list)
$ bin/policy set --scope tenant --target acme --key egress_allowlist --value 'registry.example.org,.example.net'
# promote one organisation to deny, or the whole host
$ bin/policy set --scope tenant --target acme --key egress_enforce --value deny
$ bin/policy set --key egress_enforce --value deny
# what acme actually sees after both scopes are applied
$ bin/policy get-for --tenant acme --key egress_enforceA platform set replaces the whole list. bin/policy set --key egress_allowlist without a scope writes the platform list as given, dropping anything you leave out. Start from the proposed line summary prints, which includes the current entries.
In the console
- Organisation
OrganisationsacmeSettingsEgress
Mode and the organisation's added hosts.- Requests
OrganisationsacmeSettingsAllowlist requests
Approve or deny what members asked for, with a reason recorded on the audit trail.- Operator
ConsoleEgress policy
andConsoleEgress monitor
for the platform list and traffic across organisations.- Developer
WorkspaceLogsegress
Each refused or observed row has a request button that asks an administrator to allow that host.
Firewall and DNS filter
$ sudo bin/egress-firewall preflight
$ sudo bin/egress-firewall up
$ sudo bin/egress-firewall verify
$ sudo bin/egress-firewall persistence
$ bin/dns-filter status
$ bin/dns-filter sync
$ bin/egress-coverageTasks
Promote an organisation from observe to deny
Let the team work for a week in observe
Nothing to set: it is the default. Optionally require identity so every record names a workspace.
$ bin/policy set --key egress_identity --value requiredExpected: connections without a workspace credential get a 407 that says the workspace must present its proxy credential.
Read what they reached
$ bin/egress-proxy summary --days 14Expected: a ranked table (host, requests, workspaces, bytes up and down, would-deny count, verdict) with verdicts
ALREADY ALLOWED,ALLOWorNEEDS A DECISION, then a proposed allowlist line. Nothing is applied automatically.Add the hosts that are legitimate
$ bin/policy set --scope tenant --target acme \ --key egress_allowlist --value 'registry.example.org,cdn.example.net'Expected:
bin/egress-proxy check --host registry.example.org --tenant acmenow printsallowlist ALLOW.Switch the organisation to deny
$ bin/policy set --scope tenant --target acme --key egress_enforce --value denyExpected: the next connection to an unlisted host gets a 403 whose body starts
egress-proxy: paste.example.io:443 is not permitted from this workspace.To back out during an incident, set it toobserve; it applies on the next connection.Handle the first request
A developer who hits a new host presses request in the Logs pane. You approve it in Allowlist requests; approving adds the host to the organisation's list and writes
allowlist-approveto theegressstream.
Let one organisation use a private registry
Check what would happen now
$ bin/egress-proxy check --host registry.example.org --port 443 --tenant acmeExpected:
allowlist DENYwith the reasonregistry.example.org is not on the allowlist, and aresultofOBSERVEorDENYdepending on the mode.Add it for acme only
Use the organisation scope, so other organisations on the host are unaffected. If the registry is not on 443 or 80, an operator must add the port to
egress_portsfor the whole host.Confirm DNS resolves it
Allowlisted names are forwarded by the organisation's DNS filter. Run
bin/dns-filter syncif the filter has not picked up the change, thenbin/dns-filter status.
Prove every organisation network is covered
Run the coverage check
$ bin/egress-coverage $ bin/egress-coverage --jsonExpected: exit 0 with every organisation network closed. Exit 1 means gaps or a failed verify; exit 2 means it could not tell.
Close a gap
$ sudo bin/egress-firewall up $ sudo bin/egress-firewall verifyExpected:
verifypasses with no line naming an uncovered subnet.
Verify it works
From inside a workspace, after denysynthetic
On the host
Good looks like: check says ALLOW for what the team needs and DENY (exit 2) for an unknown host; bin/egress-coverage exits 0; bin/egress-firewall verify passes; the egress audit stream shows deny records for refused hosts and allow for the rest. /api/readyz also reports firewall and DNS-filter coverage beside its checks (see Operate).
Troubleshooting
Workspace create failsegress firewall does not cover agentstack-t-acme - refusing uncontrolled egress
Cause: the organisation's network has no jump into the egress chain. Fix: sudo bin/egress-firewall up, then verify, then create again. egress firewall is missing means the chain was never installed on this host.
A build fails after denyegress-proxy: cdn.example.net:443 is not permitted from this workspace.
Cause: the host was not on the list when you promoted. Fix: add it at organisation scope, or approve the member's request. It is a settings change, not a rebuild.
Allowed host, wrong portregistry.example.org is allowed but port 8443 is not (allowed: 443, 80)
Cause: egress_ports is host-wide and defaults to 443 and 80. Fix: add the port only if a tool really needs it: bin/policy set --key egress_ports --value '443,80,8443'.
Push or clone to GitHub failsDirect GitHub access is blocked by your organisation's File transfer setting. Clone and push through the broker URL in ~/README.md.
Cause: working as intended below file transfer both, in every mode. Fix: use the broker remote. See Git & provenance.
scp or rsync refusedport 22 is a file-copy channel (ssh/scp/sftp/rsync) and workspace_file_transfer is off - copying needs 'both'
Cause: copy-tool ports are refused below both. Fix: none needed if that is your policy; otherwise raise file transfer for that organisation (see Workspaces).
Every connection fails with 503egress-proxy: audit service is unavailable; the outbound connection was not opened.
Cause: the proxy will not open a connection it cannot record. Fix: bring the audit service back, then check bin/egress-proxy status.
Coverage reports gaps1 tenant network(s) have NO egress rules - their workspaces reach the internet unexamined
Cause: a network was made outside workspace-agent, or the firewall was flushed. Fix: sudo bin/egress-firewall up, then run bin/egress-coverage again.
Deny is set, nothing is blocked
Cause: the organisation resolves to the open profile, whose egress control is open. Fix: bin/policy resolve --tenant acme shows why; move it to metered or stricter. bin/tenant-audit lists every organisation looser than the default.
Limits and what is not built yet
- Observe by default. Deny by default for fresh installs is built and held back while the team runs on deny first.
npm publishcan upload code to an allowed registry, in any mode.- Some AI provider APIs are reachable directly through the allowlist.
- Copy tools. Below file transfer
both, on workspaces on your host, in both egress modes: copy-tool ports (22, 2222, 873, 20, 21, 989, 990, 69) are refused before connecting, and on any other port the egress proxy recognises ssh, rsync and ftp by their opening bytes and refuses before any of the copy is relayed; the firewall drops anything that bypasses the proxy. Not covered: members' own computers (egress there is observed, not enforced); a copy tool hidden inside an ordinary HTTPS connection to an allowed site, and HTTPS uploads to an allowed site, which are egress-allowlist questions; and a workspace underprivilegedfull Docker, where the firewall guarantee no longer holds. - Detectable, not preventable: preview links, and code shown in an attached editor or terminal. The file-transfer setting does govern the console's Files pane, the browser IDE's Download and Upload, and scp, sftp and rsync over desktop IDE attach; an attach-copy refusal is logged inside the workspace only, not on the platform audit trail.
- No payload inspection or DLP, and no TLS termination.
- No per-organisation destination feed in the organisation console yet; operators see traffic in Egress monitor and
summary. - No coverage on hosts where the firewall's root copy is not installed.
- Members' computers. If a platform operator grants it and your organisation turns it on, a member's own computer can host their workspaces. There, internet access is observed, not enforced; DNS filtering is not enforced; the source on its disk is readable by whoever uses it; and Docker is not available.
- Docker containers in a workspace get no internet by default; with
docker_egressatproxythey use the workspace's own allowlist. Pulled images are one shared cache on the host, so a workspace can tell an image was already pulled.
Related
Verified against the main branch, 4 Oct 2026.