Skip to main content

Egress Allowlist

A hybrid or on-prem deployment runs the run gateway inside your network, which means your firewall decides which upstreams a tool call can reach. Willow can produce the list of hosts to allow.

Most of that list is already knowable: a REST connector stores the API base URL, a remote MCP integration stores its endpoint, an OAuth provider stores its token URL. Willow reads those out of stored configuration and exports them.

Stdio MCP servers are the exception, and they are the reason this page has a second half. A stdio integration stores a package to run, not a host to reach:

{ "command": "npx", "args": ["-y", "example-mcp"] }

The upstreams live inside that package. No amount of configuration inspection produces them, so the export can only mark them unknown and ask you to find them yourself. Host discovery closes that gap by recording the DNS lookups each sandbox actually makes.

Hybrid and on-prem only

SaaS deployments run the gateway on Willow's infrastructure, so there is no firewall of yours in the path and nothing to allow. The export appears only for on-prem organizations and for organizations with an external gateway.

Export the allowlist

Integrations → the menu in the page header → Export Egress Allowlist. You get a CSV, one row per host your network team has to allow.

ColumnWhat it holds
HostThe hostname to allow, or unknown for a row Willow cannot name
PortAlmost always 443; blank when the port is not knowable
IntegrationThe integration's display name, or Willow platform for shared infrastructure
Integration SlugThe integration's slug
Integration Statusactive, draft, or disabled
PurposeWhy the host is reached — see below
ProvenanceHow Willow determined it — see below
NotesThe evidence behind the row, in prose

Draft and disabled integrations are included deliberately. A firewall change takes longer to land than flipping an integration on, so you need the rules for what you are about to enable. Integration Status is how you tell "needed today" from "needed soon".

Purpose

ValueMeaning
tool-apiThe third-party API a tool actually calls
oauthAn authorization or token endpoint
mcp-endpointA remote MCP server's own URL
package-registrynpm or PyPI, fetched on every sandbox cold start
container-imageThe registry the sandbox base image is pulled from
webhookA URL a webhook workflow forwards events to
platformWillow infrastructure the gateway needs to run at all

Provenance

Provenance is the column to read before you turn a row into a firewall rule. It says how much the row can be trusted.

ValueMeaningWhat to do
resolvedRead straight out of stored configurationAllow it
observedSeen in a real DNS lookup from the integration's sandboxAllow it, but see the caveat below
missing-configThe configuration is a template whose setting was never filled inFinish configuring the integration, then re-export
unknown-runtimeA stdio MCP server whose upstreams live inside its packageTurn on host discovery, or ask the server's vendor
observed is "seen so far", not "required"

An observed host is real — the sandbox resolved it. But observation is driven by use: a tool nobody has run has never resolved anything, so the list grows as more of the server's tools are exercised. Willow keeps observed distinct from resolved, and keeps emitting the unknown row alongside it, precisely so a partial answer cannot be mistaken for a complete one.

Host discovery

Optional, off by default, hybrid and on-prem only. It turns unknown rows into named hosts.

How it works

Every outbound connection resolves a name before it connects, whatever HTTP library the package happens to use. That makes DNS the one vantage point that sees every upstream, including ones a forward proxy would miss.

When you set SANDBOX_DNS_UPSTREAM, run binds a DNS listener on the sandbox network's gateway and starts each sandbox with two resolvers: the listener first, and your real resolver second. The listener forwards every query verbatim to your resolver, relays the answer back unchanged, and records the name that was asked. Those names are reported to Willow, and appear in the export with provenance observed.

Two consequences worth understanding:

  • A blocked call still teaches you the host. Resolution happens before the connection, so when your firewall denies the connect, the lookup has already been recorded. The tool call that just failed is what produces the rule needed to make it succeed — which is why this works on exactly the locked-down networks where the answer matters most.
  • It is not a policy point. The listener forwards every query and never blocks one. What a sandbox is allowed to reach remains your firewall's decision.
What leaves your network

On hybrid, run reports observed hostnames to Willow SaaS, because that is where your organization's configuration lives. The hostnames themselves leave your network; the queries, the answers, and the traffic do not. On on-prem, everything stays in your cluster.

What is not recorded

  • Reverse lookups (*.in-addr.arpa, *.ip6.arpa) and single-label names.
  • Names ending in .local, .localdomain, .internal, or .svc. These are usually search-domain expansions of a name the resolver then retries unqualified, and listing them would send your firewall team after names that never leave the cluster. If a genuine upstream of yours lives on one of these suffixes, it will not appear — add it by hand.
  • Package registries and sandbox image registries. The export already reports those from configuration, and recording them again would put one host on two rows with different provenance.
  • Anything from an integration that already has 500 recorded hosts. Past that point the integration stops accruing rows rather than the table growing without limit.

Requirements

RequirementWhy
The dind sandbox backendThe listener attaches to a docker network. The podmcp and Cloudflare backends are not covered.
Chart 1.0.59 or laterIt grants NET_BIND_SERVICE to the run container. The listener needs port 53, and --dns cannot name any other port.
An IPv4 resolver address--dns accepts nothing else.
A UDP/53 rule, if you filter sandbox egressA hardened sandbox network drops RFC1918 wholesale, which includes its own gateway.

Step 1 — Find your resolver address

This is the resolver run will forward to, and the fallback each sandbox gets as its second --dns entry. In almost every cluster it is the in-cluster DNS service, which is what sandboxes already resolve through today:

kubectl get svc -n kube-system kube-dns -o jsonpath='{.spec.clusterIP}'
# Common values: 10.96.0.10, 172.20.0.10

Use that address, or the address of whichever resolver you would rather sandboxes use. It must be an IPv4 literal — a hostname is rejected at startup and the feature stays off.

Step 2 — Set it in your values.yaml

deployments:
run:
env:
SANDBOX_DNS_UPSTREAM: "10.96.0.10"

NET_BIND_SERVICE is already a chart default for run, so there is nothing else to add. Upgrade the release:

helm upgrade willow webrix/webrix-helm -n willow -f values.yaml

Step 3 — Allow UDP/53 to the sandbox network gateway

Skip this step unless you filter the sandbox network's egress. If you have followed the hardening guidance and dropped RFC1918 from the sandbox network, that drop also covers the network's own gateway — which is the address the listener binds. Sandboxes will fall through to their second resolver and nothing will be recorded.

Add one narrow hole in the dind sidecar's rules: UDP, port 53, to the gateway address of the sandbox network only.

# The gateway address the listener binds, as run derives it
docker network inspect "$SANDBOX_NETWORK" \
-f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'

Step 4 — Verify

The listener logs once when it binds:

kubectl logs -n willow deploy/run | grep EgressObserver
# [EgressObserver] Recording sandbox DNS lookups

Then run a tool on a stdio integration and open Integrations → your integration → SettingsEgress Hosts. Observations are batched and flushed once a minute, so give it that long and use Refresh.

The panel lists the upstreams that integration reaches, with observed rows marked. It appears only once there is something to show — a freshly installed server that has not been used yet has no observations, and an empty panel would read as "nothing to allow" rather than "nothing seen yet". The full list, including shared Willow infrastructure and the package registries, stays in the CSV export.

Turning it off

Remove SANDBOX_DNS_UPSTREAM and upgrade. Sandboxes then start with no --dns flag and resolve exactly as they did before. Rows already recorded stay in the export; they were real observations and remain accurate.

This is also the failure mode by design: if the listener cannot bind — no NET_BIND_SERVICE, a remote docker daemon, a resolver address that is not an IPv4 literal — run logs the reason, adds no --dns flag, and sandbox DNS is untouched. A deployment that asked for the feature but cannot run it behaves exactly like one that never asked.

What to watch

The listener sits in the resolution path of every dind stdio sandbox, so its failure modes matter more than the data it collects. All three metrics are on run's Prometheus endpoint.

MetricWatch for
mcp_egress_dns_queries_total{outcome}upstream_timeout and upstream_error mean sandboxes are falling through to their second resolver. Sustained overloaded means queries are going unanswered on the first try. A steady unattributed rate means lookups are seen but cannot be tied to an integration. Any spoofed_reply is a datagram from something other than your resolver — dropped, but worth an alert if it persists.
mcp_egress_observations_pendingRows buffered in memory. Should return to zero every minute.
mcp_egress_report_total{outcome}error means a batch could not be delivered. Batches are dropped rather than retried; the next tool call re-observes the same hosts.
A hung listener is worse than a stopped one

The second --dns entry covers a listener that has stopped answering: lookups fall through in about 0.2 seconds. It does not cover one that is slow — a listener that accepts packets and never replies costs roughly 5 seconds per lookup while the sandbox's resolver waits out its timeout. If upstream_timeout is climbing, restart the run pod rather than leaving it wedged.

Troubleshooting

SymptomCauseFix
No [EgressObserver] line in the logsSANDBOX_DNS_UPSTREAM is not set, or is not an IPv4 literalCheck the value; a rejected address logs egress_observer_bad_upstream
Could not bind port 53 ...Chart older than 1.0.59, so the container has no NET_BIND_SERVICEUpgrade the chart
Listener started, but nothing is ever observedThe sandbox network's egress filter is dropping UDP/53 to its own gatewayStep 3
unattributed climbing, nothing in the panelLookups are arriving from something other than a sandbox run startedExpected at a low rate; sustained, check that the dind backend is in use rather than podmcp
Panel does not appear on the integrationNothing observed yet, or the deployment has no external gatewayRun one of the server's tools and refresh after a minute
Hosts appear for one integration but not anotherThe other integration's tools have not been runObservation is driven by use — exercise the tools you care about
A known internal upstream never appearsIts name ends in .internal, .local, .localdomain, or .svcThese are filtered as cluster-internal; add the host to your allowlist by hand