Egress
Egress is spotlight egress, a container a host starts beside each sandbox
for one run and destroys with it. It is the sandbox's only way out.
The sandbox is the security boundary
gives the reasons.
Topology
| Part | Value |
|---|---|
| Sandbox network | spotlight-<runner id>, an internal network. The sandbox joins only this network. |
| Egress container | spotlight-egress-<runner id>, on the sandbox network with the alias egress, and on SPOTLIGHT_SANDBOX_NETWORK or the engine's default network. |
| Egress image | SPOTLIGHT_EGRESS_IMAGE, default ghcr.io/usespotlightai/spotlight-runner:<release>, run with entrypoint spotlight egress. |
| Port | 3128 |
| Proxy address | http://sandbox:<credential>@egress:3128, in the sandbox's HTTP_PROXY and HTTPS_PROXY. The credential is random, for this run alone. |
| Extra hosts entries | SPOTLIGHT_SANDBOX_HOSTS |
A client in the sandbox that ignores the proxy has no route out. See Inside a sandbox for the proxy and certificate variables.
On start, egress reads /run/spotlight/egress.json from the host and deletes
it. It redeems the run's secrets from Spotlight, for up to 1 minute. It then
listens, and writes its certificate authority for the host to lay into the
sandbox. The host waits up to 90 seconds for it. On SIGTERM it stops within 5
seconds and clears the secret values it held.
Admitted hosts
SPOTLIGHT_EGRESS_ALLOW | Admitted |
|---|---|
| unset or empty | Any host. |
| a list | Each exact host listed, and the subdomains of each *.<domain> entry. *.example.com admits api.example.com, not example.com. |
Host names are compared without regard to case. The Spotlight address, and the
storage hosts Spotlight names for the run, are always admitted. * alone is
refused at host start.
Refused addresses
Egress checks the address it connects to, after resolving the host name.
| Address | Refused |
|---|---|
Loopback, link-local (including 169.254.169.254), multicast, unspecified, 0.0.0.0/8 | Always. |
Private (RFC 1918, IPv6 unique local), 100.64.0.0/10, 64:ff9b::/96, 2002::/16 | Unless an allow-list entry names the host. An empty allow-list names no host. |
| Any other | Never. |
TLS
| Property | Value |
|---|---|
| Certificate authority | One per run, ECDSA P-256, made by egress. The key stays in egress's memory. |
| Lifetime | 48 hours |
| Name constraints | The hosts egress adds something to requests to. With none, spotlight.invalid. |
| Intercepted | CONNECT to port 443 of a host egress adds something to requests to. HTTP/1.1, TLS 1.2 or later. |
| Not intercepted | Every other CONNECT, tunnelled to the host named in it. |
| Upstream trust | The egress image's system certificates, plus SPOTLIGHT_EGRESS_TRUST. |
A client that pins certificates fails against an intercepted host. Egress
removes Proxy-Authorization from every request it passes on.
What egress adds to requests
On requests to the host a secret is bound to, egress replaces a request header or query parameter with the secret's value.
Harness keys
A host holds the harness keys in its settings
and hands them to egress. The sandbox gets spotlight-egress:<NAME> in their
place. Egress replaces the header only when it holds exactly that stand-in,
with its prefix, as in authorization: Bearer spotlight-egress:OPENAI_API_KEY.
Egress logs one of these reasons and sends the request without the harness key:
the host's allow-list does not admit <host>it is empty or more than one line
Run secrets
A secret an MCP server's environment names, as secret.<NAME>, is added for
the run. The MCP server gets spotlight-egress:<NAME>. Egress replaces the
bound header only on requests to the bound host whose header holds that
stand-in, with or without the binding's prefix, as in
authorization: Bearer spotlight-egress:LINEAR_API_KEY. The stand-in must be
the whole value, or come right after the exact prefix. Other requests to the
host go out without the secret.
For a secret bound to a query parameter, egress replaces the parameter's value
only where it is the stand-in, or the prefix followed by the stand-in, as in
?key=spotlight-egress:MAPS_KEY. The other parameters keep their order and
encoding. Egress never logs the full URL. It answers a plain-HTTP or
intercepted request it could not pass on with only
Egress couldn't reach <host:port>., without the reason. See
spotlight_secret and
Give an MCP server a secret.
| Secret source | How egress gets the value |
|---|---|
| Kept by Spotlight | Sealed to a key egress makes for the run (X25519, HKDF-SHA256, AES-256-GCM, bound to the run and the secret). |
| AWS Secrets Manager | Read by egress with the host's AWS identity, from the default credential chain and SPOTLIGHT_EGRESS_ENV. Only a secret SPOTLIGHT_EGRESS_READS admits. |
Egress logs one of these reasons and sends the request without the run secret:
the host holds a key of its own for <host>the host's allow-list does not admit <host>the host does not let egress read <secret id>another secret is already worn in <Header> toward <host>, orin ?<parameter> toward <host>for a query parameterit is worn in neither a header nor a query parameterit names no header to be worn in, orit names no query to be worn init holds more than one line, so it cannot be worn in a request
On a request to a host egress holds a secret or a harness key for, a header
or query parameter holding a stand-in it doesn't put a value in, such as a
misspelt name, is removed before the request goes out, and egress logs
took a stand-in egress does not hold off a request with the header's or
parameter's name only.
Egress takes the secret back out of what the bound host answers before the
sandbox sees it. In the Location, Content-Location, Link and Refresh
headers, and in a text-like body (JSON, text/*, HTML, XML and form-encoded),
every spelling of the value is swapped back to its stand-in: as written,
query-escaped, path-escaped and escaped with lowercase hex, each with and
without the binding's prefix. A binary body, or one the host compressed, is
passed on as it is. Egress asks the bound host for an uncompressed answer. A
response it rewrites loses its Content-Length and is sent chunked.
Reading a secret from AWS Secrets Manager fails with:
| Message | Cause |
|---|---|
find the host's AWS identity: <error> | No AWS identity. |
AWS Secrets Manager answered <code> | An AWS error. |
AWS Secrets Manager could not be asked: <error> | AWS not reached. |
AWS Secrets Manager holds no text for it | No text value. |
its value in AWS Secrets Manager is not JSON, so it has no <field> field | field set, value not JSON. |
its value in AWS Secrets Manager holds no text at <field> | field missing. |
Egress logs each failure, and the request goes out without that secret.
The run's Spotlight token
A run of a setup chat reaches Spotlight's own API. When egress redeems the run, Spotlight mints a workspace token for that run's attempt alone and seals it to egress, like a secret it keeps.
| Property | Value |
|---|---|
| Scopes | plan, which includes read. |
| Issued by | The person who started the chat. Recorded in the audit as minted by Spotlight, with the run and its attempt. |
| Expires | An hour after egress redeems the run. |
| Revoked | When the run completes, is abandoned or withdrawn, when its attempt lapses or is parked, or when the runner loses the run while egress redeems it. |
| Stand-in | spotlight-egress:spotlight-api, in SPOTLIGHT_API_TOKEN. See The agent's environment. |
| Worn | In Authorization, in place of the stand-in or Bearer and the stand-in, on requests to the Spotlight address's host. |
Egress wears it whatever the host's allow-list says, since the Spotlight
address is always admitted. When the Spotlight address is plain HTTP, as in
http://control-plane:3000 inside the host's network, egress also wears it
over plain HTTP, on that address's port only. No other secret is worn over
plain HTTP. Egress does not hold the token when the host holds a key of its own
for the Spotlight address, or another secret is worn in Authorization toward
it, and logs could not hold the run's Spotlight token with the reason.
The token is not listed among the workspace's tokens. A run that reaches Spotlight's API is never reserved for a runner a push pool provisions.
Responses
| Status | Message | Cause |
|---|---|---|
407 | Egress answers only the sandbox it was started for. | No proxy credential, or the wrong one. |
400 | Egress is a proxy. Please ask it for an absolute URL, or CONNECT to a host. | A request that is neither CONNECT nor an absolute URL. |
403 | Egress doesn't let this sandbox reach <host>. | The allow-list does not admit the host. |
502 | Egress couldn't reach <host:port>. Error: Egress never opens a connection to <ip>. | A tunnelled CONNECT to an always-refused address. |
502 | Egress couldn't reach <host:port>. Error: Egress opens a connection to private address <ip> only if the allow-list names the host. | A tunnelled CONNECT to a private address the allow-list does not name. |
502 | Egress couldn't reach <host:port>. Error: <reason> | Any other failure of a tunnelled CONNECT. |
502 | Egress couldn't reach <host:port>. | A plain-HTTP or intercepted request that failed for any of these causes. The reason and the URL are left out. |
502 | Egress couldn't stand in for <host>. Error: <reason> | No certificate could be made for an intercepted host. |
Connections time out after 30 seconds, and TLS handshakes upstream after 15 seconds.
Egress start failures
These fail the run with The sandbox's egress didn't start. Error: <error>:
| Message | Cause |
|---|---|
the control plane handed no credential for the sandbox's egress | Spotlight handed no egress credential with the work. |
egress was not ready within 1m30s | Egress did not publish its authority in time. |
pull the egress image <image>: <error> | The egress image could not be pulled. |
If Spotlight refuses to redeem the run's secrets, egress starts with none to
add and logs the control plane gave egress nothing to wear. If it cannot reach
Spotlight within 1 minute, it logs
gave up redeeming the run's secrets; the sandbox goes out wearing none.
Hook calls
A hook that runs on a host is sent from a one-off
egress container, spotlight-egress-call-<id>, under the same allow-list and
address rules. Only http and https are sent, and redirects are not
followed.
Limitations
- Egress cannot itself send through another proxy.
- A runner a push pool provisions has no egress. A run that uses secrets, or reaches Spotlight's API, is never reserved for one; it waits for a host.
CURSOR_API_KEYand any other variable not listed under Harness keys enter the sandbox as they are.- Logins that refresh their own tokens are not added.