Networking & ports
Expose sandbox ports with signed preview URLs, revoke access, and filter outbound traffic per sandbox.
Networking controls traffic in and out of a sandbox. Expose ports through signed preview URLs. Filter outbound traffic per sandbox by address or hostname, and add headers to outbound requests.
Expose ports at create
expose accepts up to 8 ports. Ports open when the sandbox reaches running.
const sbx = await client.sandboxes.create(
{ expose: [{ port: 8000 }, { port: 3000, public: true }] },
{ waitForRunning: true },
);
sbx.exposedUrl(8000);sbx = client.sandboxes.create(
expose=[{"port": 8000}, {"port": 3000, "public": True}],
wait_for_running=True,
)
sbx.exposed_url(8000)# no create flag in the CLI — expose after create: pt sandbox expose <id> <port>curl -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"template": "pt-base", "expose": [{"port": 8000}, {"port": 3000, "public": true}]}'Expose a port after create
The sandbox must be running.
const { url, token } = await sbx.expose(8000, { ttlSeconds: 3600 });res = sbx.expose(8000, ttl_seconds=3600)
print(res["url"])pt sandbox expose $ID 8000
# open the preview URL in your browser:
pt sandbox open $ID 8000curl -X POST "$PT_API_URL/v1/sandboxes/$ID/expose" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"port": 8000, "ttl_seconds": 3600}'The response carries the preview url. For private exposures, it also carries the token.
| Rule | Behavior |
|---|---|
| Default token TTL | 1 day |
public: true | Drops the token; anyone with the URL can reach the port |
| Re-expose a port | Mints a fresh token with a new expiry |
Use preview URLs
https://<port>-<sbxid-short>.sbx.platinum.dev/<path>?t=<hmac><sbxid-short>is the lowercase ULID tail ofsbx_<ULID>. Drop thesbx_prefix.- Private exposures need the token, as
?t=or thex-pt-preview-tokenheader. Public exposures need none. - The proxy passes WebSocket upgrades end-to-end.
- A request to a
stoppedorarchivedsandbox auto-resumes it, then forwards the request.
Revoke one token
Revoke a leaked token without removing the exposure.
await sbx.revokeExposeToken(token, "leaked in a screenshot");sbx.revoke_expose_token(token, reason="leaked in a screenshot")pt sandbox revoke $ID "<the t= value>" --reason "leaked in a screenshot"curl -X POST "$PT_API_URL/v1/sandboxes/$ID/expose/revoke" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"token": "<the t= value>", "reason": "leaked"}'Unexpose a port
Warning: unexpose kills every token ever minted for the port. Tokens stay dead after a later re-expose.
await sbx.unexpose(8000);sbx.unexpose(8000)pt sandbox unexpose $ID 8000curl -X DELETE "$PT_API_URL/v1/sandboxes/$ID/expose/8000" \
-H "Authorization: Bearer $PT_TOKEN"Block internet access
Set allowInternetAccess to false at create. The policy applies to the sandbox interface before the sandbox boots. A blocked sandbox never has a route out.
const sbx = await client.sandboxes.create(
{ allowInternetAccess: false },
{ waitForRunning: true },
);sbx = client.sandboxes.create(allow_internet_access=False, wait_for_running=True)pt sandbox network $ID --no-internetcurl -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"template": "pt-base", "allow_internet_access": false}'allowInternetAccess: false is the same policy as denyOut: ["0.0.0.0/0"].
Allow only some destinations
allowOut accepts IP addresses, CIDR blocks, hostnames and *. wildcards. denyOut accepts addresses only.
Explicit address allows take first precedence. Explicit address denies beat hostname rules. A hostname rule beats only the catch-all default deny.
Warning: a hostname allow-list needs denyOut to block everything else. The API rejects the combination without it.
Use the literal 0.0.0.0/0 as the default-deny marker. Combining narrower blocks does not enable hostname exceptions.
const sbx = await client.sandboxes.create({
network: {
allowOut: ["api.example.com", "*.github.com", "8.8.8.8"],
denyOut: ({ allTraffic }) => [allTraffic],
},
});sbx = client.sandboxes.create(network={
"allow_out": ["api.example.com", "*.github.com", "8.8.8.8"],
"deny_out": lambda ctx: [ctx.all_traffic],
})pt sandbox network $ID --allow-out api.example.com,*.github.com,8.8.8.8 --deny-out allcurl -X POST "$PT_API_URL/v1/sandboxes/$ID/network" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"allow_out": ["api.example.com", "*.github.com", "8.8.8.8"], "deny_out": ["0.0.0.0/0"]}'*.github.com matches any subdomain of github.com. It does not match github.com itself.
Platinum reads the hostname from the TLS server name on port 443 and the Host header on port 80. Traffic on other ports is filtered by address only. A hostname allow-list also permits DNS to 1.1.1.1 and 8.8.8.8 on port 53.
Platinum resolves an allowed hostname itself and connects to that result. A sandbox cannot reach a different address by claiming an allowed name.
Combine secrets with network access
Secret routing and network access are independent. A binding adds an exact credential route without replacing the user-owned network policy.
| Network access | Result after attaching a secret |
|---|---|
| Open | Every destination remains reachable. Only the secret's exact HTTPS hosts receive the credential. |
| Restricted | The secret's exact hosts become reachable. Every other destination remains blocked. |
| Custom | Existing rules remain active. Explicit address denies still block matching secret destinations. |
Removing the final binding removes only its managed routes. It restores the exact user-owned network policy.
Restricted access limits where authenticated data can move. It does not prevent the sandbox from using the credential against an approved service.
Warning: credential transforms support HTTP/1.1 over TCP port 443. They do not support QUIC, HTTP/3, HTTP/2, gRPC, ECH, mTLS, or certificate pinning.
Unrelated passthrough traffic can still use every protocol allowed by the network policy.
Update the policy on a running sandbox
updateNetwork replaces the whole policy. It does not merge. An empty object clears every rule.
await sbx.updateNetwork({ denyOut: ["8.8.8.8"] });
await sbx.updateNetwork({});sbx.update_network({"deny_out": ["8.8.8.8"]})
sbx.update_network({})pt sandbox network $ID --deny-out 8.8.8.8
pt sandbox network $ID --showcurl -X POST "$PT_API_URL/v1/sandboxes/$ID/network" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"deny_out": ["8.8.8.8"]}'The policy survives stop and start. Read it back with GET /v1/sandboxes/$ID/network.
Add headers to outbound requests
A rule injects headers into requests to one host. Register rules under network.rules.
Warning: a rule grants no access. List the host in allowOut as well when you also block other traffic.
await sbx.updateNetwork({
rules: { "api.example.com": [{ transform: { headers: { "X-Tenant": "acme" } } }] },
});sbx.update_network({
"rules": {"api.example.com": [{"transform": {"headers": {"X-Tenant": "acme"}}}]},
})pt sandbox network $ID --header 'api.example.com:X-Tenant:acme'curl -X POST "$PT_API_URL/v1/sandboxes/$ID/network" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"rules": {"api.example.com": [{"transform": {"headers": {"X-Tenant": "acme"}}}]}}'Platinum terminates TLS for a host that has a rule. Header injection into HTTPS is impossible otherwise. Platinum generates a certificate authority for that sandbox alone, trusts it inside the sandbox, and verifies the origin certificate on the outbound connection. Every host without a rule passes through untouched.
Headers that control message framing are rejected. That list covers Host, Content-Length, Transfer-Encoding, Connection, Upgrade, Keep-Alive, TE, Trailer, Expect and the Proxy- headers.
A rule's header values are part of the sandbox's policy, and readable back with GET /v1/sandboxes/$ID/network. For a credential, use a secret instead: the value is encrypted, is never returned by any endpoint, and is written into the header by the proxy on port 443 only. A rule carrying a secret is refused on port 80 with 426 Upgrade Required rather than sent in the clear. A rule of your own static values, like the X-Tenant above, applies on either port.
Understand a blocked connection
A blocked TCP connection can still look open inside the sandbox. Platinum accepts the connection before it reads the destination hostname.
Check for an application-level response instead. A blocked HTTP request returns 403 with the header X-Platinum-Egress: blocked. A blocked HTTPS connection closes without a TLS handshake.
Set an address policy (original API)
setEgressPolicy filters by CIDR only. It remains supported and maps onto the same enforcement.
Caveat: the egress policy is not a hard security boundary. A malicious process in the sandbox can spoof its source IP to evade the rules.
169.254.0.0/16 (and its IPv6 equivalents) is handled host-wide on the forward path, before any per-sandbox policy is evaluated. Unlike the per-sandbox rules, that guard matches on destination only, so it cannot be evaded by spoofing the source address. Keeping 169.254.169.254/32 in your own policy is harmless and still recommended as defense in depth. Operators can extend the host-wide list with PT_EGRESS_BLOCK_CIDRS in /etc/platinum/host.env (comma-separated CIDRs): worth adding 168.63.129.16/32 on Azure and 100.100.100.200/32 on Alibaba, since those are ordinary routable addresses the link-local range does not cover.
await sbx.setEgressPolicy({ mode: "allow", cidrs: ["10.0.0.0/8", "169.254.169.254/32"] });sbx.set_egress_policy("allow", ["10.0.0.0/8", "169.254.169.254/32"])pt sandbox egress $ID --mode allow --cidrs 10.0.0.0/8,169.254.169.254/32
# clear it (allow all outbound again):
pt sandbox egress-clear $IDcurl -X POST "$PT_API_URL/v1/sandboxes/$ID/egress-policy" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{"mode": "allow", "cidrs": ["10.0.0.0/8", "169.254.169.254/32"]}'The mode names the list's role, not the default verdict.
mode | Listed CIDRs | Everything else |
|---|---|---|
allow | dropped | accepted |
deny | accepted | dropped |
Block all egress with {"mode":"deny","cidrs":[]} or {"mode":"allow","cidrs":["0.0.0.0/0"]}.
Know what is reachable
From inside a sandbox:
| Destination | Reachable? |
|---|---|
| Internet | Yes, subject to the network policy |
Own ports, via 127.0.0.1 | Yes |
Use the dashboard
The sandbox detail view has a Ports tab. Expose, unexpose, and copy preview URLs there.\n\nThe same view has a network panel. Edit the reachable and blocked lists, add request headers, and read one sentence describing what the policy does. Changes apply immediately without a restart.
See also
- Templates — bake services into the rootfs that start on boot
- Snapshots — exposed ports survive stop and resume
- API reference — full request and response schemas